Changeset 8133
- Timestamp:
- 10/24/2026 12:31:35 AM (2 hours ago)
- Location:
- branches/2.6
- Files:
-
- 7 edited
-
CHANGELOG.md (modified) (1 diff)
-
src/includes/common/functions.php (modified) (1 diff)
-
src/includes/core/cache.php (modified) (2 diffs)
-
src/includes/core/filters.php (modified) (1 diff)
-
src/includes/forums/functions.php (modified) (10 diffs)
-
tests/phpunit/testcases/replies/password-visibility.php (modified) (2 diffs)
-
tests/phpunit/testcases/search/template/visibility.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
branches/2.6/CHANGELOG.md
r8130 r8133 20 20 ### Security 21 21 22 - Prevented password-protected forum content from influencing search results. 22 23 - Required permission to manage both forums before moving a topic between them. 23 24 - Kept password-protected topic and reply content out of BuddyPress activity. -
branches/2.6/src/includes/common/functions.php
r8097 r8133 121 121 " AND ( {$bbp_db->posts}.post_type <> %s OR EXISTS ( SELECT 1 FROM {$bbp_db->posts} AS bbp_parent_topic WHERE bbp_parent_topic.ID = {$bbp_db->posts}.post_parent AND bbp_parent_topic.post_type = %s AND bbp_parent_topic.post_status IN ({$placeholders}) ) )", 122 122 array_merge( array( bbp_get_reply_post_type(), bbp_get_topic_post_type() ), $statuses ) 123 ); 124 125 return $where; 126 } 127 128 /** 129 * Exclude password-protected bbPress matches from search queries. 130 * 131 * Password-protected content stays out of public search results even after its 132 * password has been entered. Direct requests continue to use WordPress's post 133 * password cookie normally. 134 * 135 * @since 2.6.21 bbPress (r8133) 136 * 137 * @param string $where SQL WHERE clause. 138 * @param WP_Query $posts_query Posts query. 139 * @return string SQL WHERE clause. 140 */ 141 function bbp_password_protected_search_where( $where = '', $posts_query = null ) { 142 if ( ! $posts_query instanceof WP_Query || ! $posts_query->get( '_bbp_password_protected_search' ) ) { 143 return $where; 144 } 145 146 $bbp_db = bbp_db(); 147 $where .= $bbp_db->prepare( 148 " AND ( {$bbp_db->posts}.post_type NOT IN ( %s, %s, %s ) OR CHAR_LENGTH( {$bbp_db->posts}.post_password ) = 0 )", 149 bbp_get_forum_post_type(), 150 bbp_get_topic_post_type(), 151 bbp_get_reply_post_type() 152 ); 153 154 // Metadata is a fallback, not a replacement for canonical forum ancestry. 155 $forum_ids = wp_parse_id_list( $posts_query->get( '_bbp_password_protected_forum_ids' ) ); 156 $topic_forum_sql = ''; 157 if ( ! empty( $forum_ids ) ) { 158 $placeholders = implode( ', ', array_fill( 0, count( $forum_ids ), '%d' ) ); 159 $where .= $bbp_db->prepare( 160 " AND ( {$bbp_db->posts}.post_type <> %s OR {$bbp_db->posts}.ID NOT IN ({$placeholders}) ) 161 AND ( {$bbp_db->posts}.post_type <> %s OR {$bbp_db->posts}.post_parent NOT IN ({$placeholders}) )", 162 array_merge( array( bbp_get_forum_post_type() ), $forum_ids, array( bbp_get_topic_post_type() ), $forum_ids ) 163 ); 164 $topic_forum_sql = $bbp_db->prepare( " AND bbp_password_topic.post_parent NOT IN ({$placeholders})", $forum_ids ); 165 } 166 167 // Follow reply ancestry without changing existing parent-topic status rules. 168 $where .= $bbp_db->prepare( 169 " AND ( {$bbp_db->posts}.post_type <> %s OR EXISTS ( SELECT 1 FROM {$bbp_db->posts} AS bbp_password_topic WHERE bbp_password_topic.ID = {$bbp_db->posts}.post_parent AND bbp_password_topic.post_type = %s AND CHAR_LENGTH( bbp_password_topic.post_password ) = 0{$topic_forum_sql} ) )", 170 bbp_get_reply_post_type(), 171 bbp_get_topic_post_type() 123 172 ); 124 173 -
branches/2.6/src/includes/core/cache.php
r7662 r8133 130 130 * @since 2.1.0 bbPress (r4041) 131 131 * @since 2.6.0 bbPress (r6053) Introduced the `$post_id` parameter. 132 * @since 2.6.21 bbPress (r8133) Invalidate queries when a bbPress parent is missing. 132 133 * 133 134 * @param int $post_id The post id. … … 158 159 do_action( 'bbp_clean_post_cache', $post->ID, $post ); 159 160 160 // Invalidate parent caches 161 if ( ! empty( $post->post_parent ) ) { 162 clean_post_cache( $post->post_parent ); 161 // Invalidate existing parent caches, including non-bbPress parents. 162 $parent = ! empty( $post->post_parent ) ? get_post( $post->post_parent ) : null; 163 if ( ! empty( $parent ) ) { 164 clean_post_cache( $parent ); 165 } 163 166 164 // Only bump `last_changed` when forum-root is reached165 } else{167 // A missing or non-bbPress parent cannot propagate invalidation to a root. 168 if ( empty( $parent ) || ! in_array( $parent->post_type, $post_types, true ) ) { 166 169 wp_cache_set( 'last_changed', microtime(), 'bbpress_posts' ); 167 170 } -
branches/2.6/src/includes/core/filters.php
r8130 r8133 87 87 add_action( 'posts_pre_query', 'bbp_posts_pre_query', 10, 2 ); 88 88 add_filter( 'posts_where', 'bbp_public_topic_replies_where', 11, 2 ); 89 add_filter( 'posts_where', 'bbp_password_protected_search_where', 12, 2 ); 89 90 90 91 // User Creation -
branches/2.6/src/includes/forums/functions.php
r8127 r8133 2673 2673 2674 2674 /** 2675 * Return password-protected forum IDs and all of their descendants. 2676 * 2677 * @since 2.6.21 bbPress (r8133) 2678 * 2679 * @return int[] Password-protected forum IDs and descendant forum IDs. 2680 */ 2681 function bbp_get_password_protected_forum_ids() { 2682 $last_changed = wp_cache_get_last_changed( 'bbpress_posts' ); 2683 $cache_key = 'bbp_password_protected_forum_ids:' . $last_changed; 2684 $forum_ids = wp_cache_get( $cache_key, 'bbpress_posts' ); 2685 2686 if ( false === $forum_ids ) { 2687 // Length checks preserve whitespace-only passwords with padded collations. 2688 $bbp_db = bbp_db(); 2689 $query = $bbp_db->prepare( 2690 "SELECT ID FROM {$bbp_db->posts} WHERE post_type = %s AND CHAR_LENGTH( post_password ) > 0", 2691 bbp_get_forum_post_type() 2692 ); 2693 $forum_ids = wp_parse_id_list( $bbp_db->get_col( $query ) ); 2694 2695 if ( ! empty( $forum_ids ) ) { 2696 $forum_ids = bbp_forum_query_all_descendant_ids( $forum_ids ); 2697 } 2698 2699 wp_cache_set( $cache_key, $forum_ids, 'bbpress_posts' ); 2700 } 2701 2702 return (array) apply_filters( 'bbp_get_password_protected_forum_ids', $forum_ids ); 2703 } 2704 2705 /** 2675 2706 * Returns the forum IDs that should be excluded from various views & queries, 2676 2707 * based on the current user's capabilities. … … 2870 2901 * @since 2.3.0 bbPress (r4808) 2871 2902 * @since 2.6.19 bbPress (r7692) Exclude restricted forum IDs from post__in queries. 2903 * @since 2.6.21 bbPress (r8133) Exclude password-restricted content from searches. 2872 2904 * 2873 2905 * @param WP_Query $posts_query … … 2887 2919 } 2888 2920 2889 // Preserve the main post list query without exempting admin-ajax queries 2890 if ( is_admin() && ! wp_doing_ajax() && isset( $_REQUEST['post_status'] ) && isset( $GLOBALS['pagenow'] ) && ( 'edit.php' === $GLOBALS['pagenow'] ) && $posts_query->is_main_query() && current_user_can( 'moderate' ) ) { 2921 // Preserve the existing explicit-status admin exemption. 2922 $is_admin_list = is_admin() && ! wp_doing_ajax() && isset( $GLOBALS['pagenow'] ) && ( 'edit.php' === $GLOBALS['pagenow'] ) && $posts_query->is_main_query() && current_user_can( 'moderate' ); 2923 if ( $is_admin_list && isset( $_REQUEST['post_status'] ) ) { 2891 2924 return; 2892 2925 } … … 2921 2954 // Mark this query as normalized 2922 2955 $posts_query->set( '_bbp_forum_visibility_normalized', true ); 2956 2957 // Singular requests can still contain a search term that WordPress uses in SQL. 2958 $password_search = $posts_query->is_search() || ( '' !== (string) $posts_query->get( 's' ) ); 2959 2960 // Preserve admin lists and suggestions without skipping forum restrictions. 2961 if ( $is_admin_list || ( $posts_query->get( 'suppress_filters' ) && wp_doing_ajax() && current_user_can( 'moderate' ) ) ) { 2962 $password_search = false; 2963 } 2923 2964 2924 2965 // Separate non-bbPress post types from supported bbPress post types. … … 2934 2975 * without changing the requested non-bbPress post types. 2935 2976 */ 2936 if ( ! empty( $non_bbp_post_types ) && $posts_query->get( 'suppress_filters' ) ) { 2937 $posts_query->set( 'post_type', array_values( $non_bbp_post_types ) ); 2938 return; 2977 if ( $posts_query->get( 'suppress_filters' ) ) { 2978 if ( ! empty( $non_bbp_post_types ) ) { 2979 $posts_query->set( 'post_type', array_values( $non_bbp_post_types ) ); 2980 return; 2981 } 2982 2983 // Password inheritance requires a posts_where clause during searches. 2984 if ( $password_search ) { 2985 // Explicit IDs take precedence over post__in in WordPress. 2986 $posts_query->set( 'p', 0 ); 2987 $posts_query->set( 'page_id', 0 ); 2988 $posts_query->set( 'attachment_id', 0 ); 2989 $posts_query->set( 'post__in', array( 0 ) ); 2990 return; 2991 } 2939 2992 } 2940 2993 2941 2994 // Get forums to exclude. 2942 $forum_ids = bbp_exclude_forum_ids( 'array' ); 2995 $forum_ids = bbp_exclude_forum_ids( 'array' ); 2996 $password_forum_ids = array(); 2997 2998 /** 2999 * Search matches must not reveal whether password-protected content contains 3000 * a term. Add password-protected forum trees to the normal visibility rules 3001 * and exclude protected topics through the password-search clause. 3002 */ 3003 if ( $password_search ) { 3004 $password_forum_ids = bbp_get_password_protected_forum_ids(); 3005 $forum_ids = wp_parse_id_list( array_merge( $forum_ids, $password_forum_ids ) ); 3006 $posts_query->set( '_bbp_password_protected_search', true ); 3007 $posts_query->set( '_bbp_password_protected_forum_ids', $password_forum_ids ); 3008 } 2943 3009 2944 3010 // Forums … … 2951 3017 * clause, private and hidden content inherits the broader forum statuses. 2952 3018 */ 2953 if ( ! empty( $content_post_types ) ) {3019 if ( ! $is_admin_list && ! empty( $content_post_types ) ) { 2954 3020 $content_post_statuses = bbp_get_string_array_values( $posts_query->get( 'post_status' ) ); 2955 3021 … … 2965 3031 } 2966 3032 2967 // Add all supported forum visibilities to bbPress-only queries.2968 if ( empty( $non_bbp_post_types ) ) {3033 // Add forum visibilities without replacing WordPress admin-list statuses. 3034 if ( ! $is_admin_list && empty( $non_bbp_post_types ) ) { 2969 3035 $posts_query->set( 2970 3036 'post_status', … … 3016 3082 3017 3083 /** 3018 * Mixed queries need a post-type-aware SQL clause. A meta query would use an3019 * inner join and unintentionally remove non-bbPress posts that do not have3020 * bbPress forum metadata.3084 * Mixed queries need a post-type-aware clause to preserve non-bbPress posts. 3085 * Searches also use it to reject every excluded forum metadata value, rather 3086 * than allowing a duplicate public value to satisfy a NOT IN meta query. 3021 3087 */ 3022 if ( ! empty( $non_bbp_post_types ) ) {3088 if ( ! empty( $non_bbp_post_types ) || $password_search ) { 3023 3089 $posts_query->set( '_bbp_forum_visibility_post_types', $content_post_types ); 3024 3090 $posts_query->set( '_bbp_forum_visibility_forum_ids', $forum_ids ); 3025 return; 3026 } 3027 3028 // Get forum meta query. 3091 3092 // Mixed queries cannot use a meta join without losing unrelated posts. 3093 if ( ! empty( $non_bbp_post_types ) ) { 3094 return; 3095 } 3096 } 3097 3098 // Preserve the existing meta-query filter for bbPress-only queries. 3029 3099 $forum_meta_query = bbp_exclude_forum_ids( 'meta_query' ); 3030 3100 … … 3052 3122 3053 3123 /** 3054 * Excludes protected bbPress content from mixed post-type queries .3124 * Excludes protected bbPress content from mixed post-type queries and searches. 3055 3125 * 3056 3126 * A normal meta query cannot scope its join to bbPress post types, causing … … 3144 3214 3145 3215 /** 3146 * Filters the forum visibility SQL appended to mixed post-type queries.3216 * Filters the forum visibility SQL appended to mixed queries and searches. 3147 3217 * 3148 3218 * @since 2.6.15 bbPress (r7414) -
branches/2.6/tests/phpunit/testcases/replies/password-visibility.php
r8058 r8133 73 73 $this->set_current_user( $user_id ); 74 74 75 foreach ( array( '/?post_type=reply', '/?s=Protected&post_type=reply' ) as $url ) { 76 $this->go_to_without_reregistering_blocks( $url ); 77 $this->assertContains( $reply_id, wp_list_pluck( $GLOBALS['wp_query']->posts, 'ID' ) ); 78 $this->assertTrue( post_password_required( $reply_id ) ); 79 $this->assertStringNotContainsString( 'Protected core reply marker', get_the_content( null, false, $reply_id ) ); 80 } 75 $this->go_to_without_reregistering_blocks( '/?post_type=reply' ); 76 $this->assertContains( $reply_id, wp_list_pluck( $GLOBALS['wp_query']->posts, 'ID' ) ); 77 $this->assertTrue( post_password_required( $reply_id ) ); 78 $this->assertStringNotContainsString( 'Protected core reply marker', get_the_content( null, false, $reply_id ) ); 79 80 $this->go_to_without_reregistering_blocks( '/?s=Protected&post_type=reply' ); 81 $this->assertNotContains( $reply_id, wp_list_pluck( $GLOBALS['wp_query']->posts, 'ID' ) ); 81 82 } 82 83 … … 88 89 $this->assertFalse( post_password_required( $reply_id ) ); 89 90 $this->assertStringContainsString( 'Protected core reply marker', get_the_content( null, false, $reply_id ) ); 91 92 $this->go_to_without_reregistering_blocks( '/?s=Protected&post_type=reply' ); 93 $this->assertNotContains( $reply_id, wp_list_pluck( $GLOBALS['wp_query']->posts, 'ID' ) ); 90 94 } finally { 91 95 $this->set_current_user( 0 ); -
branches/2.6/tests/phpunit/testcases/search/template/visibility.php
r7624 r8133 68 68 $this->assertContains( $reply_id, wp_list_pluck( bbpress()->search_query->posts, 'ID' ) ); 69 69 } 70 71 /** 72 * @covers ::bbp_has_search_results 73 * @covers ::bbp_get_password_protected_forum_ids 74 * @covers ::bbp_password_protected_search_where 75 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 76 * @covers ::bbp_public_topic_replies_where 77 */ 78 public function test_search_excludes_password_protected_content_for_all_roles() { 79 $forum_id = $this->factory->forum->create( 80 array( 81 'post_content' => 'Protected forum search sentinel 8143', 82 'post_password' => 'forum-secret', 83 ) 84 ); 85 $topic_id = $this->factory->topic->create( 86 array( 87 'post_content' => 'Protected topic search sentinel 8462', 88 'post_parent' => $forum_id, 89 'topic_meta' => array( 'forum_id' => $forum_id ), 90 ) 91 ); 92 $reply_id = $this->factory->reply->create( 93 array( 94 'post_parent' => $topic_id, 95 'post_content' => 'Protected search oracle sentinel 4816', 96 'reply_meta' => array( 'forum_id' => $forum_id, 'topic_id' => $topic_id ), 97 ) 98 ); 99 $child_forum_id = $this->factory->forum->create( 100 array( 'post_parent' => $forum_id ) 101 ); 102 $child_topic_id = $this->factory->topic->create( 103 array( 104 'post_parent' => $child_forum_id, 105 'topic_meta' => array( 'forum_id' => $child_forum_id ), 106 ) 107 ); 108 $child_reply_id = $this->factory->reply->create( 109 array( 110 'post_parent' => $child_topic_id, 111 'post_content' => 'Nested protected search sentinel 5937', 112 'reply_meta' => array( 'forum_id' => $child_forum_id, 'topic_id' => $child_topic_id ), 113 ) 114 ); 115 $draft_forum_id = $this->factory->forum->create( 116 array( 117 'post_parent' => $forum_id, 118 'post_status' => 'draft', 119 ) 120 ); 121 $deep_forum_id = $this->factory->forum->create( 122 array( 'post_parent' => $draft_forum_id ) 123 ); 124 $deep_topic_id = $this->factory->topic->create( 125 array( 126 'post_parent' => $deep_forum_id, 127 'topic_meta' => array( 'forum_id' => $deep_forum_id ), 128 ) 129 ); 130 $deep_reply_id = $this->factory->reply->create( 131 array( 132 'post_parent' => $deep_topic_id, 133 'post_content' => 'Deep protected search sentinel 9164', 134 'reply_meta' => array( 'forum_id' => $deep_forum_id, 'topic_id' => $deep_topic_id ), 135 ) 136 ); 137 $public_forum_id = $this->factory->forum->create(); 138 $protected_topic_id = $this->factory->topic->create( 139 array( 140 'post_parent' => $public_forum_id, 141 'post_password' => 'topic-secret', 142 'topic_meta' => array( 'forum_id' => $public_forum_id ), 143 ) 144 ); 145 $topic_reply_id = $this->factory->reply->create( 146 array( 147 'post_parent' => $protected_topic_id, 148 'post_content' => 'Topic protected search sentinel 6249', 149 'reply_meta' => array( 'forum_id' => $public_forum_id, 'topic_id' => $protected_topic_id ), 150 ) 151 ); 152 $public_topic_id = $this->factory->topic->create( 153 array( 154 'post_parent' => $public_forum_id, 155 'topic_meta' => array( 'forum_id' => $public_forum_id ), 156 ) 157 ); 158 $public_reply_id = $this->factory->reply->create( 159 array( 160 'post_parent' => $public_topic_id, 161 'post_content' => 'Public search sentinel 7351', 162 'reply_meta' => array( 'forum_id' => $public_forum_id, 'topic_id' => $public_topic_id ), 163 ) 164 ); 165 $public_post_id = $this->factory->post->create( 166 array( 'post_content' => 'Topic protected search sentinel 6249' ) 167 ); 168 $restricted = array( 169 'Protected forum search sentinel 8143' => $forum_id, 170 'Protected topic search sentinel 8462' => $topic_id, 171 'Protected search oracle sentinel 4816' => $reply_id, 172 'Nested protected search sentinel 5937' => $child_reply_id, 173 'Deep protected search sentinel 9164' => $deep_reply_id, 174 'Topic protected search sentinel 6249' => $topic_reply_id, 175 ); 176 $user_ids = array( 0 ); 177 $this->assertFalse( bbp_has_search_results( array( 's' => 'Protected search oracle sentinel 4816' ) ) ); 178 $this->assertStringContainsString( 'post_password ) = 0', bbpress()->search_query->request ); 179 180 $custom_query = new WP_Query( 181 array( 182 'post_type' => bbp_get_reply_post_type(), 183 's' => 'Topic protected search sentinel 6249', 184 ) 185 ); 186 $this->assertNotContains( $topic_reply_id, wp_list_pluck( $custom_query->posts, 'ID' ) ); 187 188 $mixed_query = new WP_Query( 189 array( 190 'post_type' => array( 'post', bbp_get_reply_post_type() ), 191 's' => 'Topic protected search sentinel 6249', 192 ) 193 ); 194 $this->assertContains( $public_post_id, wp_list_pluck( $mixed_query->posts, 'ID' ) ); 195 $this->assertNotContains( $topic_reply_id, wp_list_pluck( $mixed_query->posts, 'ID' ) ); 196 197 $suppressed_query = new WP_Query( 198 array( 199 'post_type' => bbp_get_reply_post_type(), 200 's' => 'Topic protected search sentinel 6249', 201 'suppress_filters' => true, 202 ) 203 ); 204 $this->assertEmpty( $suppressed_query->posts ); 205 206 foreach ( array( '', bbp_get_participant_role(), bbp_get_moderator_role(), bbp_get_keymaster_role() ) as $role ) { 207 $user_id = $this->factory->user->create(); 208 $user = new WP_User( $user_id ); 209 210 if ( empty( $role ) ) { 211 $user->set_role( '' ); 212 } else { 213 bbp_set_user_role( $user_id, $role ); 214 } 215 216 $user_ids[] = $user_id; 217 } 218 219 foreach ( $user_ids as $user_id ) { 220 $this->set_current_user( $user_id ); 221 222 foreach ( $restricted as $search_term => $restricted_reply_id ) { 223 $this->assertFalse( bbp_has_search_results( array( 's' => $search_term ) ) ); 224 $this->assertNotContains( $restricted_reply_id, wp_list_pluck( bbpress()->search_query->posts, 'ID' ) ); 225 } 226 227 $this->assertTrue( bbp_has_search_results( array( 's' => 'Public search sentinel 7351' ) ) ); 228 $this->assertContains( $public_reply_id, wp_list_pluck( bbpress()->search_query->posts, 'ID' ) ); 229 } 230 231 require_once ABSPATH . WPINC . '/class-phpass.php'; 232 $hasher = new PasswordHash( 8, true ); 233 $cookie = 'wp-postpass_' . COOKIEHASH; 234 $old_cookie = isset( $_COOKIE[ $cookie ] ) ? $_COOKIE[ $cookie ] : null; 235 236 try { 237 $this->set_current_user( 0 ); 238 239 foreach ( array( 'forum-secret', 'topic-secret' ) as $password ) { 240 $_COOKIE[ $cookie ] = $hasher->HashPassword( $password ); 241 242 foreach ( $restricted as $search_term => $restricted_post_id ) { 243 $this->assertFalse( bbp_has_search_results( array( 's' => $search_term ) ) ); 244 $this->assertNotContains( $restricted_post_id, wp_list_pluck( bbpress()->search_query->posts, 'ID' ) ); 245 } 246 } 247 } finally { 248 if ( null === $old_cookie ) { 249 unset( $_COOKIE[ $cookie ] ); 250 } else { 251 $_COOKIE[ $cookie ] = $old_cookie; 252 } 253 } 254 } 255 256 /** 257 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 258 * @covers ::bbp_password_protected_search_where 259 */ 260 public function test_search_terms_remain_protected_in_singular_requests() { 261 $protected = $this->create_search_tree( 'forum-secret' ); 262 $public = $this->create_search_tree(); 263 264 foreach ( array( $protected, $public ) as $tree ) { 265 foreach ( array( 'forum', 'topic', 'reply' ) as $type ) { 266 $post_id = $tree[ $type ]; 267 $post = get_post( $post_id ); 268 269 foreach ( array( 'p' => $post_id, 'page_id' => $post_id, 'name' => $post->post_name ) as $selector => $value ) { 270 foreach ( array( 'sentinel', '0', 'absentterm' ) as $term ) { 271 $args = array( 272 'post_type' => $post->post_type, 273 $selector => $value, 274 's' => $term, 275 ); 276 $query = new WP_Query( $args ); 277 $ids = wp_list_pluck( $query->posts, 'ID' ); 278 279 if ( ( $tree === $public ) && ( 'absentterm' !== $term ) ) { 280 $this->assertContains( $post_id, $ids ); 281 } else { 282 $this->assertEmpty( $ids ); 283 $this->assertSame( 0, (int) $query->found_posts ); 284 } 285 286 // Explicit IDs must not override the fail-closed fallback. 287 $args['suppress_filters'] = true; 288 $suppressed = new WP_Query( $args ); 289 $this->assertEmpty( $suppressed->posts ); 290 } 291 } 292 293 // A request with no search term retains normal direct navigation. 294 $direct = new WP_Query( array( 'post_type' => $post->post_type, 'p' => $post_id ) ); 295 $this->assertContains( $post_id, wp_list_pluck( $direct->posts, 'ID' ) ); 296 } 297 } 298 299 $attachment = new WP_Query( array( 'post_type' => bbp_get_reply_post_type(), 'attachment_id' => $protected['reply'], 's' => 'sentinel', 'suppress_filters' => true ) ); 300 $this->assertEmpty( $attachment->posts ); 301 302 // Exercise the actual public main-query route, not only secondary queries. 303 $register_blocks = remove_action( 'bbp_register', 'bbp_register_blocks', 10 ); 304 try { 305 $this->go_to( add_query_arg( array( 'post_type' => bbp_get_reply_post_type(), 'p' => $protected['reply'], 's' => 'sentinel' ), home_url( '/' ) ) ); 306 $this->assertEmpty( $GLOBALS['wp_query']->posts ); 307 $this->go_to( add_query_arg( array( 'post_type' => bbp_get_reply_post_type(), 'p' => $public['reply'], 's' => 'sentinel' ), home_url( '/' ) ) ); 308 $this->assertContains( $public['reply'], wp_list_pluck( $GLOBALS['wp_query']->posts, 'ID' ) ); 309 } finally { 310 if ( $register_blocks ) { 311 add_action( 'bbp_register', 'bbp_register_blocks', 10 ); 312 } 313 } 314 } 315 316 /** 317 * @covers ::bbp_password_protected_search_where 318 * @covers ::bbp_public_topic_replies_where 319 * @covers ::_bbp_forum_visibility_where 320 */ 321 public function test_search_checks_canonical_ancestry_and_all_forum_metadata() { 322 $protected = $this->create_search_tree( 'forum-secret' ); 323 $public = $this->create_search_tree(); 324 $post_id = $this->factory->post->create( array( 'post_content' => 'Search ancestry sentinel 0' ) ); 325 326 foreach ( array( 'duplicate', 'stale', 'missing', 'fallback' ) as $metadata ) { 327 foreach ( array( 'topic', 'reply' ) as $type ) { 328 delete_post_meta( $protected[ $type ], '_bbp_forum_id' ); 329 330 if ( 'missing' !== $metadata ) { 331 add_post_meta( $protected[ $type ], '_bbp_forum_id', ( 'fallback' === $metadata ) ? $protected['forum'] : $public['forum'] ); 332 } 333 if ( 'duplicate' === $metadata ) { 334 add_post_meta( $protected[ $type ], '_bbp_forum_id', $protected['forum'] ); 335 } 336 } 337 338 if ( 'fallback' === $metadata ) { 339 wp_update_post( array( 'ID' => $protected['topic'], 'post_parent' => 0 ) ); 340 } 341 342 foreach ( array( bbp_get_post_types(), array_merge( array( 'post' ), bbp_get_post_types() ) ) as $post_types ) { 343 $query = new WP_Query( array( 'post_type' => $post_types, 's' => 'sentinel', 'posts_per_page' => -1 ) ); 344 $ids = wp_list_pluck( $query->posts, 'ID' ); 345 346 foreach ( $protected as $protected_id ) { 347 $this->assertNotContains( $protected_id, $ids ); 348 } 349 foreach ( $public as $public_id ) { 350 $this->assertContains( $public_id, $ids ); 351 } 352 if ( in_array( 'post', $post_types, true ) ) { 353 $this->assertContains( $post_id, $ids ); 354 $this->assertSame( 4, (int) $query->found_posts ); 355 } else { 356 $this->assertSame( 3, (int) $query->found_posts ); 357 } 358 } 359 } 360 } 361 362 /** 363 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 364 */ 365 public function test_search_excludes_whitespace_only_passwords() { 366 foreach ( array( 'forum', 'topic', 'reply' ) as $type ) { 367 $tree = $this->create_search_tree(); 368 wp_update_post( array( 'ID' => $tree[ $type ], 'post_password' => ' ' ) ); 369 $this->assertTrue( post_password_required( $tree[ $type ] ) ); 370 371 $query = new WP_Query( array( 'post_type' => bbp_get_post_types(), 'post__in' => array_values( $tree ), 's' => 'sentinel' ) ); 372 $ids = wp_list_pluck( $query->posts, 'ID' ); 373 $types = array_keys( $tree ); 374 $index = array_search( $type, $types, true ); 375 376 foreach ( array_slice( $tree, $index ) as $protected_id ) { 377 $this->assertNotContains( $protected_id, $ids ); 378 } 379 } 380 } 381 382 /** 383 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 384 */ 385 public function test_authorized_admin_search_does_not_require_a_status_parameter() { 386 $protected = $this->create_search_tree( 'forum-secret' ); 387 $old_request = $_REQUEST; 388 $old_pagenow = isset( $GLOBALS['pagenow'] ) ? $GLOBALS['pagenow'] : null; 389 $old_query = $GLOBALS['wp_the_query']; 390 391 try { 392 unset( $_REQUEST['post_status'] ); 393 set_current_screen( 'edit-topic' ); 394 $GLOBALS['pagenow'] = 'edit.php'; 395 396 foreach ( array( bbp_get_moderator_role(), bbp_get_keymaster_role() ) as $role ) { 397 $user_id = $this->factory->user->create(); 398 bbp_set_user_role( $user_id, $role ); 399 $this->set_current_user( $user_id ); 400 $query = new WP_Query(); 401 $GLOBALS['wp_the_query'] = $query; 402 $query->query( array( 'post_type' => bbp_get_topic_post_type(), 's' => 'sentinel' ) ); 403 $this->assertContains( $protected['topic'], wp_list_pluck( $query->posts, 'ID' ) ); 404 405 $secondary = new WP_Query( array( 'post_type' => bbp_get_topic_post_type(), 's' => 'sentinel' ) ); 406 $this->assertEmpty( $secondary->posts ); 407 408 // Admin context alone does not exempt an unauthorized main query. 409 $this->set_current_user( 0 ); 410 $query = new WP_Query(); 411 $GLOBALS['wp_the_query'] = $query; 412 $query->query( array( 'post_type' => bbp_get_topic_post_type(), 's' => 'sentinel' ) ); 413 $this->assertEmpty( $query->posts ); 414 } 415 } finally { 416 $_REQUEST = $old_request; 417 $GLOBALS['wp_the_query'] = $old_query; 418 $GLOBALS['pagenow'] = $old_pagenow; 419 set_current_screen( 'front' ); 420 } 421 } 422 423 /** 424 * @covers ::bbp_get_password_protected_forum_ids 425 */ 426 public function test_password_forum_lookup_caches_empty_results_and_invalidates_with_posts() { 427 global $wpdb; 428 429 $forum_id = $this->factory->forum->create(); 430 $this->assertSame( array(), bbp_get_password_protected_forum_ids() ); 431 $queries = $wpdb->num_queries; 432 $this->assertSame( array(), bbp_get_password_protected_forum_ids() ); 433 $this->assertSame( $queries, $wpdb->num_queries ); 434 435 wp_update_post( array( 'ID' => $forum_id, 'post_password' => 'forum-secret' ) ); 436 $this->assertContains( $forum_id, bbp_get_password_protected_forum_ids() ); 437 $queries = $wpdb->num_queries; 438 $this->assertContains( $forum_id, bbp_get_password_protected_forum_ids() ); 439 $this->assertSame( $queries, $wpdb->num_queries ); 440 441 $child_id = $this->factory->forum->create( array( 'post_parent' => $forum_id ) ); 442 $this->assertContains( $child_id, bbp_get_password_protected_forum_ids() ); 443 444 wp_update_post( array( 'ID' => $child_id, 'post_parent' => 0 ) ); 445 $this->assertNotContains( $child_id, bbp_get_password_protected_forum_ids() ); 446 447 wp_update_post( array( 'ID' => $forum_id, 'post_password' => '' ) ); 448 $this->assertSame( array(), bbp_get_password_protected_forum_ids() ); 449 } 450 451 /** 452 * @group multisite 453 * @covers ::bbp_get_password_protected_forum_ids 454 */ 455 public function test_password_forum_lookup_is_scoped_to_the_current_site() { 456 if ( ! is_multisite() ) { 457 $this->markTestSkipped( 'Requires multisite.' ); 458 } 459 460 $forum_id = $this->factory->forum->create( array( 'post_password' => 'forum-secret' ) ); 461 $site_id = $this->factory->blog->create(); 462 $this->assertContains( $forum_id, bbp_get_password_protected_forum_ids() ); 463 464 switch_to_blog( $site_id ); 465 try { 466 $this->assertSame( array(), bbp_get_password_protected_forum_ids() ); 467 $other_id = $this->factory->forum->create( array( 'post_password' => 'other-secret' ) ); 468 $this->assertSame( array( $other_id ), bbp_get_password_protected_forum_ids() ); 469 } finally { 470 restore_current_blog(); 471 } 472 473 $this->assertSame( array( $forum_id ), bbp_get_password_protected_forum_ids() ); 474 } 475 476 /** 477 * @covers ::bbp_password_protected_search_where 478 * @covers ::bbp_public_topic_replies_where 479 */ 480 public function test_password_search_preserves_secondary_reply_status_opt_ins() { 481 $public = $this->create_search_tree(); 482 $private = $this->create_search_tree(); 483 $pending = $this->create_search_tree(); 484 wp_update_post( array( 'ID' => $private['topic'], 'post_status' => bbp_get_private_status_id() ) ); 485 wp_update_post( array( 'ID' => $pending['topic'], 'post_status' => bbp_get_pending_status_id() ) ); 486 $args = array( 487 'post_type' => bbp_get_reply_post_type(), 488 'post__in' => array( $public['reply'], $private['reply'], $pending['reply'] ), 489 's' => 'sentinel', 490 'fields' => 'ids', 491 'posts_per_page' => -1, 492 ); 493 494 foreach ( array( bbp_get_moderator_role(), bbp_get_keymaster_role() ) as $role ) { 495 $user_id = $this->factory->user->create(); 496 bbp_set_user_role( $user_id, $role ); 497 $this->set_current_user( $user_id ); 498 499 // Password protection must not opt a custom query into status filtering. 500 $query = new WP_Query( $args ); 501 $this->assertContains( $public['reply'], $query->posts ); 502 $this->assertContains( $private['reply'], $query->posts ); 503 $this->assertContains( $pending['reply'], $query->posts ); 504 505 // Existing public-listing and private-search opt-ins retain their meaning. 506 $public_args = array_merge( $args, array( '_bbp_public_topic_replies' => true ) ); 507 $query = new WP_Query( $public_args ); 508 $this->assertContains( $public['reply'], $query->posts ); 509 $this->assertNotContains( $private['reply'], $query->posts ); 510 $this->assertNotContains( $pending['reply'], $query->posts ); 511 512 $public_args['_bbp_search_private_topic_replies'] = true; 513 $query = new WP_Query( $public_args ); 514 $this->assertContains( $public['reply'], $query->posts ); 515 $this->assertContains( $private['reply'], $query->posts ); 516 $this->assertNotContains( $pending['reply'], $query->posts ); 517 } 518 } 519 520 /** 521 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 522 */ 523 public function test_ajax_password_exemption_preserves_existing_forum_filters() { 524 $excluded = $this->create_search_tree(); 525 $protected = $this->create_search_tree( 'forum-secret' ); 526 $user_id = $this->factory->user->create(); 527 bbp_set_user_role( $user_id, bbp_get_moderator_role() ); 528 $this->set_current_user( $user_id ); 529 530 // Model an integration's additional forum restrictions, not stock role caps. 531 $filter = function( $value, $forum_ids, $type ) use ( $excluded ) { 532 if ( 'array' === $type ) { 533 return wp_parse_id_list( array_merge( $value, array( $excluded['forum'] ) ) ); 534 } 535 if ( 'meta_query' === $type ) { 536 return array( 537 'key' => '_bbp_forum_id', 538 'value' => array( $excluded['forum'] ), 539 'compare' => 'NOT IN', 540 'type' => 'NUMERIC', 541 ); 542 } 543 544 return $value; 545 }; 546 add_filter( 'bbp_exclude_forum_ids', $filter, 10, 3 ); 547 add_filter( 'wp_doing_ajax', '__return_true' ); 548 549 try { 550 $args = array( 551 'post_type' => bbp_get_topic_post_type(), 552 'post__in' => array( $excluded['topic'], $protected['topic'] ), 553 's' => 'sentinel', 554 'fields' => 'ids', 555 'suppress_filters' => true, 556 ); 557 $query = new WP_Query( $args ); 558 $this->assertNotContains( $excluded['topic'], $query->posts ); 559 $this->assertContains( $protected['topic'], $query->posts ); 560 $this->assertNotEmpty( $query->get( 'meta_query' ) ); 561 562 $this->set_current_user( 0 ); 563 $query = new WP_Query( $args ); 564 $this->assertEmpty( $query->posts ); 565 566 $this->set_current_user( $user_id ); 567 remove_filter( 'wp_doing_ajax', '__return_true' ); 568 $query = new WP_Query( $args ); 569 $this->assertEmpty( $query->posts ); 570 } finally { 571 remove_filter( 'bbp_exclude_forum_ids', $filter, 10 ); 572 remove_filter( 'wp_doing_ajax', '__return_true' ); 573 } 574 } 575 576 /** 577 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 578 */ 579 public function test_empty_search_excludes_password_content_but_archive_does_not() { 580 $protected = $this->create_search_tree( 'forum-secret' ); 581 $public = $this->create_search_tree(); 582 $args = array( 583 'post_type' => bbp_get_topic_post_type(), 584 'post__in' => array( $protected['topic'], $public['topic'] ), 585 'fields' => 'ids', 586 's' => '', 587 ); 588 $query = new WP_Query( $args ); 589 $this->assertTrue( $query->is_search() ); 590 $this->assertContains( $public['topic'], $query->posts ); 591 $this->assertNotContains( $protected['topic'], $query->posts ); 592 593 unset( $args['s'] ); 594 $query = new WP_Query( $args ); 595 $this->assertContains( $public['topic'], $query->posts ); 596 $this->assertContains( $protected['topic'], $query->posts ); 597 $this->assertTrue( post_password_required( $protected['topic'] ) ); 598 } 599 600 /** 601 * @covers ::bbp_clean_post_cache 602 * @covers ::bbp_get_password_protected_forum_ids 603 */ 604 public function test_password_cache_invalidates_with_missing_or_non_forum_parents() { 605 $other_post = $this->factory->post->create(); 606 foreach ( array( 0, $other_post + 100000, $other_post ) as $parent_id ) { 607 $tree = $this->create_search_tree(); 608 wp_update_post( array( 'ID' => $tree['forum'], 'post_parent' => $parent_id ) ); 609 $this->assertSame( $parent_id, (int) get_post_field( 'post_parent', $tree['forum'] ) ); 610 611 // Warm the exclusion cache before an ordinary WordPress password update. 612 $this->assertNotContains( $tree['forum'], bbp_get_password_protected_forum_ids() ); 613 wp_update_post( array( 'ID' => $tree['forum'], 'post_password' => 'forum-secret' ) ); 614 $this->assertContains( $tree['forum'], bbp_get_password_protected_forum_ids(), 'Parent: ' . $parent_id ); 615 616 $args = array( 617 'post_type' => array( bbp_get_topic_post_type(), bbp_get_reply_post_type() ), 618 'post__in' => array( $tree['topic'], $tree['reply'] ), 619 's' => 'sentinel', 620 'fields' => 'ids', 621 ); 622 $query = new WP_Query( $args ); 623 $this->assertEmpty( $query->posts ); 624 625 wp_update_post( array( 'ID' => $tree['forum'], 'post_password' => '' ) ); 626 $this->assertNotContains( $tree['forum'], bbp_get_password_protected_forum_ids() ); 627 $query = new WP_Query( $args ); 628 $this->assertContains( $tree['topic'], $query->posts ); 629 $this->assertContains( $tree['reply'], $query->posts ); 630 } 631 } 632 633 /** 634 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 635 */ 636 public function test_authorized_admin_forum_all_view_retains_non_public_statuses() { 637 $old_request = $_REQUEST; 638 $old_pagenow = isset( $GLOBALS['pagenow'] ) ? $GLOBALS['pagenow'] : null; 639 $old_query = $GLOBALS['wp_the_query']; 640 $user_id = $this->factory->user->create(); 641 bbp_set_user_role( $user_id, bbp_get_moderator_role() ); 642 $this->set_current_user( $user_id ); 643 $forum_ids = array(); 644 645 foreach ( array( bbp_get_public_status_id(), bbp_get_private_status_id(), bbp_get_hidden_status_id(), 'draft', 'pending', 'future' ) as $status ) { 646 $forum_id = $this->factory->forum->create( array( 'post_author' => $user_id ) ); 647 $post_date = gmdate( 'Y-m-d H:i:s', time() + ( 'future' === $status ? DAY_IN_SECONDS : -DAY_IN_SECONDS ) ); 648 wp_update_post( 649 array( 650 'ID' => $forum_id, 651 'post_status' => $status, 652 'post_date' => $post_date, 653 'post_date_gmt' => $post_date, 654 ) 655 ); 656 $forum_ids[] = $forum_id; 657 $this->assertSame( $status, get_post_status( $forum_id ) ); 658 } 659 660 try { 661 unset( $_REQUEST['post_status'] ); 662 set_current_screen( 'edit-forum' ); 663 $GLOBALS['pagenow'] = 'edit.php'; 664 $query = new WP_Query(); 665 $GLOBALS['wp_the_query'] = $query; 666 $query->query( array( 'post_type' => bbp_get_forum_post_type(), 'post__in' => $forum_ids, 'fields' => 'ids', 'posts_per_page' => -1 ) ); 667 foreach ( $forum_ids as $forum_id ) { 668 $this->assertContains( $forum_id, $query->posts ); 669 } 670 } finally { 671 $_REQUEST = $old_request; 672 $GLOBALS['wp_the_query'] = $old_query; 673 $GLOBALS['pagenow'] = $old_pagenow; 674 set_current_screen( 'front' ); 675 } 676 } 677 678 /** 679 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 680 */ 681 public function test_admin_all_view_preserves_custom_forum_exclusions() { 682 $excluded = $this->create_search_tree(); 683 $protected = $this->create_search_tree( 'forum-secret' ); 684 $public = $this->create_search_tree(); 685 $old_request = $_REQUEST; 686 $old_pagenow = isset( $GLOBALS['pagenow'] ) ? $GLOBALS['pagenow'] : null; 687 $old_query = $GLOBALS['wp_the_query']; 688 $user_id = $this->factory->user->create(); 689 bbp_set_user_role( $user_id, bbp_get_moderator_role() ); 690 $this->set_current_user( $user_id ); 691 692 $filter = function( $value, $forum_ids, $type ) use ( $excluded ) { 693 if ( 'array' === $type ) { 694 return wp_parse_id_list( array_merge( $value, array( $excluded['forum'] ) ) ); 695 } 696 if ( 'meta_query' === $type ) { 697 return array( 698 'key' => '_bbp_forum_id', 699 'value' => array( $excluded['forum'] ), 700 'compare' => 'NOT IN', 701 'type' => 'NUMERIC', 702 ); 703 } 704 705 return $value; 706 }; 707 add_filter( 'bbp_exclude_forum_ids', $filter, 10, 3 ); 708 709 try { 710 unset( $_REQUEST['post_status'] ); 711 $GLOBALS['pagenow'] = 'edit.php'; 712 713 foreach ( array( 'forum', 'topic', 'reply' ) as $type ) { 714 set_current_screen( 'edit-' . get_post_type( $public[ $type ] ) ); 715 foreach ( array( null, 'sentinel' ) as $term ) { 716 $query = new WP_Query(); 717 $GLOBALS['wp_the_query'] = $query; 718 $args = array( 719 'post_type' => get_post_type( $public[ $type ] ), 720 'post__in' => array( $excluded[ $type ], $protected[ $type ], $public[ $type ] ), 721 'fields' => 'ids', 722 'posts_per_page' => -1, 723 ); 724 if ( null !== $term ) { 725 $args['s'] = $term; 726 } 727 $query->query( $args ); 728 729 $this->assertNotContains( $excluded[ $type ], $query->posts ); 730 $this->assertContains( $public[ $type ], $query->posts ); 731 $this->assertContains( $protected[ $type ], $query->posts ); 732 } 733 } 734 } finally { 735 remove_filter( 'bbp_exclude_forum_ids', $filter, 10 ); 736 $_REQUEST = $old_request; 737 $GLOBALS['pagenow'] = $old_pagenow; 738 $GLOBALS['wp_the_query'] = $old_query; 739 set_current_screen( 'front' ); 740 } 741 } 742 743 /** 744 * @covers ::bbp_pre_get_posts_normalize_forum_visibility 745 * @covers ::_bbp_forum_visibility_where 746 */ 747 public function test_password_search_preserves_meta_query_only_exclusion_filter() { 748 $this->factory->forum->create( array( 'post_status' => bbp_get_hidden_status_id() ) ); 749 $excluded = $this->create_search_tree(); 750 $protected = $this->create_search_tree( 'forum-secret' ); 751 $public = $this->create_search_tree(); 752 753 // Duplicate public metadata must not bypass the password-search clause. 754 foreach ( array( 'topic', 'reply' ) as $type ) { 755 add_post_meta( $protected[ $type ], '_bbp_forum_id', $public['forum'] ); 756 } 757 758 $filter = function( $value, $forum_ids, $type ) use ( $excluded ) { 759 if ( 'meta_query' === $type ) { 760 $value['value'] = wp_parse_id_list( array_merge( $value['value'], array( $excluded['forum'] ) ) ); 761 } 762 763 return $value; 764 }; 765 add_filter( 'bbp_exclude_forum_ids', $filter, 10, 3 ); 766 $this->set_current_user( 0 ); 767 768 try { 769 foreach ( array( array(), array( 770 'relation' => 'OR', 771 array( 'key' => '_bbp_forum_id', 'value' => $public['forum'] ), 772 array( 'key' => '_bbp_forum_id', 'value' => $excluded['forum'] ), 773 ) ) as $meta_query ) { 774 $query = new WP_Query( 775 array( 776 'post_type' => array( bbp_get_topic_post_type(), bbp_get_reply_post_type() ), 777 'post__in' => array_merge( array_values( $excluded ), array_values( $protected ), array_values( $public ) ), 778 's' => 'sentinel', 779 'fields' => 'ids', 780 'posts_per_page' => -1, 781 'meta_query' => $meta_query, 782 ) 783 ); 784 foreach ( array( 'topic', 'reply' ) as $type ) { 785 $this->assertNotContains( $excluded[ $type ], $query->posts ); 786 $this->assertNotContains( $protected[ $type ], $query->posts ); 787 $this->assertContains( $public[ $type ], $query->posts ); 788 } 789 } 790 } finally { 791 remove_filter( 'bbp_exclude_forum_ids', $filter, 10 ); 792 } 793 } 794 795 /** 796 * Create matching forum, topic, and reply search controls. 797 */ 798 private function create_search_tree( $password = '' ) { 799 $forum_id = $this->factory->forum->create( 800 array( 'post_password' => $password, 'post_content' => 'Search ancestry sentinel 0' ) 801 ); 802 $topic_id = $this->factory->topic->create( 803 array( 'post_parent' => $forum_id, 'post_content' => 'Search ancestry sentinel 0', 'topic_meta' => array( 'forum_id' => $forum_id ) ) 804 ); 805 $reply_id = $this->factory->reply->create( 806 array( 'post_parent' => $topic_id, 'post_content' => 'Search ancestry sentinel 0', 'reply_meta' => array( 'forum_id' => $forum_id, 'topic_id' => $topic_id ) ) 807 ); 808 809 return array( 'forum' => $forum_id, 'topic' => $topic_id, 'reply' => $reply_id ); 810 } 811 70 812 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)