From 0c73fd7f1fe33a47ac8ef7a0e14944bae46d3652 Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 17 Apr 2026 17:36:53 +0200 Subject: [PATCH 1/9] feat: implement support for protecting posts with category-based recursive restriction - extend protection logic to support posts - implement recursive restriction via categories and subcategories - keep feature optional via filter (no UI changes) - align behavior with existing page protection --- README.md | 20 +++ floauth.php | 2 +- includes/extranet.php | 275 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 267 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e0f1283..e8401ac 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ FloAuth is a WordPress plugin for bridging FloMembers membership management and * Extranet (pages restricted to logged-in users only, **optional**) * Restricts access to extranet page and its children * Removes extranet pages from search results + * Optional: restrict **posts** by category tree (parent categories plus subcategories), enabled only via filters in `functions.php` — same capability rules as pages; restricted posts are omitted from the **main** blog home, post archives (category, tag, author, date), **search**, and **RSS/Atom feeds** for visitors without access (singular URLs still redirect; some theme blocks/widgets use separate queries and are not covered) * Hides WordPress toolbar from Subscribers ## Installation @@ -114,6 +115,25 @@ function custom_restrict_extranet_pages_capability( $capability ) { } add_filter( 'floauth_restrict_extranet_pages_capability', 'custom_restrict_extranet_pages_capability' ); ``` + +### Extranet: restrict posts by category (optional) + +By default, only the **Extranet path** setting (pages and child pages) applies. To also protect **standard WordPress posts** that belong to a category and its subcategories, enable the feature and return the **root** category term IDs (numeric `term_id` values from **Posts → Categories** in the admin, or from the REST API / database). + +Restrictions use the same capability as pages (`floauth_restrict_extranet_pages_capability`, default `read`). Visitors without that access are **redirected** away from singular posts in those categories and from **category archive** URLs for those terms. They also do **not** see those posts in the **main** blog/feed listing queries: home, typical post archives, **search**, and **RSS/Atom**. Custom blocks or widgets that run their own `WP_Query` may still list titles unless the theme adjusts them. + +```php +add_filter( 'floauth_extranet_restrict_posts_by_category', '__return_true' ); + +function my_site_floauth_extranet_restricted_category_ids() { + // One or more parent category term IDs; all descendant categories are included. + return array( 12, 34 ); +} +add_filter( 'floauth_extranet_restricted_category_ids', 'my_site_floauth_extranet_restricted_category_ids' ); +``` + +This does not add fields to the FloAuth settings screen; it keeps the UI unchanged for other sites. + More info on WordPress [Roles and Capabilities](https://wordpress.org/support/article/roles-and-capabilities/) ### Keep WordPress toolbar always visible diff --git a/floauth.php b/floauth.php index f935181..cf61dc5 100755 --- a/floauth.php +++ b/floauth.php @@ -1,7 +1,7 @@ 0 ) { + $out[] = $id; + } + } + return array_values( array_unique( $out ) ); +} + +/** + * Category term IDs that restrict posts (roots plus all descendants). + * + * @return int[] + */ +function floauth_get_extranet_restricted_category_term_ids() { + if ( ! floauth_extranet_restrict_posts_by_category_enabled() ) { + return array(); + } + $roots = floauth_get_extranet_restricted_category_root_ids(); + if ( empty( $roots ) ) { + return array(); + } + $all = array(); + foreach ( $roots as $root_id ) { + $term = get_term( $root_id, 'category' ); + if ( $term instanceof WP_Term && ! is_wp_error( $term ) ) { + $all[] = (int) $term->term_id; + } + $children = get_term_children( $root_id, 'category' ); + if ( ! is_wp_error( $children ) && is_array( $children ) ) { + foreach ( $children as $child_id ) { + $all[] = (int) $child_id; + } + } + } + return array_values( array_unique( array_filter( $all ) ) ); +} + +/** + * Redirect when extranet content is blocked for the current user. + * + * @return void + */ +function floauth_extranet_block_redirect() { + wp_safe_redirect( apply_filters( 'floauth_restrict_extranet_block_redirect', home_url( '/' ) ) ); + exit(); +} + +/** + * Merge a tax_query clause with an existing query tax_query using AND. + * + * @param array $existing Existing tax_query array. + * @param array $clause New clause. + * @return array + */ +function floauth_extranet_merge_tax_query( $existing, $clause ) { + if ( empty( $existing ) || ! is_array( $existing ) ) { + return array( $clause ); + } + $clauses = array(); + foreach ( $existing as $key => $value ) { + if ( 'relation' === $key ) { + continue; + } + if ( is_array( $value ) ) { + $clauses[] = $value; + } + } + return array_merge( + array( 'relation' => 'AND' ), + $clauses, + array( $clause ) + ); +} + +/** + * Whether the current user may see extranet content (pages and optional category posts). + * + * @return bool + */ +function floauth_extranet_user_can_read_extranet() { + $capability = apply_filters( 'floauth_restrict_extranet_pages_capability', 'read' ); + return is_user_logged_in() && current_user_can( $capability ); +} + +/** + * Whether this is a main front-end listing/feed/search query where restricted posts should be omitted. + * + * Skips singular queries so template_redirect can still run for blocked single posts. + * + * @param WP_Query $query Query instance. + * @return bool + */ +function floauth_extranet_query_lists_posts_for_hiding( $query ) { + if ( is_admin() || ! $query->is_main_query() ) { + return false; + } + if ( $query->is_singular() ) { + return false; + } + if ( $query->is_search() || $query->is_feed() ) { + return true; + } + if ( $query->is_home() || $query->is_post_type_archive( 'post' ) ) { + return true; + } + if ( $query->is_category() || $query->is_tag() || $query->is_author() || $query->is_date() ) { + return true; + } + return false; +} + +/** + * Whether the query includes the post post type (default blog queries do). + * + * @param WP_Query $query Query instance. + * @return bool + */ +function floauth_extranet_query_includes_post_type_post( $query ) { + $post_type = $query->get( 'post_type' ); + if ( empty( $post_type ) ) { + return true; + } + if ( 'post' === $post_type ) { + return true; + } + if ( is_array( $post_type ) && in_array( 'post', $post_type, true ) ) { + return true; + } + return false; +} + +/** + * Remove extranet path and its children from search results if user has no rights. + * Optionally exclude posts in restricted categories from search, listings, and feeds (see filters). * * Capability can be changed with filter "floauth_restrict_extranet_pages_capability" * Capability defaults to "read", also logged-in users with no role have no access @@ -15,31 +172,71 @@ * @return WP_Query */ function floauth_filter_pre_get_posts( $query ) { - if ( $query->is_search ) { - $capability = apply_filters( 'floauth_restrict_extranet_pages_capability', 'read' ); - if ( ! is_user_logged_in() || ! current_user_can( $capability ) ) { - $restricted_post_id = (int) floauth_get_extranet_post_id(); - if ( 0 !== $restricted_post_id ) { - $children = get_pages( - array( - 'child_of' => $restricted_post_id, - ) - ); - $restricted_ids = array(); - $restricted_ids[] = $restricted_post_id; - foreach ( $children as $child ) { - $restricted_ids[] = $child->ID; - } - $query->set( 'post__not_in', $restricted_ids ); + if ( is_admin() || ! $query->is_main_query() ) { + return $query; + } + if ( floauth_extranet_user_can_read_extranet() ) { + return $query; + } + + if ( $query->is_search() ) { + $restricted_page_ids = array(); + $restricted_post_id = (int) floauth_get_extranet_post_id(); + if ( 0 !== $restricted_post_id ) { + $children = get_pages( + array( + 'child_of' => $restricted_post_id, + ) + ); + $restricted_page_ids[] = $restricted_post_id; + foreach ( $children as $child ) { + $restricted_page_ids[] = $child->ID; } } + if ( ! empty( $restricted_page_ids ) ) { + $not_in = $query->get( 'post__not_in' ); + if ( ! is_array( $not_in ) ) { + $not_in = array(); + } + $query->set( + 'post__not_in', + array_values( + array_unique( + array_merge( + array_map( 'intval', $not_in ), + $restricted_page_ids + ) + ) + ) + ); + } } + + $category_term_ids = floauth_get_extranet_restricted_category_term_ids(); + if ( + ! empty( $category_term_ids ) + && floauth_extranet_query_lists_posts_for_hiding( $query ) + && floauth_extranet_query_includes_post_type_post( $query ) + ) { + $extranet_tax = array( + 'taxonomy' => 'category', + 'field' => 'term_id', + 'terms' => $category_term_ids, + 'operator' => 'NOT IN', + ); + $query->set( + 'tax_query', + floauth_extranet_merge_tax_query( $query->get( 'tax_query' ), $extranet_tax ) + ); + } + return $query; } add_filter( 'pre_get_posts', 'floauth_filter_pre_get_posts' ); /** - * Disable access to extranet path and its children if user has no rights + * Disable access to extranet path and its children if user has no rights. + * Optionally restrict singular posts and category archives by category tree (see filters). * * Capability can be changed with filter "floauth_restrict_extranet_pages_capability" * Capability defaults to "read", also logged-in users with no role have no access @@ -47,20 +244,40 @@ function floauth_filter_pre_get_posts( $query ) { * @return void */ function floauth_block_extranet_pages() { - $capability = apply_filters( 'floauth_restrict_extranet_pages_capability', 'read' ); - if ( ! is_user_logged_in() || ! current_user_can( $capability ) ) { - if ( ! is_search() ) { - $restricted_post_id = (int) floauth_get_extranet_post_id(); - if ( 0 !== $restricted_post_id ) { - $current_post_id = get_the_ID(); - $ancestors = get_post_ancestors( $current_post_id ); - if ( $restricted_post_id === $current_post_id || in_array( $restricted_post_id, $ancestors, true ) ) { - wp_safe_redirect( apply_filters( 'floauth_restrict_extranet_block_redirect', home_url( '/' ) ) ); - exit(); - } + if ( floauth_extranet_user_can_read_extranet() ) { + return; + } + if ( is_search() ) { + return; + } + + $restricted_post_id = (int) floauth_get_extranet_post_id(); + if ( 0 !== $restricted_post_id ) { + $current_post_id = get_the_ID(); + if ( $current_post_id ) { + $ancestors = get_post_ancestors( $current_post_id ); + if ( $restricted_post_id === $current_post_id || in_array( $restricted_post_id, $ancestors, true ) ) { + floauth_extranet_block_redirect(); } } } + + $term_ids = floauth_get_extranet_restricted_category_term_ids(); + if ( empty( $term_ids ) ) { + return; + } + + if ( is_singular( 'post' ) ) { + $post = get_queried_object(); + if ( $post instanceof WP_Post && has_category( $term_ids, $post ) ) { + floauth_extranet_block_redirect(); + } + } elseif ( is_category() ) { + $term = get_queried_object(); + if ( $term instanceof WP_Term && 'category' === $term->taxonomy && in_array( (int) $term->term_id, $term_ids, true ) ) { + floauth_extranet_block_redirect(); + } + } } add_action( 'template_redirect', 'floauth_block_extranet_pages' ); From 27ce9951af8585a9156bfa19998d0393baa77249 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 21 Apr 2026 18:22:59 +0200 Subject: [PATCH 2/9] feat: implement support for protecting posts with category-based recursive restriction --- README.md | 20 +++ floauth.php | 2 +- includes/extranet.php | 275 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 267 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e0f1283..e8401ac 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ FloAuth is a WordPress plugin for bridging FloMembers membership management and * Extranet (pages restricted to logged-in users only, **optional**) * Restricts access to extranet page and its children * Removes extranet pages from search results + * Optional: restrict **posts** by category tree (parent categories plus subcategories), enabled only via filters in `functions.php` — same capability rules as pages; restricted posts are omitted from the **main** blog home, post archives (category, tag, author, date), **search**, and **RSS/Atom feeds** for visitors without access (singular URLs still redirect; some theme blocks/widgets use separate queries and are not covered) * Hides WordPress toolbar from Subscribers ## Installation @@ -114,6 +115,25 @@ function custom_restrict_extranet_pages_capability( $capability ) { } add_filter( 'floauth_restrict_extranet_pages_capability', 'custom_restrict_extranet_pages_capability' ); ``` + +### Extranet: restrict posts by category (optional) + +By default, only the **Extranet path** setting (pages and child pages) applies. To also protect **standard WordPress posts** that belong to a category and its subcategories, enable the feature and return the **root** category term IDs (numeric `term_id` values from **Posts → Categories** in the admin, or from the REST API / database). + +Restrictions use the same capability as pages (`floauth_restrict_extranet_pages_capability`, default `read`). Visitors without that access are **redirected** away from singular posts in those categories and from **category archive** URLs for those terms. They also do **not** see those posts in the **main** blog/feed listing queries: home, typical post archives, **search**, and **RSS/Atom**. Custom blocks or widgets that run their own `WP_Query` may still list titles unless the theme adjusts them. + +```php +add_filter( 'floauth_extranet_restrict_posts_by_category', '__return_true' ); + +function my_site_floauth_extranet_restricted_category_ids() { + // One or more parent category term IDs; all descendant categories are included. + return array( 12, 34 ); +} +add_filter( 'floauth_extranet_restricted_category_ids', 'my_site_floauth_extranet_restricted_category_ids' ); +``` + +This does not add fields to the FloAuth settings screen; it keeps the UI unchanged for other sites. + More info on WordPress [Roles and Capabilities](https://wordpress.org/support/article/roles-and-capabilities/) ### Keep WordPress toolbar always visible diff --git a/floauth.php b/floauth.php index f935181..cf61dc5 100755 --- a/floauth.php +++ b/floauth.php @@ -1,7 +1,7 @@ 0 ) { + $out[] = $id; + } + } + return array_values( array_unique( $out ) ); +} + +/** + * Category term IDs that restrict posts (roots plus all descendants). + * + * @return int[] + */ +function floauth_get_extranet_restricted_category_term_ids() { + if ( ! floauth_extranet_restrict_posts_by_category_enabled() ) { + return array(); + } + $roots = floauth_get_extranet_restricted_category_root_ids(); + if ( empty( $roots ) ) { + return array(); + } + $all = array(); + foreach ( $roots as $root_id ) { + $term = get_term( $root_id, 'category' ); + if ( $term instanceof WP_Term && ! is_wp_error( $term ) ) { + $all[] = (int) $term->term_id; + } + $children = get_term_children( $root_id, 'category' ); + if ( ! is_wp_error( $children ) && is_array( $children ) ) { + foreach ( $children as $child_id ) { + $all[] = (int) $child_id; + } + } + } + return array_values( array_unique( array_filter( $all ) ) ); +} + +/** + * Redirect when extranet content is blocked for the current user. + * + * @return void + */ +function floauth_extranet_block_redirect() { + wp_safe_redirect( apply_filters( 'floauth_restrict_extranet_block_redirect', home_url( '/' ) ) ); + exit(); +} + +/** + * Merge a tax_query clause with an existing query tax_query using AND. + * + * @param array $existing Existing tax_query array. + * @param array $clause New clause. + * @return array + */ +function floauth_extranet_merge_tax_query( $existing, $clause ) { + if ( empty( $existing ) || ! is_array( $existing ) ) { + return array( $clause ); + } + $clauses = array(); + foreach ( $existing as $key => $value ) { + if ( 'relation' === $key ) { + continue; + } + if ( is_array( $value ) ) { + $clauses[] = $value; + } + } + return array_merge( + array( 'relation' => 'AND' ), + $clauses, + array( $clause ) + ); +} + +/** + * Whether the current user may see extranet content (pages and optional category posts). + * + * @return bool + */ +function floauth_extranet_user_can_read_extranet() { + $capability = apply_filters( 'floauth_restrict_extranet_pages_capability', 'read' ); + return is_user_logged_in() && current_user_can( $capability ); +} + +/** + * Whether this is a main front-end listing/feed/search query where restricted posts should be omitted. + * + * Skips singular queries so template_redirect can still run for blocked single posts. + * + * @param WP_Query $query Query instance. + * @return bool + */ +function floauth_extranet_query_lists_posts_for_hiding( $query ) { + if ( is_admin() || ! $query->is_main_query() ) { + return false; + } + if ( $query->is_singular() ) { + return false; + } + if ( $query->is_search() || $query->is_feed() ) { + return true; + } + if ( $query->is_home() || $query->is_post_type_archive( 'post' ) ) { + return true; + } + if ( $query->is_category() || $query->is_tag() || $query->is_author() || $query->is_date() ) { + return true; + } + return false; +} + +/** + * Whether the query includes the post post type (default blog queries do). + * + * @param WP_Query $query Query instance. + * @return bool + */ +function floauth_extranet_query_includes_post_type_post( $query ) { + $post_type = $query->get( 'post_type' ); + if ( empty( $post_type ) ) { + return true; + } + if ( 'post' === $post_type ) { + return true; + } + if ( is_array( $post_type ) && in_array( 'post', $post_type, true ) ) { + return true; + } + return false; +} + +/** + * Remove extranet path and its children from search results if user has no rights. + * Optionally exclude posts in restricted categories from search, listings, and feeds (see filters). * * Capability can be changed with filter "floauth_restrict_extranet_pages_capability" * Capability defaults to "read", also logged-in users with no role have no access @@ -15,31 +172,71 @@ * @return WP_Query */ function floauth_filter_pre_get_posts( $query ) { - if ( $query->is_search ) { - $capability = apply_filters( 'floauth_restrict_extranet_pages_capability', 'read' ); - if ( ! is_user_logged_in() || ! current_user_can( $capability ) ) { - $restricted_post_id = (int) floauth_get_extranet_post_id(); - if ( 0 !== $restricted_post_id ) { - $children = get_pages( - array( - 'child_of' => $restricted_post_id, - ) - ); - $restricted_ids = array(); - $restricted_ids[] = $restricted_post_id; - foreach ( $children as $child ) { - $restricted_ids[] = $child->ID; - } - $query->set( 'post__not_in', $restricted_ids ); + if ( is_admin() || ! $query->is_main_query() ) { + return $query; + } + if ( floauth_extranet_user_can_read_extranet() ) { + return $query; + } + + if ( $query->is_search() ) { + $restricted_page_ids = array(); + $restricted_post_id = (int) floauth_get_extranet_post_id(); + if ( 0 !== $restricted_post_id ) { + $children = get_pages( + array( + 'child_of' => $restricted_post_id, + ) + ); + $restricted_page_ids[] = $restricted_post_id; + foreach ( $children as $child ) { + $restricted_page_ids[] = $child->ID; } } + if ( ! empty( $restricted_page_ids ) ) { + $not_in = $query->get( 'post__not_in' ); + if ( ! is_array( $not_in ) ) { + $not_in = array(); + } + $query->set( + 'post__not_in', + array_values( + array_unique( + array_merge( + array_map( 'intval', $not_in ), + $restricted_page_ids + ) + ) + ) + ); + } } + + $category_term_ids = floauth_get_extranet_restricted_category_term_ids(); + if ( + ! empty( $category_term_ids ) + && floauth_extranet_query_lists_posts_for_hiding( $query ) + && floauth_extranet_query_includes_post_type_post( $query ) + ) { + $extranet_tax = array( + 'taxonomy' => 'category', + 'field' => 'term_id', + 'terms' => $category_term_ids, + 'operator' => 'NOT IN', + ); + $query->set( + 'tax_query', + floauth_extranet_merge_tax_query( $query->get( 'tax_query' ), $extranet_tax ) + ); + } + return $query; } add_filter( 'pre_get_posts', 'floauth_filter_pre_get_posts' ); /** - * Disable access to extranet path and its children if user has no rights + * Disable access to extranet path and its children if user has no rights. + * Optionally restrict singular posts and category archives by category tree (see filters). * * Capability can be changed with filter "floauth_restrict_extranet_pages_capability" * Capability defaults to "read", also logged-in users with no role have no access @@ -47,20 +244,40 @@ function floauth_filter_pre_get_posts( $query ) { * @return void */ function floauth_block_extranet_pages() { - $capability = apply_filters( 'floauth_restrict_extranet_pages_capability', 'read' ); - if ( ! is_user_logged_in() || ! current_user_can( $capability ) ) { - if ( ! is_search() ) { - $restricted_post_id = (int) floauth_get_extranet_post_id(); - if ( 0 !== $restricted_post_id ) { - $current_post_id = get_the_ID(); - $ancestors = get_post_ancestors( $current_post_id ); - if ( $restricted_post_id === $current_post_id || in_array( $restricted_post_id, $ancestors, true ) ) { - wp_safe_redirect( apply_filters( 'floauth_restrict_extranet_block_redirect', home_url( '/' ) ) ); - exit(); - } + if ( floauth_extranet_user_can_read_extranet() ) { + return; + } + if ( is_search() ) { + return; + } + + $restricted_post_id = (int) floauth_get_extranet_post_id(); + if ( 0 !== $restricted_post_id ) { + $current_post_id = get_the_ID(); + if ( $current_post_id ) { + $ancestors = get_post_ancestors( $current_post_id ); + if ( $restricted_post_id === $current_post_id || in_array( $restricted_post_id, $ancestors, true ) ) { + floauth_extranet_block_redirect(); } } } + + $term_ids = floauth_get_extranet_restricted_category_term_ids(); + if ( empty( $term_ids ) ) { + return; + } + + if ( is_singular( 'post' ) ) { + $post = get_queried_object(); + if ( $post instanceof WP_Post && has_category( $term_ids, $post ) ) { + floauth_extranet_block_redirect(); + } + } elseif ( is_category() ) { + $term = get_queried_object(); + if ( $term instanceof WP_Term && 'category' === $term->taxonomy && in_array( (int) $term->term_id, $term_ids, true ) ) { + floauth_extranet_block_redirect(); + } + } } add_action( 'template_redirect', 'floauth_block_extranet_pages' ); From fe66bf296bf77911c18dbc2a1dca73e4362d1b64 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 28 Apr 2026 12:02:23 +0200 Subject: [PATCH 3/9] fix: Add transient cache for restricted extranet category term resolution --- includes/extranet.php | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/includes/extranet.php b/includes/extranet.php index ff9048a..24044ef 100644 --- a/includes/extranet.php +++ b/includes/extranet.php @@ -45,12 +45,26 @@ function floauth_get_extranet_restricted_category_root_ids() { */ function floauth_get_extranet_restricted_category_term_ids() { if ( ! floauth_extranet_restrict_posts_by_category_enabled() ) { + delete_transient( 'floauth_extranet_restricted_category_term_ids' ); return array(); } $roots = floauth_get_extranet_restricted_category_root_ids(); if ( empty( $roots ) ) { + delete_transient( 'floauth_extranet_restricted_category_term_ids' ); return array(); } + + $roots_hash = md5( wp_json_encode( $roots ) ); + $cached = get_transient( 'floauth_extranet_restricted_category_term_ids' ); + if ( + is_array( $cached ) + && isset( $cached['roots_hash'], $cached['term_ids'] ) + && $roots_hash === $cached['roots_hash'] + && is_array( $cached['term_ids'] ) + ) { + return array_values( array_unique( array_filter( array_map( 'intval', $cached['term_ids'] ) ) ) ); + } + $all = array(); foreach ( $roots as $root_id ) { $term = get_term( $root_id, 'category' ); @@ -64,7 +78,16 @@ function floauth_get_extranet_restricted_category_term_ids() { } } } - return array_values( array_unique( array_filter( $all ) ) ); + $term_ids = array_values( array_unique( array_filter( $all ) ) ); + set_transient( + 'floauth_extranet_restricted_category_term_ids', + array( + 'roots_hash' => $roots_hash, + 'term_ids' => $term_ids, + ), + HOUR_IN_SECONDS + ); + return $term_ids; } /** @@ -314,3 +337,15 @@ function floauth_clear_extranet_transient( $old_value, $new_value ) { delete_transient( 'floauth_extranet_post_id' ); } add_action( 'update_option_floauth_extranet_path', 'floauth_clear_extranet_transient', 10, 2 ); + +/** + * Clear restricted category term IDs transient. + * + * @return void + */ +function floauth_clear_extranet_restricted_category_term_ids_transient() { + delete_transient( 'floauth_extranet_restricted_category_term_ids' ); +} +add_action( 'created_category', 'floauth_clear_extranet_restricted_category_term_ids_transient' ); +add_action( 'edited_category', 'floauth_clear_extranet_restricted_category_term_ids_transient' ); +add_action( 'delete_category', 'floauth_clear_extranet_restricted_category_term_ids_transient' ); From 26922d592608ad945ed694bb4de014c86c71473f Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 28 Apr 2026 13:44:49 +0200 Subject: [PATCH 4/9] fix: Resolve restricted category trees via single query --- includes/extranet.php | 51 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/includes/extranet.php b/includes/extranet.php index 24044ef..11d27b8 100644 --- a/includes/extranet.php +++ b/includes/extranet.php @@ -65,20 +65,53 @@ function floauth_get_extranet_restricted_category_term_ids() { return array_values( array_unique( array_filter( array_map( 'intval', $cached['term_ids'] ) ) ) ); } - $all = array(); + $term_parent_map = get_terms( + array( + 'taxonomy' => 'category', + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + if ( is_wp_error( $term_parent_map ) || ! is_array( $term_parent_map ) ) { + return array(); + } + + $children_by_parent = array(); + foreach ( $term_parent_map as $term_id => $parent_id ) { + $term_id = (int) $term_id; + $parent_id = (int) $parent_id; + if ( ! isset( $children_by_parent[ $parent_id ] ) ) { + $children_by_parent[ $parent_id ] = array(); + } + $children_by_parent[ $parent_id ][] = $term_id; + } + + $pending = array(); + $seen = array(); + $term_ids = array(); foreach ( $roots as $root_id ) { - $term = get_term( $root_id, 'category' ); - if ( $term instanceof WP_Term && ! is_wp_error( $term ) ) { - $all[] = (int) $term->term_id; + if ( isset( $term_parent_map[ (string) $root_id ] ) || isset( $term_parent_map[ $root_id ] ) ) { + $pending[] = (int) $root_id; + } + } + + while ( ! empty( $pending ) ) { + $current = array_pop( $pending ); + if ( isset( $seen[ $current ] ) ) { + continue; } - $children = get_term_children( $root_id, 'category' ); - if ( ! is_wp_error( $children ) && is_array( $children ) ) { - foreach ( $children as $child_id ) { - $all[] = (int) $child_id; + $seen[ $current ] = true; + $term_ids[] = $current; + if ( isset( $children_by_parent[ $current ] ) ) { + foreach ( $children_by_parent[ $current ] as $child_id ) { + if ( ! isset( $seen[ $child_id ] ) ) { + $pending[] = (int) $child_id; + } } } } - $term_ids = array_values( array_unique( array_filter( $all ) ) ); + + $term_ids = array_values( array_unique( array_filter( $term_ids ) ) ); set_transient( 'floauth_extranet_restricted_category_term_ids', array( From b4b293b0fc5976f6bf30dccaa64e794dacaa23f1 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 28 Apr 2026 14:43:05 +0200 Subject: [PATCH 5/9] fix: Reduce nesting in extranet post__not_in assignment --- includes/extranet.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/includes/extranet.php b/includes/extranet.php index 11d27b8..b26bf16 100644 --- a/includes/extranet.php +++ b/includes/extranet.php @@ -254,17 +254,10 @@ function floauth_filter_pre_get_posts( $query ) { if ( ! is_array( $not_in ) ) { $not_in = array(); } - $query->set( - 'post__not_in', - array_values( - array_unique( - array_merge( - array_map( 'intval', $not_in ), - $restricted_page_ids - ) - ) - ) - ); + $existing_not_in = array_map( 'intval', $not_in ); + $merged_not_in = array_merge( $existing_not_in, $restricted_page_ids ); + $unique_not_in = array_values( array_unique( $merged_not_in ) ); + $query->set( 'post__not_in', $unique_not_in ); } } From 49a2c3a555cb9b9ab8f06887f9f980f91a11ea6b Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 28 Apr 2026 15:05:13 +0200 Subject: [PATCH 6/9] fix: Clean up README headings and capability reference placement --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8401ac..1de0610 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,9 @@ function custom_restrict_extranet_pages_capability( $capability ) { add_filter( 'floauth_restrict_extranet_pages_capability', 'custom_restrict_extranet_pages_capability' ); ``` -### Extranet: restrict posts by category (optional) +More info on WordPress [Roles and Capabilities](https://wordpress.org/support/article/roles-and-capabilities/) + +### Restrict posts by category By default, only the **Extranet path** setting (pages and child pages) applies. To also protect **standard WordPress posts** that belong to a category and its subcategories, enable the feature and return the **root** category term IDs (numeric `term_id` values from **Posts → Categories** in the admin, or from the REST API / database). @@ -134,8 +136,6 @@ add_filter( 'floauth_extranet_restricted_category_ids', 'my_site_floauth_extrane This does not add fields to the FloAuth settings screen; it keeps the UI unchanged for other sites. -More info on WordPress [Roles and Capabilities](https://wordpress.org/support/article/roles-and-capabilities/) - ### Keep WordPress toolbar always visible ```php From 4097aa1cd2424313d1d4e55be5e1e1e5d19fa53b Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 8 May 2026 14:05:01 +0200 Subject: [PATCH 7/9] refactor: centralize extranet ID normalization logic --- includes/extranet.php | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/includes/extranet.php b/includes/extranet.php index b26bf16..5c0bf78 100644 --- a/includes/extranet.php +++ b/includes/extranet.php @@ -38,6 +38,28 @@ function floauth_get_extranet_restricted_category_root_ids() { return array_values( array_unique( $out ) ); } +/** + * Normalize a list of IDs to unique, positive integers. + * + * @param mixed $ids Values to normalize. + * @return int[] + */ +function floauth_extranet_normalize_positive_ids( $ids ) { + if ( ! is_array( $ids ) ) { + return array(); + } + + $normalized = array(); + foreach ( $ids as $id ) { + $id = absint( $id ); + if ( $id > 0 ) { + $normalized[] = $id; + } + } + + return array_values( array_unique( $normalized ) ); +} + /** * Category term IDs that restrict posts (roots plus all descendants). * @@ -62,7 +84,7 @@ function floauth_get_extranet_restricted_category_term_ids() { && $roots_hash === $cached['roots_hash'] && is_array( $cached['term_ids'] ) ) { - return array_values( array_unique( array_filter( array_map( 'intval', $cached['term_ids'] ) ) ) ); + return floauth_extranet_normalize_positive_ids( $cached['term_ids'] ); } $term_parent_map = get_terms( @@ -111,7 +133,7 @@ function floauth_get_extranet_restricted_category_term_ids() { } } - $term_ids = array_values( array_unique( array_filter( $term_ids ) ) ); + $term_ids = floauth_extranet_normalize_positive_ids( $term_ids ); set_transient( 'floauth_extranet_restricted_category_term_ids', array( @@ -250,13 +272,9 @@ function floauth_filter_pre_get_posts( $query ) { } } if ( ! empty( $restricted_page_ids ) ) { - $not_in = $query->get( 'post__not_in' ); - if ( ! is_array( $not_in ) ) { - $not_in = array(); - } - $existing_not_in = array_map( 'intval', $not_in ); + $existing_not_in = floauth_extranet_normalize_positive_ids( $query->get( 'post__not_in' ) ); $merged_not_in = array_merge( $existing_not_in, $restricted_page_ids ); - $unique_not_in = array_values( array_unique( $merged_not_in ) ); + $unique_not_in = floauth_extranet_normalize_positive_ids( $merged_not_in ); $query->set( 'post__not_in', $unique_not_in ); } } From a9e4f5ea6ba9132ce66c7e807ce41cbd8847cbee Mon Sep 17 00:00:00 2001 From: michael Date: Wed, 13 May 2026 17:45:59 +0200 Subject: [PATCH 8/9] refactor: reuse ID normalizer for extranet category root IDs --- includes/extranet.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/includes/extranet.php b/includes/extranet.php index 5c0bf78..686db48 100644 --- a/includes/extranet.php +++ b/includes/extranet.php @@ -25,17 +25,7 @@ function floauth_extranet_restrict_posts_by_category_enabled() { */ function floauth_get_extranet_restricted_category_root_ids() { $ids = apply_filters( 'floauth_extranet_restricted_category_ids', array() ); - if ( ! is_array( $ids ) ) { - return array(); - } - $out = array(); - foreach ( $ids as $id ) { - $id = absint( $id ); - if ( $id > 0 ) { - $out[] = $id; - } - } - return array_values( array_unique( $out ) ); + return floauth_extranet_normalize_positive_ids( $ids ); } /** From e87e5ce5e6825c9d4b69b570d99d95abbe1e53dd Mon Sep 17 00:00:00 2001 From: Tapio Nurminen Date: Wed, 20 May 2026 07:55:05 +0300 Subject: [PATCH 9/9] Bump version from 1.0.7 to 1.1.0 See semantic versioning --- floauth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floauth.php b/floauth.php index cf61dc5..06ce4eb 100755 --- a/floauth.php +++ b/floauth.php @@ -1,7 +1,7 @@