From a0a34518221028cdba4e353f46f997aa4b42c063 Mon Sep 17 00:00:00 2001 From: Simon Costea Date: Mon, 7 Sep 2026 15:42:09 +0300 Subject: [PATCH 1/4] Fix visibility filtering across post types Route login visibility rules through `posts_where` so mixed post-type queries keep unsupported types visible while still filtering supported content. This also preserves filtering for suppressed-filter queries via the existing meta query fallback and avoids skipping secondary single-post queries. --- .../includes/class-bcc-login-visibility.php | 104 +++++++++++++++++- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/plugins/bcc-login/includes/class-bcc-login-visibility.php b/plugins/bcc-login/includes/class-bcc-login-visibility.php index d29e63e..5afa1dd 100644 --- a/plugins/bcc-login/includes/class-bcc-login-visibility.php +++ b/plugins/bcc-login/includes/class-bcc-login-visibility.php @@ -47,6 +47,7 @@ function __construct( BCC_Login_Settings $settings, BCC_Login_Client $client, BC add_action( 'updated_post_meta', array( $this, 'on_meta_saved' ), 10, 4 ); add_action( 'enqueue_block_editor_assets', array( $this, 'on_block_editor_assets' ) ); add_action( 'pre_get_posts', array( $this, 'filter_pre_get_posts' ) ); + add_filter( 'posts_where', array( $this, 'filter_posts_where' ), 10, 2 ); add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_menu_items' ), 20 ); add_filter( 'render_block', array( $this, 'on_render_block' ), 10, 2 ); @@ -613,7 +614,9 @@ function filter_pre_get_posts( $query ) { } } - if ( current_user_can( 'edit_posts' ) || $query->is_singular ) { + // Single post views are handled in 'on_template_redirect()'. Secondary + // queries for a single post (e.g. 'p' => 123) still need filtering. + if ( current_user_can( 'edit_posts' ) || ( $query->is_singular && $query->is_main_query() ) ) { return; } @@ -730,6 +733,24 @@ function filter_pre_get_posts( $query ) { // Indicate that this set of rules is for the visibility filter $query->set('bcc_login_visibility_filter_added', 1); + $unsupported_post_types = $this->get_unsupported_post_types( $query ); + + // The rules are normally added as a subquery in 'filter_posts_where()'. + // That keeps them out of the caller's own meta query, lets post types + // without visibility support skip them - those never store + // 'bcc_login_visibility', so the rules would otherwise hide all of them - + // and avoids the joins and GROUP BY that a merged meta query adds, which + // measured about twice as fast on a post archive. + // + // 'posts_where' is skipped for queries using 'suppress_filters' (as + // get_posts() does), so those fall back to the meta query rather than + // being left unfiltered. Unsupported post types are hidden in that case. + if ( ! $query->get('suppress_filters') ) { + $query->set('bcc_login_visibility_rules', $rules); + $query->set('bcc_login_unsupported_post_types', $unsupported_post_types); + return; + } + // Add all the rules to the meta query $meta_query = array( 'relation' => 'AND', @@ -741,6 +762,53 @@ function filter_pre_get_posts( $query ) { $query->set('meta_query', $meta_query); } + /** + * Applies the visibility rules to queries that also span post types without + * visibility support, leaving those post types visible. + * + * The rules are added as a subquery rather than merged into the meta query, + * both so they can be skipped per post type and so they don't leak into the + * conditions the caller asked for. + * + * @param string $where + * @param WP_Query $query + * @return string + */ + function filter_posts_where( $where, $query ) { + $rules = $query->get('bcc_login_visibility_rules'); + $unsupported_post_types = (array) $query->get('bcc_login_unsupported_post_types'); + + if ( empty( $rules ) ) { + return $where; + } + + global $wpdb; + + // Build the rules against an aliased copy of the posts table, so the + // generated postmeta joins stay scoped to the subquery and can't collide + // with the joins of the query we're filtering. + $meta_query = new WP_Meta_Query( $rules ); + $clauses = $meta_query->get_sql( 'post', 'bcc_visibility_posts', 'ID', $query ); + + $subquery = "{$wpdb->posts}.ID IN (" + . " SELECT bcc_visibility_posts.ID FROM {$wpdb->posts} AS bcc_visibility_posts" + . " {$clauses['join']} WHERE 1=1 {$clauses['where']}" + . ' )'; + + // Post types without visibility support skip the rules entirely. + if ( ! empty( $unsupported_post_types ) ) { + $placeholders = implode( ', ', array_fill( 0, count( $unsupported_post_types ), '%s' ) ); + $escape = $wpdb->prepare( + "{$wpdb->posts}.post_type IN ( {$placeholders} )", + $unsupported_post_types + ); + + return $where . " AND ( {$escape} OR {$subquery} )"; + } + + return $where . " AND ( {$subquery} )"; + } + /** * Filters out menu items that the current users shouldn't see. * @@ -1321,10 +1389,40 @@ function get_bcc_group_name_by_id($atts) { } function supports_visibility_filter($query) { - if (!array_key_exists('post_type', $query->query)){ + $post_types = array_key_exists('post_type', $query->query) ? $query->query['post_type'] : null; + + // No post type given: WP_Query defaults to 'post', which we do filter. + // 'any' spans the public post types, so it needs filtering as well. + if ( empty( $post_types ) || 'any' === $post_types ) { return true; } - return in_array($query->query['post_type'], $this->visibility_post_types) && $query->query['post_type'] != 'nav_menu_item'; + + // Menu items are handled in 'filter_menu_items()'. + $post_types = array_diff( (array) $post_types, array( 'nav_menu_item' ) ); + + return (bool) array_intersect( $post_types, $this->visibility_post_types ); + } + + /** + * Returns the queried post types that don't support visibility. + * + * @param WP_Query $query + * @return string[] + */ + private function get_unsupported_post_types($query) { + $post_types = array_key_exists('post_type', $query->query) ? $query->query['post_type'] : null; + + // No post type given: WP_Query defaults to 'post', which is supported. + if ( empty( $post_types ) ) { + return array(); + } + + // 'any' resolves to the post types WP_Query itself searches. + if ( 'any' === $post_types ) { + $post_types = array_keys( get_post_types( array( 'exclude_from_search' => false ) ) ); + } + + return array_values( array_diff( (array) $post_types, $this->visibility_post_types ) ); } function supports_target_groups_filtering($query) { From 80c69e0c105e9ee582050dfcd18a8137b52ce109 Mon Sep 17 00:00:00 2001 From: Simon Costea Date: Mon, 7 Sep 2026 15:50:40 +0300 Subject: [PATCH 2/4] Filter out 'nav_menu_item' from post types Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- plugins/bcc-login/includes/class-bcc-login-visibility.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/bcc-login/includes/class-bcc-login-visibility.php b/plugins/bcc-login/includes/class-bcc-login-visibility.php index 5afa1dd..c7c9fb9 100644 --- a/plugins/bcc-login/includes/class-bcc-login-visibility.php +++ b/plugins/bcc-login/includes/class-bcc-login-visibility.php @@ -1422,6 +1422,9 @@ private function get_unsupported_post_types($query) { $post_types = array_keys( get_post_types( array( 'exclude_from_search' => false ) ) ); } + // Menu items are handled separately in 'filter_menu_items()'. + $post_types = array_diff( (array) $post_types, array( 'nav_menu_item' ) ); + return array_values( array_diff( (array) $post_types, $this->visibility_post_types ) ); } From 03b93bed453149981f55804ec19296d261c9c5d2 Mon Sep 17 00:00:00 2001 From: Simon Costea Date: Mon, 7 Sep 2026 15:56:13 +0300 Subject: [PATCH 3/4] Fix meta_query initialization with array wrapping Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- plugins/bcc-login/includes/class-bcc-login-visibility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/bcc-login/includes/class-bcc-login-visibility.php b/plugins/bcc-login/includes/class-bcc-login-visibility.php index c7c9fb9..867d55c 100644 --- a/plugins/bcc-login/includes/class-bcc-login-visibility.php +++ b/plugins/bcc-login/includes/class-bcc-login-visibility.php @@ -787,7 +787,7 @@ function filter_posts_where( $where, $query ) { // Build the rules against an aliased copy of the posts table, so the // generated postmeta joins stay scoped to the subquery and can't collide // with the joins of the query we're filtering. - $meta_query = new WP_Meta_Query( $rules ); + $meta_query = new WP_Meta_Query( array( $rules ) ); $clauses = $meta_query->get_sql( 'post', 'bcc_visibility_posts', 'ID', $query ); $subquery = "{$wpdb->posts}.ID IN (" From 2f3383434d1316f0809be01229cc2aab43665f51 Mon Sep 17 00:00:00 2001 From: Simon Costea Date: Mon, 7 Sep 2026 19:26:45 +0300 Subject: [PATCH 4/4] Wrap visibility rules for WP_Meta_Query Ensure visibility filters are always passed as clause groups instead of bare first-order clauses. This prevents WP_Meta_Query from silently dropping rules and leaving queries unfiltered, and aligns both the default visibility branch and subquery SQL builder with the expected input shape. --- .../includes/class-bcc-login-visibility.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/plugins/bcc-login/includes/class-bcc-login-visibility.php b/plugins/bcc-login/includes/class-bcc-login-visibility.php index 867d55c..156d7ab 100644 --- a/plugins/bcc-login/includes/class-bcc-login-visibility.php +++ b/plugins/bcc-login/includes/class-bcc-login-visibility.php @@ -661,8 +661,10 @@ function filter_pre_get_posts( $query ) { ); } - // Default: just the visibility clause - $visibility_rules = $visibility_clause; + // Default: just the visibility clause, wrapped as a clause group. + // Every branch below keeps $rules a group: WP_Meta_Query expects one and + // silently drops a bare clause, which would leave a query unfiltered. + $visibility_rules = array( $visibility_clause ); // Include also posts where visibility isn't specified based on the Default Content Access if ( $user_level >= $this->_settings->default_visibility ) { @@ -784,10 +786,18 @@ function filter_posts_where( $where, $query ) { global $wpdb; + // 'filter_pre_get_posts()' always builds a clause group, but guard anyway: + // WP_Meta_Query expects a group and silently drops a bare clause, which + // would leave the query unfiltered. Same test as its own + // WP_Meta_Query::is_first_order_clause(). + if ( isset( $rules['key'] ) || isset( $rules['value'] ) ) { + $rules = array( $rules ); + } + // Build the rules against an aliased copy of the posts table, so the // generated postmeta joins stay scoped to the subquery and can't collide // with the joins of the query we're filtering. - $meta_query = new WP_Meta_Query( array( $rules ) ); + $meta_query = new WP_Meta_Query( $rules ); $clauses = $meta_query->get_sql( 'post', 'bcc_visibility_posts', 'ID', $query ); $subquery = "{$wpdb->posts}.ID IN ("