PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.8.9
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.8.9
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
← All changes | includes/Core/Query.php +995 -1415 4.9.13.8.9 View file →
@@ -1,14 +1,8 @@
1 1 <?php
2 -// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query,WordPress.DB.SlowDBQuery.slow_db_query_tax_query,WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- core docs query builder; meta/tax filtering required for docs/category/KB filtering.
3 -// phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- excludes are user-driven (settings UI) and part of the query builder's public contract.
2 +
4 3 namespace WPDeveloper\BetterDocs\Core;
5 4
6 -if ( ! defined( 'ABSPATH' ) ) {
7 - exit;
8 -}
9 -
10 -
11 5 use WP_Query;
12 6 use WPDeveloper\BetterDocs\Utils\Base;
13 7 use WPDeveloper\BetterDocs\Utils\Database;
14 8 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
@@ -13,1358 +7,953 @@
13 7 use WPDeveloper\BetterDocs\Utils\Database;
14 8 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
15 9
16 10 class Query extends Base {
17 - private $container;
18 - protected $database;
19 - protected $settings;
11 + private $container;
12 + protected $database;
13 + protected $settings;
20 14
21 - public function __construct( Container $container, Database $database, Settings $settings ) {
22 - $this->container = $container;
23 - $this->database = $database;
24 - $this->settings = $settings;
15 + public function __construct( Container $container, Database $database, Settings $settings ) {
16 + $this->container = $container;
17 + $this->database = $database;
18 + $this->settings = $settings;
25 19
26 - add_action( 'parse_term_query', array( $this, 'parse_term_query' ) );
27 - // add_action( 'parse_query', [$this, 'parse_query'], 1 );
28 - add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 1 );
29 - add_filter( 'betterdocs_base_terms_args', array( $this, 'modify_terms_args_for_private_docs' ), 10, 1 );
30 - // Drops terms whose private-inclusive count is 0 after `hide_empty` was
31 - // turned off by modify_terms_args_for_private_docs().
32 - add_filter( 'get_terms', array( $this, 'filter_terms_for_private_docs' ), 10, 3 );
20 + add_action( 'parse_term_query', [ $this, 'parse_term_query' ] );
21 + // add_action( 'parse_query', [$this, 'parse_query'], 1 );
22 + add_action( 'pre_get_posts', [ $this, 'pre_get_posts' ], 1 );
33 23
34 - // Invalidate cached doc-category counts on any write that could change them.
35 - add_action( 'save_post_docs', array( $this, 'flush_term_counts_cache' ) );
36 - add_action( 'deleted_post', array( $this, 'flush_term_counts_cache_on_post' ), 10, 2 );
37 - add_action( 'edited_doc_category', array( $this, 'flush_term_counts_cache' ) );
38 - add_action( 'created_doc_category', array( $this, 'flush_term_counts_cache' ) );
39 - add_action( 'delete_doc_category', array( $this, 'flush_term_counts_cache' ) );
40 - add_action( 'set_object_terms', array( $this, 'flush_term_counts_cache_on_set' ), 10, 4 );
24 + /**
25 + * These below filters are hooked for navigation only.
26 + *
27 + * For old version of this portion.
28 + * @see `betterdocs_single_post_nav` filter
29 + *
30 + * For details:
31 + * @see https://developer.wordpress.org/reference/functions/get_next_post/
32 + * @see https://developer.wordpress.org/reference/functions/get_previous_post/
33 + *
34 + * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/
35 + */
36 + add_filter( 'get_next_post_where', [ $this, 'next_post_where' ], 99, 5 );
37 + add_filter( 'get_previous_post_where', [ $this, 'previous_post_where' ], 99, 5 );
41 38
42 - /**
43 - * These below filters are hooked for navigation only.
44 - *
45 - * For old version of this portion.
46 - * @see `betterdocs_single_post_nav` filter
47 - *
48 - * For details:
49 - * @see https://developer.wordpress.org/reference/functions/get_next_post/
50 - * @see https://developer.wordpress.org/reference/functions/get_previous_post/
51 - *
52 - * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/
53 - */
54 - add_filter( 'get_next_post_where', array( $this, 'next_post_where' ), 99, 5 );
55 - add_filter( 'get_previous_post_where', array( $this, 'previous_post_where' ), 99, 5 );
39 + $this->init();
56 40
57 - $this->init();
41 + /**
42 + * Modify Popular Docs Query (For Shortcode & Widget)
43 + */
44 + add_filter( 'posts_clauses', [ $this, 'mod_query_popular_docs' ], 10, 2 );
45 + }
58 46
59 - /**
60 - * Modify Popular Docs Query (For Shortcode & Widget)
61 - */
62 - add_filter( 'posts_clauses', array( $this, 'mod_query_popular_docs' ), 10, 2 );
63 - }
47 + public function mod_query_popular_docs( $clauses, $wp_query ) {
48 + if ( isset( $wp_query->query['meta_key'] ) ) {
49 + if ( $wp_query->query['meta_key'] == '_betterdocs_meta_views' ) {
50 + global $wpdb;
51 + $order = isset( $wp_query->query['order'] ) ? $wp_query->query['order'] : '';
52 + $order_by_query = ( $order == 'ASC' || $order == 'DESC' ) ? "SUM({$wpdb->prefix}betterdocs_analytics.impressions) {$order}" : ( $order == 'MODIFIED' ? "{$wpdb->prefix}posts.post_modified_gmt DESC" : "{$wpdb->prefix}posts.post_date_gmt DESC" );
53 + $clauses['join'] = "JOIN {$wpdb->prefix}betterdocs_analytics ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}betterdocs_analytics.post_id";
54 + $clauses['where'] = ! is_user_logged_in() ? "AND ( ( {$wpdb->prefix}posts.post_type = 'docs' ) AND ( {$wpdb->prefix}posts.post_status = 'publish' OR {$wpdb->prefix}posts.post_status = 'future' ) )" : "AND ( ( {$wpdb->prefix}posts.post_type = 'docs' ) AND ( {$wpdb->prefix}posts.post_status = 'publish' OR {$wpdb->prefix}posts.post_status = 'future' OR {$wpdb->prefix}posts.post_status = 'draft' OR {$wpdb->prefix}posts.post_status = 'pending' OR {$wpdb->prefix}posts.post_status = 'private' ) )";
55 + $clauses['orderby'] = $order_by_query;
56 + $clauses['groupby'] = "{$wpdb->prefix}betterdocs_analytics.post_id";
57 + if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
58 + global $sitepress;
59 + if ( $sitepress->is_setup_complete() ) {
60 + $constant_language_code = ICL_LANGUAGE_CODE;
61 + $clauses['join'] .= " JOIN {$wpdb->prefix}icl_translations ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}icl_translations.element_id";
62 + $clauses['where'] .= " AND ( {$wpdb->prefix}icl_translations.language_code = '{$constant_language_code}' ) AND ( {$wpdb->prefix}icl_translations.element_type = CONCAT('post_', {$wpdb->prefix}posts.post_type ) )";
63 + }
64 + }
65 + }
66 + }
67 + return $clauses;
68 + }
64 69
65 - public function mod_query_popular_docs( $clauses, $wp_query ) {
66 - if ( isset( $wp_query->query[ 'meta_key' ] ) ) {
67 - if ( '_betterdocs_meta_views' == $wp_query->query[ 'meta_key' ] ) {
68 - global $wpdb;
69 - $order = isset( $wp_query->query[ 'order' ] ) ? $wp_query->query[ 'order' ] : '';
70 - $order_by_query = ( 'ASC' == $order || 'DESC' == $order ) ? "SUM({$wpdb->prefix}betterdocs_analytics.impressions) {$order}" : ( 'MODIFIED' == $order ? "{$wpdb->prefix}posts.post_modified_gmt DESC" : "{$wpdb->prefix}posts.post_date_gmt DESC" );
71 - $clauses[ 'join' ] = "JOIN {$wpdb->prefix}betterdocs_analytics ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}betterdocs_analytics.post_id";
72 - $clauses[ 'where' ] = ! current_user_can( 'read_private_docs' ) ? "AND ( ( {$wpdb->prefix}posts.post_type = 'docs' ) AND ( {$wpdb->prefix}posts.post_status = 'publish' OR {$wpdb->prefix}posts.post_status = 'future' ) )" : "AND ( ( {$wpdb->prefix}posts.post_type = 'docs' ) AND ( {$wpdb->prefix}posts.post_status = 'publish' OR {$wpdb->prefix}posts.post_status = 'future' OR {$wpdb->prefix}posts.post_status = 'draft' OR {$wpdb->prefix}posts.post_status = 'pending' OR {$wpdb->prefix}posts.post_status = 'private' ) )";
73 - $clauses[ 'orderby' ] = $order_by_query;
74 - $clauses[ 'groupby' ] = "{$wpdb->prefix}betterdocs_analytics.post_id";
75 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
76 - global $sitepress;
77 - if ( $sitepress->is_setup_complete() ) {
78 - $constant_language_code = ICL_LANGUAGE_CODE;
79 - $clauses[ 'join' ] .= " JOIN {$wpdb->prefix}icl_translations ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}icl_translations.element_id";
80 - $clauses[ 'where' ] .= " AND ( {$wpdb->prefix}icl_translations.language_code = '{$constant_language_code}' ) AND ( {$wpdb->prefix}icl_translations.element_type = CONCAT('post_', {$wpdb->prefix}posts.post_type ) )";
81 - }
82 - }
83 - }
84 - }
85 - return $clauses;
86 - }
70 + public function init() {
71 + }
87 72
88 - public function init() {
89 - }
73 + public function parse_term_query( $term_query ) {
74 + if ( empty( $term_query->query_vars['taxonomy'] ) ) {
75 + return;
76 + }
90 77
91 - /**
92 - * Modify terms args to include terms whose only docs are private, for users allowed to read them.
93 - *
94 - * Gated on being logged in rather than on `read_private_docs`, because a doc's own
95 - * author may read their private doc without holding that capability. Which docs
96 - * actually count is decided per post by can_read_doc().
97 - *
98 - * @param array $args
99 - * @return array
100 - */
101 - public function modify_terms_args_for_private_docs( $args ) {
102 - // Logged-out visitors always get WordPress' stock `hide_empty` behaviour.
103 - if ( ! is_user_logged_in() || ! isset( $args[ 'taxonomy' ] ) ) {
104 - return $args;
105 - }
78 + if ( ! in_array( 'doc_category', $term_query->query_vars['taxonomy'], true ) ) {
79 + return;
80 + }
106 81
107 - /**
108 - * Let add-ons opt their own taxonomies into the private-docs rescue path.
109 - *
110 - * BetterDocs Pro hooks this for `knowledge_base` (Multiple KB), where docs
111 - * ARE assigned to the KB term directly.
112 - *
113 - * @param array $args Term query args.
114 - */
115 - $_filtered = apply_filters( 'betterdocs_modify_terms_args_for_private_docs', $args );
116 - if ( is_array( $_filtered ) ) {
117 - $args = $_filtered;
118 - }
82 + global $current_screen;
119 83
120 - // Already handled by an add-on (e.g. Pro's Multiple KB).
121 - if ( ! empty( $args[ '_betterdocs_filter_private' ] ) ) {
122 - return $args;
123 - }
84 + if ( $current_screen == null ) {
85 + return;
86 + }
124 87
125 - /**
126 - * Taxonomies whose terms have docs assigned directly, so a private-inclusive
127 - * object count can rescue a term WordPress dropped for `hide_empty`.
128 - *
129 - * `knowledge_base` is included so a KB whose only docs are private still shows
130 - * up for privileged users even on older Pro builds that predate the filter above.
131 - */
132 - $supported_taxonomies = array( 'doc_category', 'knowledge_base' );
133 - if ( ! in_array( $args[ 'taxonomy' ], $supported_taxonomies, true ) ) {
134 - return $args;
135 - }
88 + if ( $current_screen->taxonomy !== 'doc_category' || $current_screen->id != 'edit-doc_category' ) {
89 + return;
90 + }
136 91
137 - // If hide_empty is true, we need to modify the logic to include terms with private docs
138 - if ( isset( $args[ 'hide_empty' ] ) && $args[ 'hide_empty' ] ) {
139 - // Set hide_empty to false and we'll filter manually later
140 - $args[ 'hide_empty' ] = false;
141 - // Add a flag to indicate we need to filter manually
142 - $args[ '_betterdocs_filter_private' ] = true;
143 - }
92 + $term_query->query_vars['meta_query'] = [
93 + [
94 + 'key' => 'doc_category_order',
95 + 'type' => 'NUMERIC'
96 + ]
97 + ];
144 98
145 - return $args;
146 - }
99 + $term_query->query_vars['orderby'] = 'meta_value_num';
100 + }
147 101
148 - public function parse_term_query( $term_query ) {
149 - if ( empty( $term_query->query_vars[ 'taxonomy' ] ) ) {
150 - return;
151 - }
102 + public function parse_query( &$query ) {
103 + // dump( is_single(), $query->query_vars );
104 + // if ( is_single() && isset( $query->query_vars['post_type'] ) && $query->query_vars['post_type'] == 'docs' ) {
105 + // $query->is_single = false;
106 + // $query->is_archive = true;
107 + // $query->set( 'knowledge_base', $query->query_vars['docs'] );
108 + // }
109 + }
152 110
153 - if ( ! in_array( 'doc_category', $term_query->query_vars[ 'taxonomy' ], true ) ) {
154 - return;
155 - }
111 + public function pre_get_posts( &$query ) {
112 + if ( is_admin() || ! $query->is_main_query() ) {
113 + return;
114 + }
156 115
157 - global $current_screen;
116 + if ( is_tax( 'doc_category' ) ) {
117 + $query->set( 'post_type', 'docs' );
118 + $query->set( 'posts_per_archive_page', -1 );
158 119
159 - if ( null == $current_screen ) {
160 - return;
161 - }
120 + $term = get_term_by( 'slug', $query->get( 'doc_category', '' ), 'doc_category' );
162 121
163 - if ( 'doc_category' !== $current_screen->taxonomy || 'edit-doc_category' != $current_screen->id ) {
164 - return;
165 - }
122 + $post__in = $this->get_docs_order_by_terms( $term->term_id );
123 + if ( ! empty( $post__in ) ) {
124 + $query->set( 'orderby', 'post__in' );
125 + $query->set( 'post__in', $post__in );
126 + }
166 127
167 - // Use base meta key for admin listing - fallback logic is handled in set_tax_order
168 - $meta_key = 'doc_category_order';
128 + // if ( ! empty( $query->query_vars['knowledge_base'] ) ) {
129 + // $query->betterdocs_terms = get_terms( [
130 + // 'taxonomy' => 'doc_category',
131 + // 'parent' => 0,
132 + // 'hide_empty' => true,
133 + // 'meta_query' => [
134 + // 'relation' => 'OR',
135 + // [
136 + // 'key' => 'doc_category_knowledge_base',
137 + // 'value' => $query->query_vars['knowledge_base'],
138 + // 'compare' => 'LIKE'
139 + // ]
140 + // ]
141 + // ] );
142 + // }
143 + }
169 144
170 - $term_query->query_vars[ 'meta_query' ] = array(
171 - array(
172 - 'key' => $meta_key,
173 - 'type' => 'NUMERIC'
174 - )
175 - );
145 + // dump( is_archive(), $query->query_vars );
146 + }
176 147
177 - $term_query->query_vars[ 'orderby' ] = 'meta_value_num';
178 - }
148 + /**
149 + * Get docs orders by term_id.
150 + *
151 + * @since 2.5.0
152 + * @param int $term_id
153 + *
154 + * @return array
155 + */
156 + public function get_docs_order_by_terms( $term_id ) {
157 + global $wpdb;
158 + $_docs_order = get_term_meta( $term_id, '_docs_order', true );
159 + $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}term_relationships WHERE term_taxonomy_id = %d", $term_id );
160 + $query_key = "docs_order_by_terms_{$term_id}_" . md5( $query );
179 161
180 - public function parse_query( &$query ) {
181 - // dump( is_single(), $query->query_vars );
182 - // if ( is_single() && isset( $query->query_vars['post_type'] ) && $query->query_vars['post_type'] == 'docs' ) {
183 - // $query->is_single = false;
184 - // $query->is_archive = true;
185 - // $query->set( 'knowledge_base', $query->query_vars['docs'] );
186 - // }
187 - }
162 + if ( ( $results = $this->database->get_cache( $query_key ) ) !== false ) {
163 + return $results;
164 + }
188 165
189 - public function pre_get_posts( &$query ) {
190 - if ( is_admin() || ! $query->is_main_query() ) {
191 - return;
192 - }
166 + if ( ! empty( $_docs_order ) ) {
167 + $_docs_order = explode( ',', $_docs_order );
168 + $new_ids = [];
193 169
194 - if ( is_tax( 'doc_category' ) ) {
195 - $query->set( 'post_type', 'docs' );
196 - $query->set( 'posts_per_archive_page', -1 );
170 + $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
197 171
198 - $term = get_term_by( 'slug', $query->get( 'doc_category', '' ), 'doc_category' );
172 + if ( is_array( $results ) && ! empty( $results ) ) {
173 + $object_ids = array_filter(
174 + $results,
175 + function ( $value ) use ( $_docs_order ) {
176 + return ! in_array( $value->object_id, $_docs_order );
177 + }
178 + );
199 179
200 - if ( $term && isset( $term->term_id ) ) {
201 - $post__in = $this->get_docs_order_by_terms( $term->term_id );
202 - } else {
203 - $post__in = array();
204 - }
205 - if ( ! empty( $post__in ) ) {
206 - $query->set( 'orderby', 'post__in' );
207 - $query->set( 'post__in', $post__in );
208 - }
180 + if ( ! empty( $object_ids ) ) {
181 + array_walk(
182 + $object_ids,
183 + function ( $value ) use ( &$new_ids ) {
184 + $new_ids[] = $value->object_id;
185 + }
186 + );
187 + }
188 + }
209 189
210 - // if ( ! empty( $query->query_vars['knowledge_base'] ) ) {
211 - // $query->betterdocs_terms = get_terms( [
212 - // 'taxonomy' => 'doc_category',
213 - // 'parent' => 0,
214 - // 'hide_empty' => true,
215 - // 'meta_query' => [
216 - // 'relation' => 'OR',
217 - // [
218 - // 'key' => 'doc_category_knowledge_base',
219 - // 'value' => $query->query_vars['knowledge_base'],
220 - // 'compare' => 'LIKE'
221 - // ]
222 - // ]
223 - // ] );
224 - // }
225 - }
190 + $_docs_order = array_merge( $new_ids, $_docs_order );
191 + $this->database->set_cache( $query_key, $_docs_order, 1 );
226 192
227 - // dump( is_archive(), $query->query_vars );
228 - }
193 + return $_docs_order;
194 + }
229 195
230 - /**
231 - * Get docs orders by term_id.
232 - *
233 - * @since 2.5.0
234 - * @param int $term_id
235 - *
236 - * @return array
237 - */
238 - public function get_docs_order_by_terms( $term_id ) {
239 - global $wpdb;
196 + return [];
197 + }
240 198
241 - // Get language-specific meta key with fallback
242 - $meta_key = \WPDeveloper\BetterDocs\Utils\Helper::get_meta_key_with_fallback( '_docs_order', $term_id );
243 - $_docs_order = get_term_meta( $term_id, $meta_key, true );
199 + /**
200 + * For determine the next post ID
201 + *
202 + * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/
203 + *
204 + * @param mixed $where
205 + * @param mixed $in_same_term
206 + * @param mixed $excluded_terms
207 + * @param mixed $taxonomy
208 + * @param mixed $post
209 + * @return mixed
210 + */
211 + public function next_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) {
212 + return $this->get_adjacent_post_id( 'next', $where, $post, $taxonomy );
213 + }
244 214
245 - $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}term_relationships WHERE term_taxonomy_id = %d", $term_id );
246 - $query_key = "docs_order_by_terms_{$term_id}_{$meta_key}_" . md5( $query );
215 + /**
216 + * For determine the previous post ID
217 + *
218 + * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/
219 + *
220 + * @param mixed $where
221 + * @param mixed $in_same_term
222 + * @param mixed $excluded_terms
223 + * @param mixed $taxonomy
224 + * @param mixed $post
225 + * @return mixed
226 + */
227 + public function previous_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) {
228 + return $this->get_adjacent_post_id( 'previous', $where, $post, $taxonomy );
229 + }
247 230
248 - if ( ( $results = $this->database->get_cache( $query_key ) ) !== false ) {
249 - return $results;
250 - }
231 + /**
232 + * Get where clause for next/previous post ID mainly used in post navigation.
233 + *
234 + * @see `previous_post_where` and `next_post_where` methods.
235 + *
236 + * @param mixed $adjacent
237 + * @param mixed $where
238 + * @param mixed $post
239 + * @param mixed $taxonomy
240 + * @return mixed
241 + */
242 + private function get_adjacent_post_id( $adjacent, $where, $post, $taxonomy ) {
243 + if ( $taxonomy !== 'doc_category' ) {
244 + return $where;
245 + }
251 246
252 - if ( ! empty( $_docs_order ) ) {
253 - $_docs_order = explode( ',', $_docs_order );
254 - $new_ids = array();
247 + $_id = null;
248 + $_terms = get_the_terms( $post->ID, 'doc_category' );
249 + if ( empty( $_terms ) ) {
250 + return $where;
251 + }
255 252
256 - // $query is prepared upstream; results are cached via $this->database->get_cache above (line 210).
257 - $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
253 + global $wp_query, $wpdb;
258 254
259 - if ( is_array( $results ) && ! empty( $results ) ) {
260 - $object_ids = array_filter(
261 - $results,
262 - function ( $value ) use ( $_docs_order ) {
263 - return ! in_array( $value->object_id, $_docs_order );
264 - }
265 - );
255 + $_docs_order = $this->get_docs_order_by_terms( $_terms[0]->term_id );
266 256
267 - if ( ! empty( $object_ids ) ) {
268 - array_walk(
269 - $object_ids,
270 - function ( $value ) use ( &$new_ids ) {
271 - $new_ids[ ] = $value->object_id;
272 - }
273 - );
274 - }
275 - }
257 + $_where = explode( 'AND', $where );
258 + if ( is_array( $_where ) ) {
259 + // $_where[0] = str_replace( 'WHERE ', '', $_where[0] );
260 + unset( $_where[0] );
261 + $_where = implode( 'AND', $_where );
262 + }
276 263
277 - $_docs_order = array_merge( $new_ids, $_docs_order );
278 - $this->database->set_cache( $query_key, $_docs_order, 1 );
264 + $_orderby = $this->settings->get( 'alphabetically_order_post', 'betterdocs_order' );
265 + $_order = $this->settings->get( 'docs_order', 'ASC' );
266 + $_docs_order = $_orderby === 'betterdocs_order' ? $_docs_order : [];
279 267
280 - return $_docs_order;
281 - }
268 + if ( empty( $_docs_order ) ) {
269 + $statuses = [ 'publish' ];
282 270
283 - return array();
284 - }
271 + if ( is_user_logged_in() ) {
272 + $statuses[] = 'private';
273 + }
285 274
286 - /**
287 - * For determine the next post ID
288 - *
289 - * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/
290 - *
291 - * @param mixed $where
292 - * @param mixed $in_same_term
293 - * @param mixed $excluded_terms
294 - * @param mixed $taxonomy
295 - * @param mixed $post
296 - * @return mixed
297 - */
298 - public function next_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) {
299 - return $this->get_adjacent_post_id( 'next', $where, $post, $taxonomy );
300 - }
275 + $_args = [
276 + 'post_status' => $statuses
277 + ];
278 + if ( isset( $wp_query->query_vars['doc_category'] ) ) {
279 + $_args['tax_query'][] = [
280 + 'taxonomy' => 'doc_category',
281 + 'field' => 'slug',
282 + 'terms' => $wp_query->query_vars['doc_category'],
283 + 'operator' => 'AND',
284 + 'include_children' => false
285 + ];
286 + }
301 287
302 - /**
303 - * For determine the previous post ID
304 - *
305 - * @link https://developer.wordpress.org/reference/hooks/get_adjacent_post_where/
306 - *
307 - * @param mixed $where
308 - * @param mixed $in_same_term
309 - * @param mixed $excluded_terms
310 - * @param mixed $taxonomy
311 - * @param mixed $post
312 - * @return mixed
313 - */
314 - public function previous_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) {
315 - return $this->get_adjacent_post_id( 'previous', $where, $post, $taxonomy );
316 - }
288 + $_args['orderby'] = $_orderby;
289 + if ( $_orderby != 'betterdocs_order' ) {
290 + $_args['order'] = $_order;
291 + }
317 292
318 - /**
319 - * Get where clause for next/previous post ID mainly used in post navigation.
320 - *
321 - * @see `previous_post_where` and `next_post_where` methods.
322 - *
323 - * @param mixed $adjacent
324 - * @param mixed $where
325 - * @param mixed $post
326 - * @param mixed $taxonomy
327 - * @return mixed
328 - */
329 - private function get_adjacent_post_id( $adjacent, $where, $post, $taxonomy ) {
330 - if ( 'doc_category' !== $taxonomy ) {
331 - return $where;
332 - }
293 + /**
294 + * Before Query
295 + */
296 + do_action_ref_array( 'betterdocs_navigation_docs_query', [ &$_args ] );
297 + $docs = $this->get_posts( $this->docs_query_args( $_args ) );
333 298
334 - $_id = null;
335 - $_terms = get_the_terms( $post->ID, 'doc_category' );
336 - if ( empty( $_terms ) ) {
337 - return $where;
338 - }
299 + $_docs = [];
300 + if ( $docs->have_posts() ) {
301 + array_map(
302 + function ( $post ) use ( &$_docs ) {
303 + $_docs[] = $post->ID;
304 + },
305 + $docs->posts
306 + );
307 + }
339 308
340 - global $wp_query, $wpdb;
309 + $_docs_order = $_docs;
310 + }
341 311
342 - $_docs_order = $this->get_docs_order_by_terms( $_terms[ 0 ]->term_id );
312 + $_docs_order = apply_filters( 'betterdocs_adjacent_docs_order', $_docs_order, $_terms );
343 313
344 - $_orderby = $this->settings->get( 'alphabetically_order_post', 'betterdocs_order' );
345 - $_order = $this->settings->get( 'docs_order', 'ASC' );
346 - $_docs_order = 'betterdocs_order' === $_orderby ? $_docs_order : array();
314 + $_id_index = array_search( $post->ID, $_docs_order );
315 + $_id_index = $adjacent === 'next' ? $_id_index + 1 : $_id_index - 1;
316 + $_id = isset( $_docs_order[ $_id_index ] ) ? (int) $_docs_order[ $_id_index ] : null;
347 317
348 - if ( empty( $_docs_order ) ) {
349 - $statuses = array( 'publish' );
318 + return $wpdb->prepare( "WHERE p.ID = %s AND $_where", (int) $_id );
319 + }
350 320
351 - if ( is_user_logged_in() ) {
352 - $statuses[ ] = 'private';
353 - }
321 + public function parse_terms_args( $args = [] ) {
322 + $_default_args = [
323 + 'hide_empty' => true,
324 + 'taxonomy' => 'doc_category'
325 + ];
354 326
355 - $_args = array(
356 - 'post_status' => $statuses,
357 - // Required: with an explicit `post_status` WordPress only narrows private
358 - // docs to the ones the user may read when `perm` is `readable`. Without it,
359 - // listing `private` would expose every private doc to any logged-in user.
360 - 'perm' => 'readable',
361 - 'term_id' => $_terms[ 0 ]->term_id
362 - );
363 - if ( isset( $wp_query->query_vars[ 'doc_category' ] ) ) {
364 - $_args[ 'tax_query' ][ ] = array(
365 - 'taxonomy' => 'doc_category',
366 - 'field' => 'slug',
367 - 'terms' => $wp_query->query_vars[ 'doc_category' ],
368 - 'operator' => 'AND',
369 - 'include_children' => false
370 - );
371 - } elseif ( isset( $wp_query->query_vars[ 'name' ] ) && isset( $wp_query->query_vars[ 'post_type' ] ) && 'docs' == $wp_query->query_vars[ 'post_type' ] ) {
372 - $_post = get_page_by_path( $wp_query->query_vars[ 'name' ], OBJECT, 'docs' );
373 - $doc_terms = array();
327 + // OrderBy & Order
328 + $_orderby = ! empty( $args['orderby'] ) && $args['orderby'] != 1 ? $args['orderby'] : 'name';
329 + $_order = ! empty( $args['order'] ) ? $args['order'] : '';
374 330
375 - if ( isset( $_post->ID ) ) {
376 - $terms = get_the_terms( $_post->ID, 'doc_category' );
377 - if ( ! empty( $terms ) ) {
378 - $doc_terms = wp_list_pluck( $terms, 'term_id' );
379 - }
380 - }
331 + if ( $_orderby == 'betterdocs_order' ) {
332 + $args['meta_key'] = 'doc_category_order';
333 + $args['orderby'] = 'meta_value_num';
334 + $args['order'] = 'ASC';
335 + } else {
336 + $args['orderby'] = $_orderby;
337 + $args['order'] = $_order;
338 + }
381 339
382 - if ( ! empty( $doc_terms ) ) {
383 - $_args[ 'tax_query' ][ ] = array(
384 - 'taxonomy' => 'doc_category',
385 - 'field' => 'term_id',
386 - 'terms' => $doc_terms,
387 - 'operator' => 'AND',
388 - 'include_children' => false
389 - );
390 - $_args[ 'term_id' ] = $doc_terms[ 0 ];
391 - }
392 - } else {
393 - // Fallback: use the term from current post
394 - $_args[ 'tax_query' ][ ] = array(
395 - 'taxonomy' => 'doc_category',
396 - 'field' => 'term_id',
397 - 'terms' => $_terms[ 0 ]->term_id,
398 - 'operator' => 'AND',
399 - 'include_children' => false
400 - );
401 - }
340 + // Nested Sub Category
341 + // $args['parent'] = 0;
342 + // if ( $nested_subcategory == true ) {
343 + // }
402 344
403 - $_args[ 'orderby' ] = $_orderby;
404 - if ( 'betterdocs_order' != $_orderby ) {
405 - $_args[ 'order' ] = $_order;
406 - }
345 + if ( ! isset( $args['number'] ) ) {
346 + global $wp_query;
347 + if ( $wp_query->query === null || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) {
348 + $args['number'] = 4;
349 + }
350 + }
407 351
408 - /**
409 - * Before Query
410 - */
411 - do_action_ref_array( 'betterdocs_navigation_docs_query', array( &$_args ) );
412 - $docs = $this->get_posts( $this->docs_query_args( $_args ) );
352 + // Includes
353 + if ( isset( $args['include'] ) ) {
354 + $_include = ! is_array( $args['include'] ) ? explode( ',', $args['include'] ) : $args['include'];
355 + $args['include'] = $_include;
356 + $args['orderby'] = 'include';
413 357
414 - $_docs = array();
415 - if ( $docs->have_posts() ) {
416 - array_map(
417 - function ( $_post ) use ( &$_docs ) {
418 - $_docs[ ] = $_post->ID;
419 - },
420 - $docs->posts
421 - );
422 - }
358 + unset( $args['parent'] );
359 + }
423 360
424 - $_docs_order = $_docs;
425 - }
361 + $_meta_query = ! empty( $args['meta_query'] ) ? $args['meta_query'] : [];
362 + $args['meta_query'] = apply_filters( 'betterdocs_taxonomy_object_meta_query', $_meta_query, $args );
426 363
427 - $_docs_order = apply_filters( 'betterdocs_adjacent_docs_order', $_docs_order, $_terms );
364 + return apply_filters( 'betterdocs_category_terms_object', wp_parse_args( $args, $_default_args ), $args );
365 + }
428 366
429 - // Ensure consistent type comparison by converting all IDs to integers
430 - $_docs_order = array_map( 'intval', $_docs_order );
431 - $_docs_order = array_values( $_docs_order ); // Re-index array after type conversion
367 + public function get_terms( $args ) {
368 + return get_terms( $this->parse_terms_args( $args ) );
369 + }
432 370
433 - $_id_index = array_search( (int) $post->ID, $_docs_order, true );
371 + public function get_child_terms( $args ) {
372 + if ( ! isset( $args['number'] ) ) {
373 + global $wp_query;
374 + if ( $wp_query->query === null || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) {
375 + $args['number'] = 1;
376 + }
377 + }
434 378
435 - // If current post not found in order list, return original where clause
436 - if ( false === $_id_index ) {
437 - return $where;
438 - }
379 + return $this->get_terms( $args );
380 + }
439 381
440 - $_id_index = 'next' === $adjacent ? $_id_index + 1 : $_id_index - 1;
441 - $_id = isset( $_docs_order[ $_id_index ] ) ? (int) $_docs_order[ $_id_index ] : null;
382 + /**
383 + * Get POSTs of Docs type.
384 + *
385 + * @param mixed $args
386 + * @return WP_Query
387 + */
388 + public function get_posts( $args, $ignore = false ) {
389 + if ( ! $ignore ) {
390 + $args = $this->docs_query_args( $args );
391 + }
442 392
443 - // Fix: replace only ID comparison in where clause using regex
444 - if ( $_id ) {
445 - // This replaces any 'p.ID < N', 'p.ID > N', 'p.ID <= N', or 'p.ID >= N' etc, with 'p.ID = $_id'
446 - $where = preg_replace( '/p\.ID\s*[<>!=]+\s*\d+/', 'p.ID = ' . (int) $_id, $where );
447 - }
393 + return new WP_Query( $args );
394 + }
448 395
449 - return $where;
450 - }
396 + public function get_taxonomy( $tax = '' ) {
397 + global $wp_query;
398 + if ( is_tax( 'knowledge_base' ) ) {
399 + $_tax = $wp_query->tax_query->queried_terms;
400 + if ( array_key_exists( 'doc_category', $_tax ) ) {
401 + $tax = 'doc_category';
402 + } else {
403 + $tax = 'knowledge_base';
404 + }
405 + } elseif ( is_tax( 'doc_category' ) ) {
406 + $tax = 'doc_category';
407 + }
451 408
452 - public function parse_terms_args( $args = array() ) {
453 - $_default_args = array(
454 - 'hide_empty' => true,
455 - 'taxonomy' => 'doc_category'
456 - );
409 + return $tax;
410 + }
457 411
458 - // OrderBy & Order
459 - $_orderby = ! empty( $args[ 'orderby' ] ) && 1 != $args[ 'orderby' ] ? $args[ 'orderby' ] : 'name';
460 - $_order = ! empty( $args[ 'order' ] ) ? $args[ 'order' ] : '';
412 + public function terms_query( $args = [] ) {
413 + global $wp_query;
414 + $_origin_args = $args;
461 415
462 - if ( 'betterdocs_order' == $_orderby ) {
463 - // Use base meta key - fallback logic will be handled in the terms_clauses filter
464 - $args[ 'meta_key' ] = 'doc_category_order';
465 - $args[ 'orderby' ] = 'meta_value_num';
466 - $args[ 'order' ] = 'ASC';
467 - } else {
468 - $args[ 'orderby' ] = $_orderby;
469 - $args[ 'order' ] = $_order;
470 - }
416 + $default_args = [
417 + 'hide_empty' => true,
418 + 'taxonomy' => 'doc_category',
419 + 'orderby' => 'name'
420 + ];
471 421
472 - // Nested Sub Category
473 - // $args['parent'] = 0;
474 - // if ( $nested_subcategory == true ) {
475 - // }
422 + /**
423 + * Number Set
424 + *
425 + * @FIX: If Built-in Docs page off and docs_page in use, then Terms Query Number 4|1 set.
426 + */
427 + // if ( $wp_query->query === NULL || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) {
428 + // $default_args['number'] = 1;
429 + // }
476 430
477 - if ( ! isset( $args[ 'number' ] ) ) {
478 - global $wp_query;
479 - if ( null === $wp_query->query || ( isset( $wp_query->query[ 'post_type' ] ) && 'docs' != $wp_query->query[ 'post_type' ] ) ) {
480 - $args[ 'number' ] = 4;
481 - }
482 - }
431 + /**
432 + * Nested Sub Category
433 + */
434 + if ( isset( $args['nested_subcategory'] ) && $args['nested_subcategory'] == true ) {
435 + $default_args['parent'] = 0;
436 + unset( $args['nested_subcategory'] );
437 + // if ( $wp_query->query === NULL || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) {
438 + // $default_args['number'] = 4;
439 + // }
440 + }
483 441
484 - // Includes
485 - if ( isset( $args[ 'include' ] ) ) {
486 - $_include = ! is_array( $args[ 'include' ] ) ? explode( ',', $args[ 'include' ] ) : $args[ 'include' ];
487 - $args[ 'include' ] = $_include;
488 - $args[ 'orderby' ] = 'include';
442 + /**
443 + * OrderBy and Order
444 + */
445 + if ( ! isset( $args['orderby'] ) ) {
446 + $_orderby = $this->settings->get( 'terms_orderby', 'name' );
447 + $_order = $this->settings->get( 'terms_order', '' );
448 + } else {
449 + $_orderby = $args['orderby'];
450 + $_order = ! empty( $args['order'] ) ? $args['order'] : '';
451 + }
489 452
490 - unset( $args[ 'parent' ] );
491 - }
453 + if ( 'betterdocs_order' === $_orderby ) {
454 + $default_args['meta_key'] = 'doc_category_order';
455 + $_orderby = 'meta_value_num';
456 + $_order = 'ASC';
457 + } elseif ( $_orderby === true ) {
458 + $_orderby = 'name';
459 + }
492 460
493 - $_meta_query = ! empty( $args[ 'meta_query' ] ) ? $args[ 'meta_query' ] : array();
494 - $args[ 'meta_query' ] = apply_filters( 'betterdocs_taxonomy_object_meta_query', $_meta_query, $args );
461 + $args['orderby'] = $_orderby;
462 + if ( ! empty( $_order ) ) {
463 + $args['order'] = $_order;
464 + }
495 465
496 - return apply_filters( 'betterdocs_category_terms_object', wp_parse_args( $args, $_default_args ), $args );
497 - }
466 + /**
467 + * @todo old hook
468 + * hook: betterdocs_child_taxonomy_meta_query
469 + */
470 + $_multiple_kb = apply_filters(
471 + 'betterdocs_query_args_multiple_kb_enabled',
472 + isset( $args['multiple_kb'] ) ? (bool) $args['multiple_kb'] : false,
473 + $_origin_args
474 + );
498 475
499 - public function get_terms( $args ) {
500 - $parsed_args = $this->parse_terms_args( $args );
501 - $terms = get_terms( $parsed_args );
476 + $_kb_slug = isset( $args['kb_slug'] ) ? trim( $args['kb_slug'] ) : '';
502 477
503 - // Filter terms manually if we need to consider private docs the current user may read
504 - if ( isset( $parsed_args[ '_betterdocs_filter_private' ] ) && $parsed_args[ '_betterdocs_filter_private' ] && is_user_logged_in() ) {
505 - $terms = array_filter( $terms, function ( $term ) {
506 - // Get the actual count including private docs for users with read_private_docs capability
507 - return $this->get_private_inclusive_docs_count( $term ) > 0;
508 - } );
509 - }
478 + unset( $args['multiple_kb'] );
479 + unset( $args['kb_slug'] );
510 480
511 - return $terms;
512 - }
481 + $meta_query = ! empty( $args['meta_query'] ) ? $args['meta_query'] : [];
482 + $meta_query = apply_filters( 'betterdocs_terms_meta_query_args', $meta_query, $_multiple_kb, $_kb_slug, $_origin_args );
513 483
514 - /**
515 - * Drop terms with no visible docs after `hide_empty` was disabled for private docs.
516 - *
517 - * modify_terms_args_for_private_docs() turns `hide_empty` off so WordPress stops
518 - * dropping terms whose only docs are private. Without this counterpart, every
519 - * genuinely empty term would leak into the listing for privileged users. Runs only
520 - * for queries carrying the `_betterdocs_filter_private` flag, so anonymous and
521 - * unflagged queries are untouched.
522 - *
523 - * @param array|WP_Error $terms
524 - * @param array|null $taxonomies
525 - * @param array $args
526 - * @return array|WP_Error
527 - */
528 - public function filter_terms_for_private_docs( $terms, $taxonomies, $args ) {
529 - if ( empty( $args[ '_betterdocs_filter_private' ] ) || empty( $terms ) || is_wp_error( $terms ) ) {
530 - return $terms;
531 - }
484 + if ( ! empty( $meta_query ) ) {
485 + $default_args['meta_query'] = $meta_query;
486 + }
532 487
533 - if ( ! is_user_logged_in() ) {
534 - return $terms;
535 - }
488 + if ( ! empty( $args['terms'] ) ) {
489 + $args['include'] = explode( ',', $args['terms'] );
490 + $args['orderby'] = 'include';
491 + $args['order'] = 'ASC';
536 492
537 - // Only term objects can be counted; `ids`, `names`, `count`, ... pass through.
538 - $_fields = isset( $args[ 'fields' ] ) ? $args[ 'fields' ] : 'all';
539 - if ( ! in_array( $_fields, array( 'all', 'all_with_object_id' ), true ) ) {
540 - return $terms;
541 - }
493 + unset( $default_args['parent'] );
494 + unset( $args['parent'] );
495 + unset( $args['terms'] );
496 + }
542 497
543 - $_filtered = array_filter( $terms, function ( $term ) {
544 - if ( ! is_object( $term ) ) {
545 - return true;
546 - }
498 + $_query_args = wp_parse_args( $args, $default_args );
499 + return apply_filters( 'betterdocs_terms_query_args', $_query_args, $_origin_args );
500 + }
547 501
548 - return $this->get_private_inclusive_docs_count( $term ) > 0;
549 - } );
502 + public function get_term_parents( $term_id, $taxonomy = 'doc_category', $args = [] ) {
503 + $term = get_term( $term_id, $taxonomy );
504 + if ( is_wp_error( $term ) ) {
505 + return $term;
506 + }
550 507
551 - return array_values( $_filtered );
552 - }
508 + if ( ! $term ) {
509 + return [];
510 + }
553 511
554 - /**
555 - * Docs count for a term, including private docs when the user may read them.
556 - *
557 - * KB terms need object-in-term counting rather than the doc_category count path,
558 - * which BetterDocs Pro provides through the filter below; get_docs_count() is the
559 - * fallback and already counts via get_objects_in_term() for the term's own taxonomy.
560 - *
561 - * @param WP_Term $term
562 - * @return int
563 - */
564 - /**
565 - * Whether the current user may see a doc in a listing.
566 - *
567 - * Public docs are visible to everyone. A private doc is visible to whoever may read
568 - * it — `read_post` maps to the base `read` capability for the doc's own author and
569 - * to `read_private_docs` for everyone else, so owners see their own private docs
570 - * without needing the capability. Other non-public statuses (draft, pending) stay
571 - * out of listings for everyone, as before.
572 - *
573 - * @param int $post_id
574 - * @return bool
575 - */
576 - /**
577 - * Cache-key fragment identifying whose visibility a cached count reflects.
578 - *
579 - * Everyone who can read every private doc sees the same numbers, so they share one
580 - * `priv` bucket; authors differ from each other and get their own. Logged-out
581 - * visitors all share bucket `0`, keeping the anonymous cache as hot as before.
582 - *
583 - * @return string
584 - */
585 - protected function count_cache_viewer_key() {
586 - if ( ! is_user_logged_in() ) {
587 - return '0';
588 - }
512 + $_lists = [];
513 + $origin_term = $term_id;
514 + $term_id = $term->term_id;
589 515
590 - if ( current_user_can( 'read_private_docs' ) ) {
591 - return 'priv';
592 - }
516 + $defaults = [
517 + 'format' => 'name',
518 + 'inclusive' => true
519 + ];
593 520
594 - return 'u' . get_current_user_id();
595 - }
521 + $args = wp_parse_args( $args, $defaults );
596 522
597 - public function can_read_doc( $post_id ) {
598 - if ( is_post_publicly_viewable( $post_id ) ) {
599 - return true;
600 - }
523 + $args['inclusive'] = wp_validate_boolean( $args['inclusive'] );
601 524
602 - return 'private' === get_post_status( $post_id ) && current_user_can( 'read_post', $post_id );
603 - }
525 + $parents = get_ancestors( $term_id, $taxonomy );
604 526
605 - protected function get_private_inclusive_docs_count( $term ) {
606 - /**
607 - * Let add-ons supply their own private-inclusive count for a term.
608 - *
609 - * Passing `null` means "not handled" — implementations that don't recognise the
610 - * term's taxonomy return the value untouched, and we fall back to get_docs_count().
611 - *
612 - * @param int|null $count
613 - * @param WP_Term $term
614 - */
615 - $_count = apply_filters( 'betterdocs_get_term_docs_count_for_private_filter', null, $term );
527 + if ( $args['inclusive'] ) {
528 + array_unshift( $parents, $term_id );
529 + }
616 530
617 - if ( null !== $_count ) {
618 - return (int) $_count;
619 - }
531 + foreach ( array_reverse( $parents ) as $term_id ) {
532 + $parent = get_term( $term_id, $taxonomy );
533 + $name = ( 'slug' === $args['format'] ) ? $parent->slug : $parent->name;
534 + $term_permalink = get_term_link( $parent->term_id, $taxonomy );
535 + $term_permalink = apply_filters( 'betterdocs_breadcrumb_term_permalink', $term_permalink, $term_id );
620 536
621 - // Count nested sub-category docs too, so a parent whose docs live only in its
622 - // children stays visible to logged-in users exactly as it does for anonymous
623 - // visitors (whose grid is descendant-aware). get_docs_count( …, true ) already
624 - // filters each descendant doc through can_read_doc() via get_doc_ids_by_term(),
625 - // so private/unreadable docs never resurrect a term. A non-nested count here
626 - // hid those parents from logged-in users only.
627 - return (int) $this->get_docs_count( $term, true );
628 - }
537 + $_item = [
538 + 'url' => $term_permalink,
539 + 'text' => $name
540 + ];
629 541
630 - public function get_child_terms( $args ) {
631 - if ( ! isset( $args[ 'number' ] ) ) {
632 - global $wp_query;
633 - if ( null === $wp_query->query || ( isset( $wp_query->query[ 'post_type' ] ) && 'docs' != $wp_query->query[ 'post_type' ] ) ) {
634 - $args[ 'number' ] = 1;
635 - }
636 - }
542 + $_lists[] = $_item;
543 + }
637 544
638 - return $this->get_terms( $args );
639 - }
545 + return apply_filters( 'betterdocs_breadcrumb_archive_lists', $_lists, $origin_term );
546 + }
640 547
641 - /**
642 - * Get POSTs of Docs type.
643 - *
644 - * @param mixed $args
645 - * @return WP_Query
646 - */
647 - public function get_posts( $args, $ignore = false ) {
648 - if ( ! $ignore ) {
649 - $args = $this->docs_query_args( $args );
650 - }
548 + /**
549 + * Get all non-empty child term IDs recursively for a given taxonomy and parent term.
550 + *
551 + * This function retrieves all child terms for a specified taxonomy and parent term,
552 + * recursively fetching child terms of child terms, and returns only those with a non-zero post count.
553 + *
554 + * @param string $taxonomy The taxonomy name (e.g., 'doc_category').
555 + * @param int $parent_id The ID of the parent term to start retrieving children from.
556 + *
557 + * @return array An array of non-empty child term IDs.
558 + */
559 + public function get_all_child_term_ids( $taxonomy, $parent_id ) {
560 + // Set up the arguments for retrieving child terms
561 + $args = apply_filters(
562 + 'betterdocs_get_child_term_ids_args',
563 + [
564 + 'taxonomy' => $taxonomy,
565 + 'parent' => $parent_id,
566 + 'hide_empty' => true,
567 + 'fields' => 'ids'
568 + ]
569 + );
651 570
652 - return new WP_Query( $args );
653 - }
571 + // Get the terms based on the arguments
572 + $terms = get_terms( $args );
654 573
655 - public function get_taxonomy( $tax = '' ) {
656 - global $wp_query;
657 - if ( is_tax( 'knowledge_base' ) ) {
658 - $_tax = $wp_query->tax_query->queried_terms;
659 - if ( array_key_exists( 'doc_category', $_tax ) ) {
660 - $tax = 'doc_category';
661 - } else {
662 - $tax = 'knowledge_base';
663 - }
664 - } elseif ( is_tax( 'doc_category' ) ) {
665 - $tax = 'doc_category';
666 - }
574 + // Initialize an empty array to hold non-empty child term IDs
575 + $non_empty_children = [];
667 576
668 - return $tax;
669 - }
577 + // Check if terms were retrieved without errors and that the result isn't empty
578 + if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
579 + $term_ids = [];
670 580
671 - public function terms_query( $args = array() ) {
672 - global $wp_query;
673 - $_origin_args = $args;
581 + // Loop through each term ID
582 + foreach ( $terms as $term_id ) {
583 + // Add the current term ID to the term_ids array
584 + $term_ids[] = $term_id;
674 585
675 - $default_args = array(
676 - 'hide_empty' => true,
677 - 'taxonomy' => 'doc_category',
678 - 'orderby' => 'name'
679 - );
586 + // Recursively get child terms for the current term
587 + $child_term_ids = $this->get_all_child_term_ids( $taxonomy, $term_id );
680 588
681 - /**
682 - * Number Set
683 - *
684 - * @FIX: If Built-in Docs page off and docs_page in use, then Terms Query Number 4|1 set.
685 - */
686 - // if ( $wp_query->query === NULL || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) {
687 - // $default_args['number'] = 1;
688 - // }
589 + // If there are child terms, merge them into the term_ids array
590 + if ( ! empty( $child_term_ids ) ) {
591 + $term_ids = array_merge( $term_ids, $child_term_ids );
592 + }
593 + }
689 594
690 - /**
691 - * Nested Sub Category
692 - */
693 - if ( isset( $args[ 'nested_subcategory' ] ) && true == $args[ 'nested_subcategory' ] ) {
694 - $default_args[ 'parent' ] = 0;
695 - unset( $args[ 'nested_subcategory' ] );
696 - // if ( $wp_query->query === NULL || ( isset( $wp_query->query['post_type'] ) && $wp_query->query['post_type'] != 'docs' ) ) {
697 - // $default_args['number'] = 4;
698 - // }
699 - }
595 + // Loop through all retrieved term IDs to filter out empty ones
596 + foreach ( $term_ids as $term_id ) {
597 + // Get the term object for the current term ID
598 + $child_term = get_term( $term_id, $taxonomy );
700 599
701 - /**
702 - * OrderBy and Order
703 - */
704 - if ( ! isset( $args[ 'orderby' ] ) ) {
705 - $_orderby = $this->settings->get( 'terms_orderby', 'name' );
706 - $_order = $this->settings->get( 'terms_order', '' );
707 - } else {
708 - $_orderby = $args[ 'orderby' ];
709 - $_order = ! empty( $args[ 'order' ] ) ? $args[ 'order' ] : '';
710 - }
600 + // Only include terms that have a non-zero post count
601 + if ( $child_term && $child_term->count > 0 ) {
602 + $non_empty_children[] = $child_term->term_id;
603 + }
604 + }
605 + }
711 606
712 - if ( 'betterdocs_order' === $_orderby ) {
713 - // Use different meta keys for different taxonomies
714 - if ( isset( $args[ 'taxonomy' ] ) && 'knowledge_base' === $args[ 'taxonomy' ] ) {
715 - $default_args[ 'meta_key' ] = 'kb_order';
716 - } else {
717 - $default_args[ 'meta_key' ] = 'doc_category_order';
718 - }
719 - $_orderby = 'meta_value_num';
720 - $_order = 'ASC';
721 - } elseif ( true === $_orderby ) {
722 - $_orderby = 'name';
723 - }
607 + // Return the final array of non-empty child term IDs
608 + return $non_empty_children;
609 + }
724 610
725 - $args[ 'orderby' ] = $_orderby;
726 - if ( ! empty( $_order ) ) {
727 - $args[ 'order' ] = $_order;
728 - }
611 + /**
612 + * Get all nested child term IDs of a specific parent term in a taxonomy and return as a comma-separated string.
613 + *
614 + * @param string $taxonomy The taxonomy.
615 + * @param int $parent_id The parent term ID.
616 + * @return string The comma-separated term IDs.
617 + */
618 + public function get_child_term_ids_by_parent_id( $taxonomy, $parent_id ) {
619 + $term_ids = $this->get_all_child_term_ids( $taxonomy, $parent_id );
620 + return implode( ',', $term_ids );
621 + }
729 622
730 - /**
731 - * @todo old hook
732 - * hook: betterdocs_child_taxonomy_meta_query
733 - */
734 - $_multiple_kb = apply_filters(
735 - 'betterdocs_query_args_multiple_kb_enabled',
736 - isset( $args[ 'multiple_kb' ] ) ? (bool) $args[ 'multiple_kb' ] : false,
737 - $_origin_args
738 - );
623 + public function get_terms_children( $taxonomy, $parent_id ) {
624 + $children = get_term_children( $parent_id, $taxonomy );
625 + $non_empty_children = [];
626 + if ( ! is_wp_error( $children ) && ! empty( $children ) ) {
627 + foreach ( $children as $child_id ) {
628 + $child_term = get_term( $child_id, $taxonomy );
739 629
740 - $_kb_slug = isset( $args[ 'kb_slug' ] ) ? trim( $args[ 'kb_slug' ] ) : '';
630 + // Only include non-empty terms
631 + if ( $child_term && $child_term->count > 0 ) {
632 + $non_empty_children[] = $child_term;
633 + }
634 + }
635 + }
741 636
742 - unset( $args[ 'multiple_kb' ] );
743 - unset( $args[ 'kb_slug' ] );
637 + return $non_empty_children;
638 + }
744 639
745 - $meta_query = ! empty( $args[ 'meta_query' ] ) ? $args[ 'meta_query' ] : array();
746 - $meta_query = apply_filters( 'betterdocs_terms_meta_query_args', $meta_query, $_multiple_kb, $_kb_slug, $_origin_args );
640 + public function get_terms_children_ids( $taxonomy, $parent_id ) {
641 + $children = $this->get_terms_children( $taxonomy, $parent_id );
642 + $term_ids = wp_list_pluck( $children, 'term_id' );
747 643
748 - if ( ! empty( $meta_query ) ) {
749 - $default_args[ 'meta_query' ] = $meta_query;
750 - }
644 + return implode( ',', $term_ids );
645 + }
751 646
752 - if ( ! empty( $args[ 'terms' ] ) ) {
753 - $args[ 'include' ] = explode( ',', $args[ 'terms' ] );
754 - $args[ 'orderby' ] = 'include';
755 - $args[ 'order' ] = 'ASC';
647 + public function count_terms_children( $taxonomy, $parent_id ) {
648 + return count( $this->get_terms_children( $taxonomy, $parent_id ) );
649 + }
756 650
757 - unset( $default_args[ 'parent' ] );
758 - unset( $args[ 'parent' ] );
759 - unset( $args[ 'terms' ] );
760 - }
651 + public function is_inner_templates() {
652 + if ( is_tax( 'knowledge_base' ) || is_tax( 'doc_category' ) || is_tax( 'doc_tag' ) || is_singular( 'docs' ) ) {
653 + return true;
654 + }
655 + return false;
656 + }
761 657
762 - $_query_args = wp_parse_args( $args, $default_args );
763 - $_query_args = apply_filters( 'betterdocs_terms_query_args', $_query_args, $_origin_args );
658 + /**
659 + * Summary of docs_query_args
660 + * @param mixed $args
661 + * @throws \Exception
662 + * @return mixed
663 + */
664 + public function docs_query_args( $args, $filter = [] ) {
665 + $_origin_args = $args;
764 666
765 - // Apply private docs logic for logged-in users
766 - $_query_args = $this->modify_terms_args_for_private_docs( $_query_args );
667 + $default_args = [
668 + 'post_type' => 'docs'
669 + ];
767 670
768 - return $_query_args;
769 - }
671 + if ( ! empty( $args['post_type'] ) && trim( $args['post_type'] ) === 'docs_any' ) {
672 + $default_args['post_type'] = 'docs';
673 + $default_args['post_status'] = 'any';
770 674
771 - public function get_term_parents( $term_id, $taxonomy = 'doc_category', $args = array() ) {
772 - $term = get_term( $term_id, $taxonomy );
773 - if ( is_wp_error( $term ) ) {
774 - return $term;
775 - }
675 + unset( $args['post_type'] );
676 + }
776 677
777 - if ( ! $term || ! isset( $term->term_id ) ) {
778 - return array();
779 - }
678 + /**
679 + * OrderBy and Order
680 + */
681 + if ( ! isset( $args['orderby'] ) ) {
682 + $_orderby = $this->settings->get( 'alphabetically_order_post' );
683 + $_order = $this->settings->get( 'docs_order', 'ASC' );
684 + } else {
685 + $_orderby = $args['orderby'];
686 + $_order = ! empty( $args['order'] ) ? $args['order'] : 'ASC';
687 + }
780 688
781 - $_lists = array();
782 - $origin_term = $term_id;
783 - $term_id = $term->term_id;
689 + if ( 'betterdocs_order' != $_orderby ) {
690 + if ( $_orderby === true ) {
691 + $args['orderby'] = 'title';
692 + } else {
693 + $args['orderby'] = $_orderby;
694 + }
784 695
785 - $defaults = array(
786 - 'format' => 'name',
787 - 'inclusive' => true
788 - );
696 + $args['order'] = $_order;
697 + } elseif ( 'betterdocs_order' == $_orderby ) {
698 + unset( $args['orderby'] );
699 + }
789 700
790 - $args = wp_parse_args( $args, $defaults );
701 + if ( empty( $args['orderby'] ) ) {
702 + unset( $args['order'] );
703 + }
791 704
792 - $args[ 'inclusive' ] = wp_validate_boolean( $args[ 'inclusive' ] );
705 + /**
706 + * Term ID
707 + */
708 + $_term_id = null;
709 + if ( ! empty( $args['term_id'] ) ) {
710 + $_term_id = intval( $args['term_id'] );
711 + unset( $args['term_id'] );
712 + }
793 713
794 - $parents = get_ancestors( $term_id, $taxonomy );
714 + // if ( $_term_id == null ) {
715 + // throw new \Exception( __( '$args["term_id"] cannot be null.', 'betterdocs' ) );
716 + // }
795 717
796 - if ( $args[ 'inclusive' ] ) {
797 - array_unshift( $parents, $term_id );
798 - }
718 + /**
719 + * Term Slug for tax_query
720 + */
721 + $_term_slug = '';
722 + if ( ! empty( $args['term_slug'] ) ) {
723 + $_term_slug = trim( $args['term_slug'] );
724 + unset( $args['term_slug'] );
725 + }
799 726
800 - foreach ( array_reverse( $parents ) as $term_id ) {
801 - $parent = get_term( $term_id, $taxonomy );
802 - $name = ( 'slug' === $args[ 'format' ] ) ? $parent->slug : $parent->name;
803 - $term_permalink = get_term_link( $parent->term_id, $taxonomy );
804 - $term_permalink = apply_filters( 'betterdocs_breadcrumb_term_permalink', $term_permalink, $term_id );
727 + /**
728 + * @todo old hook
729 + * hook: betterdocs_cat_template_multikb
730 + */
805 731
806 - $_item = array(
807 - 'url' => $term_permalink,
808 - 'text' => $name
809 - );
732 + $_multiple_kb = apply_filters(
733 + 'betterdocs_enable_multiple_knowledge_base',
734 + isset( $args['multiple_kb'] ) ? (bool) $args['multiple_kb'] : false,
735 + $_origin_args
736 + );
810 737
811 - $_lists[ ] = $_item;
812 - }
738 + $_kb_slug = isset( $args['kb_slug'] ) ? trim( $args['kb_slug'] ) : '';
813 739
814 - return apply_filters( 'betterdocs_breadcrumb_archive_lists', $_lists, $origin_term );
815 - }
740 + unset( $args['multiple_kb'] );
741 + unset( $args['kb_slug'] );
816 742
817 - /**
818 - * Get all non-empty child term IDs recursively for a given taxonomy and parent term.
819 - *
820 - * This function retrieves all child terms for a specified taxonomy and parent term,
821 - * recursively fetching child terms of child terms, and returns only those with a non-zero post count.
822 - *
823 - * @param string $taxonomy The taxonomy name (e.g., 'doc_category').
824 - * @param int $parent_id The ID of the parent term to start retrieving children from.
825 - *
826 - * @return array An array of non-empty child term IDs.
827 - */
828 - public function get_all_child_term_ids( $taxonomy, $parent_id ) {
829 - $version = $this->database->get_cache_version( 'betterdocs_term_counts' );
830 - $cache_key = "bd_term_counts_v{$version}_child_ids_{$taxonomy}_{$parent_id}";
743 + $tax_query = [
744 + [
745 + 'taxonomy' => 'doc_category',
746 + 'field' => 'slug',
747 + 'terms' => $_term_slug,
748 + 'operator' => 'AND',
749 + 'include_children' => false
750 + ]
751 + ];
831 752
832 - $cached = wp_cache_get( $cache_key, 'betterdocs' );
833 - if ( false !== $cached ) {
834 - return $cached;
835 - }
753 + if ( ! isset( $args['orderby'] ) || $args['orderby'] == 'betterdocs_order' ) {
754 + $args['orderby'] = 'post__in';
755 + $args['post__in'] = $this->get_docs_order_by_terms( $_term_id );
756 + }
836 757
837 - // get_terms( child_of => X ) walks the full descendant tree using WP's
838 - // internally cached term hierarchy, replacing the previous recursive
839 - // get_term()-in-a-loop pattern with one call.
840 - $args = apply_filters(
841 - 'betterdocs_get_child_term_ids_args',
842 - array(
843 - 'taxonomy' => $taxonomy,
844 - 'child_of' => $parent_id,
845 - 'hide_empty' => true,
846 - 'fields' => 'ids',
847 - )
848 - );
758 + if ( isset( $args['tax_query'] ) ) {
759 + $tax_query = $args['tax_query'];
760 + }
849 761
850 - $ids = get_terms( $args );
851 - $ids = ( is_wp_error( $ids ) || ! is_array( $ids ) ) ? array() : array_map( 'intval', $ids );
762 + $args['tax_query'] = apply_filters(
763 + 'betterdocs_docs_tax_query_args',
764 + $tax_query,
765 + $_multiple_kb,
766 + $_term_slug,
767 + $_kb_slug,
768 + $_origin_args,
769 + $this->is_inner_templates()
770 + );
771 + /**
772 + * Final parse args
773 + */
774 + $args = wp_parse_args( $args, $default_args );
852 775
853 - wp_cache_set( $cache_key, $ids, 'betterdocs', HOUR_IN_SECONDS * 6 );
776 + if ( ! empty( $filter ) ) {
777 + $filter = array_flip( $filter );
778 + $args = array_filter(
779 + $args,
780 + function ( $item ) use ( $filter ) {
781 + return ! array_key_exists( $item, $filter );
782 + },
783 + ARRAY_FILTER_USE_KEY
784 + );
785 + }
854 786
855 - return $ids;
856 - }
787 + return apply_filters( 'betterdocs_articles_args', $args, $_term_id, $_origin_args );
788 + }
857 789
858 - /**
859 - * Get all nested child term IDs of a specific parent term in a taxonomy and return as a comma-separated string.
860 - *
861 - * @param string $taxonomy The taxonomy.
862 - * @param int $parent_id The parent term ID.
863 - * @return string The comma-separated term IDs.
864 - */
865 - public function get_child_term_ids_by_parent_id( $taxonomy, $parent_id ) {
866 - $term_ids = $this->get_all_child_term_ids( $taxonomy, $parent_id );
867 - return implode( ',', $term_ids );
868 - }
790 + public function faq_terms_query_args( $includes = '', $excludes = '', $args = [] ) {
791 + $_args = [
792 + 'taxonomy' => 'betterdocs_faq_category',
793 + 'meta_key' => 'order',
794 + 'orderby' => 'meta_value_num',
795 + 'order' => 'ASC',
796 + 'include' => $includes,
797 + 'exclude' => $excludes,
798 + 'meta_query' => [
799 + [
800 + 'key' => 'status',
801 + 'value' => 1,
802 + 'compare' => '=='
803 + ]
804 + ]
805 + ];
869 806
870 - public function get_terms_children( $taxonomy, $parent_id ) {
871 - $children = get_term_children( $parent_id, $taxonomy );
872 - $non_empty_children = array();
873 - if ( ! is_wp_error( $children ) && ! empty( $children ) ) {
874 - foreach ( $children as $child_id ) {
875 - $child_term = get_term( $child_id, $taxonomy );
807 + if ( $_args['include'] == 'all' ) {
808 + unset( $_args['include'] );
809 + unset( $_args['exclude'] );
810 + }
876 811
877 - // Only include non-empty terms
878 - if ( $child_term && $child_term->count > 0 ) {
879 - $non_empty_children[ ] = $child_term;
880 - }
881 - }
882 - }
812 + if ( empty( $_args['exclude'] ) ) {
813 + unset( $_args['exclude'] );
814 + }
883 815
884 - return $non_empty_children;
885 - }
816 + if ( empty( $_args['include'] ) ) {
817 + unset( $_args['include'] );
818 + }
886 819
887 - public function get_terms_children_ids( $taxonomy, $parent_id ) {
888 - $children = $this->get_terms_children( $taxonomy, $parent_id );
889 - $term_ids = wp_list_pluck( $children, 'term_id' );
820 + return wp_parse_args( $args, $_args );
821 + }
890 822
891 - return implode( ',', $term_ids );
892 - }
823 + public function get_faq_by_term( $term_id ) {
824 + global $wpdb;
893 825
894 - public function count_terms_children( $taxonomy, $parent_id ) {
895 - return count( $this->get_terms_children( $taxonomy, $parent_id ) );
896 - }
826 + $args = [
827 + 'post_type' => 'betterdocs_faq',
828 + 'post_status' => 'publish',
829 + 'tax_query' => [
830 + [
831 + 'taxonomy' => 'betterdocs_faq_category',
832 + 'field' => 'term_id',
833 + 'terms' => $term_id,
834 + 'operator' => 'AND'
835 + ]
836 + ],
837 + 'posts_per_page' => -1
838 + ];
897 839
898 - public function is_inner_templates() {
899 - if ( is_tax( 'knowledge_base' ) || is_tax( 'doc_category' ) || is_tax( 'doc_tag' ) || is_singular( 'docs' ) ) {
900 - return true;
901 - }
902 - return false;
903 - }
840 + $args['orderby'] = 'post__in';
841 + $args['post__in'] = $this->get_faq_orders( $term_id );
904 842
905 - /**
906 - * Summary of docs_query_args
907 - * @param mixed $args
908 - * @throws \Exception
909 - * @return mixed
910 - */
911 - public function docs_query_args( $args, $filter = array() ) {
912 - $_origin_args = $args;
843 + return new WP_Query( $args );
844 + }
913 845
914 - $default_args = array(
915 - 'post_type' => 'docs'
916 - );
846 + public function get_faq_orders( $term_id = null ) {
847 + global $wpdb;
848 + $faq_order = get_term_meta( $term_id, '_betterdocs_faq_order', true );
849 + $faq_order = explode( ',', $faq_order );
917 850
918 - if ( ! empty( $args[ 'post_type' ] ) && trim( $args[ 'post_type' ] ) === 'docs_any' ) {
919 - $default_args[ 'post_type' ] = 'docs';
920 - $default_args[ 'post_status' ] = 'any';
851 + $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}term_relationships WHERE term_taxonomy_id = %d", $term_id );
852 + $query_key = 'betterdocs_faq_order_' . md5( $query );
921 853
922 - unset( $args[ 'post_type' ] );
923 - }
854 + if ( ( $results = $this->database->get_cache( $query_key ) ) !== false ) {
855 + return $results;
856 + }
924 857
925 - /**
926 - * OrderBy and Order
927 - */
928 - if ( ! isset( $args[ 'orderby' ] ) ) {
929 - $_orderby = $this->settings->get( 'alphabetically_order_post' );
930 - $_order = $this->settings->get( 'docs_order', 'ASC' );
931 - } else {
932 - $_orderby = $args[ 'orderby' ];
933 - $_order = ! empty( $args[ 'order' ] ) ? $args[ 'order' ] : 'ASC';
934 - }
858 + if ( ! empty( $faq_order ) ) {
859 + $new_ids = [];
860 + $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
935 861
936 - if ( 'betterdocs_order' != $_orderby ) {
937 - if ( true === $_orderby ) {
938 - $args[ 'orderby' ] = 'title';
939 - } else {
940 - $args[ 'orderby' ] = $_orderby;
941 - }
862 + if ( ! is_null( $results ) && ! empty( $results ) && is_array( $results ) ) {
863 + $object_ids = array_filter(
864 + $results,
865 + function ( $value ) use ( $faq_order ) {
866 + return ! in_array( $value->object_id, $faq_order );
867 + }
868 + );
942 869
943 - $args[ 'order' ] = $_order;
944 - } elseif ( 'betterdocs_order' == $_orderby ) {
945 - unset( $args[ 'orderby' ] );
946 - }
870 + if ( ! empty( $object_ids ) ) {
871 + array_walk(
872 + $object_ids,
873 + function ( $value ) use ( &$new_ids ) {
874 + $new_ids[] = $value->object_id;
875 + }
876 + );
877 + }
878 + }
947 879
948 - if ( empty( $args[ 'orderby' ] ) ) {
949 - unset( $args[ 'order' ] );
950 - }
880 + $faq_order = array_merge( $new_ids, $faq_order );
881 + }
951 882
952 - /**
953 - * Term ID
954 - */
955 - $_term_id = null;
956 - if ( ! empty( $args[ 'term_id' ] ) ) {
957 - $_term_id = intval( $args[ 'term_id' ] );
958 - unset( $args[ 'term_id' ] );
959 - }
883 + $this->database->set_cache( $query_key, $faq_order, 1 );
960 884
961 - // if ( $_term_id == null ) {
962 - // throw new \Exception( __( '$args["term_id"] cannot be null.', 'betterdocs' ) );
963 - // }
885 + return $faq_order;
886 + }
964 887
965 - /**
966 - * Term Slug for tax_query
967 - */
968 - $_term_slug = '';
969 - if ( ! empty( $args[ 'term_slug' ] ) ) {
970 - $_term_slug = trim( $args[ 'term_slug' ] );
971 - unset( $args[ 'term_slug' ] );
972 - }
888 + public function get_faq_terms( $terms = [] ) {
889 + $_terms = get_terms(
890 + [
891 + 'taxonomy' => 'betterdocs_faq_category',
892 + 'hide_empty' => true,
893 + 'orderby' => 'name',
894 + 'order' => 'ASC',
895 + 'meta_query' => [
896 + [
897 + 'key' => 'status',
898 + 'value' => 1,
899 + 'compare' => '=='
900 + ]
901 + ]
902 + ]
903 + );
973 904
974 - /**
975 - * @todo old hook
976 - * hook: betterdocs_cat_template_multikb
977 - */
905 + if ( ! is_wp_error( $_terms ) ) {
906 + foreach ( $_terms as $term ) {
907 + $terms[ $term->term_id ] = $term->name;
908 + }
909 + }
978 910
979 - $_multiple_kb = apply_filters(
980 - 'betterdocs_enable_multiple_knowledge_base',
981 - isset( $args[ 'multiple_kb' ] ) ? (bool) $args[ 'multiple_kb' ] : false,
982 - $_origin_args
983 - );
911 + return $terms;
912 + }
984 913
985 - $_kb_slug = isset( $args[ 'kb_slug' ] ) ? trim( $args[ 'kb_slug' ] ) : '';
914 + public function get_doc_terms( $terms = [] ) {
915 + $_terms = get_terms(
916 + [
917 + 'taxonomy' => 'doc_category',
918 + 'hide_empty' => true,
919 + 'orderby' => 'name',
920 + 'order' => 'ASC',
921 + ]
922 + );
986 923
987 - unset( $args[ 'multiple_kb' ] );
988 - unset( $args[ 'kb_slug' ] );
924 + if ( ! is_wp_error( $_terms ) ) {
925 + foreach ( $_terms as $term ) {
926 + $terms[ $term->term_id ] = $term->name;
927 + }
928 + }
989 929
990 - $tax_query = array(
991 - array(
992 - 'taxonomy' => 'doc_category',
993 - 'field' => 'slug',
994 - 'terms' => $_term_slug,
995 - 'operator' => 'AND',
996 - 'include_children' => false
997 - )
998 - );
930 + return $terms;
931 + }
999 932
1000 - if ( ! isset( $args[ 'orderby' ] ) || 'betterdocs_order' == $args[ 'orderby' ] ) {
1001 - $args[ 'orderby' ] = 'post__in';
1002 - $args[ 'post__in' ] = $this->get_docs_order_by_terms( $_term_id );
1003 - }
1004 933
1005 - if ( isset( $args[ 'tax_query' ] ) ) {
1006 - $tax_query = $args[ 'tax_query' ];
1007 - }
934 + public function get_docs_count( $term, $nested_subcategory = false, $args = [] ) {
935 + $counts = isset( $term->count ) ? $term->count : 0;
1008 936
1009 - $args[ 'tax_query' ] = apply_filters(
1010 - 'betterdocs_docs_tax_query_args',
1011 - $tax_query,
1012 - $_multiple_kb,
1013 - $_term_slug,
1014 - $_kb_slug,
1015 - $_origin_args,
1016 - $this->is_inner_templates()
1017 - );
1018 - /**
1019 - * Final parse args
1020 - */
1021 - $args = wp_parse_args( $args, $default_args );
937 + if ( $nested_subcategory == false ) {
938 + return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args );
939 + }
1022 940
1023 - if ( ! empty( $filter ) ) {
1024 - $filter = array_flip( $filter );
1025 - $args = array_filter(
1026 - $args,
1027 - function ( $item ) use ( $filter ) {
1028 - return ! array_key_exists( $item, $filter );
1029 - },
1030 - ARRAY_FILTER_USE_KEY
1031 - );
1032 - }
941 + $_child_terms_docs_ids = $this->get_doc_ids_by_term( $term, null, $nested_subcategory );
942 + if ( is_array( $_child_terms_docs_ids ) ) {
943 + $counts = count( $_child_terms_docs_ids );
944 + }
1033 945
1034 - $final_args = apply_filters( 'betterdocs_articles_args', $args, $_term_id, $_origin_args );
946 + return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args );
947 + }
1035 948
1036 - // Process betterdocs_order AFTER all filters have run
1037 - if ( isset( $final_args[ 'orderby' ] ) && 'betterdocs_order' == $final_args[ 'orderby' ] ) {
1038 - $docs_order = $this->get_docs_order_by_terms( $_term_id );
1039 -
1040 - if ( ! empty( $docs_order ) ) {
1041 - $final_args[ 'orderby' ] = 'post__in';
1042 - $final_args[ 'post__in' ] = $docs_order;
1043 - } else {
1044 - $final_args[ 'orderby' ] = 'menu_order';
1045 - $final_args[ 'order' ] = 'ASC';
1046 - }
1047 - }
1048 -
1049 - return $final_args;
1050 - }
1051 -
1052 - /**
1053 - * The front-end FAQ ordering preference, set via the FAQ Builder header
1054 - * dropdown (the `.betterdocs-dropdown-select` control, saved to the
1055 - * `betterdocs_faq_order` option). The front end mirrors the builder so it
1056 - * shows FAQ groups (and the FAQs inside them) in the same order the admin
1057 - * sees while building.
1058 - *
1059 - * @return string One of: default, most_recent, least_recent, a_to_z, z_to_a, most_questions.
1060 - */
1061 - public function get_faq_order_key() {
1062 - $key = get_option( 'betterdocs_faq_order', 'default' );
1063 - $allowed = array( 'default', 'most_recent', 'least_recent', 'a_to_z', 'z_to_a', 'most_questions' );
1064 -
1065 - if ( ! in_array( $key, $allowed, true ) ) {
1066 - $key = 'default';
1067 - }
1068 -
1069 - return apply_filters( 'betterdocs_faq_order_key', $key );
1070 - }
1071 -
1072 - /**
1073 - * Translate the FAQ order preference into `get_terms()` order clauses for
1074 - * the FAQ group list. Mirrors the admin builder's ORDER_MAP so the front
1075 - * end matches what's configured there.
1076 - *
1077 - * @return array orderby/order (+ meta_key for the manual `default` order).
1078 - */
1079 - public function faq_terms_order_clause() {
1080 - switch ( $this->get_faq_order_key() ) {
1081 - case 'most_recent':
1082 - return array( 'orderby' => 'term_id', 'order' => 'DESC' );
1083 - case 'least_recent':
1084 - return array( 'orderby' => 'term_id', 'order' => 'ASC' );
1085 - case 'a_to_z':
1086 - return array( 'orderby' => 'name', 'order' => 'ASC' );
1087 - case 'z_to_a':
1088 - return array( 'orderby' => 'name', 'order' => 'DESC' );
1089 - case 'most_questions':
1090 - return array( 'orderby' => 'count', 'order' => 'DESC' );
1091 - case 'default':
1092 - default:
1093 - // Manual drag-drop order stored in the `order` term meta.
1094 - return array( 'meta_key' => 'order', 'orderby' => 'meta_value_num', 'order' => 'ASC' );
1095 - }
1096 - }
1097 -
1098 - public function faq_terms_query_args( $includes = '', $excludes = '', $args = array(), $taxonomy = 'betterdocs_faq_category' ) {
1099 - $_args = array(
1100 - 'taxonomy' => $taxonomy,
1101 - 'include' => $includes,
1102 - 'exclude' => $excludes,
1103 - 'meta_query' => array(
1104 - array(
1105 - 'key' => 'status',
1106 - 'value' => 1,
1107 - 'compare' => '=='
1108 - )
1109 - )
1110 - );
1111 -
1112 - $_args = array_merge( $_args, $this->faq_terms_order_clause() );
1113 -
1114 - if ( 'all' == $_args[ 'include' ] ) {
1115 - unset( $_args[ 'include' ] );
1116 - unset( $_args[ 'exclude' ] );
1117 - }
1118 -
1119 - if ( empty( $_args[ 'exclude' ] ) ) {
1120 - unset( $_args[ 'exclude' ] );
1121 - }
1122 -
1123 - if ( empty( $_args[ 'include' ] ) ) {
1124 - unset( $_args[ 'include' ] );
1125 - }
1126 -
1127 - return wp_parse_args( $args, $_args );
1128 - }
1129 -
1130 - public function get_faq_by_term( $term_id, $taxonomy = 'betterdocs_faq_category' ) {
1131 - global $wpdb;
1132 -
1133 - $args = array(
1134 - 'post_type' => 'betterdocs_faq',
1135 - 'post_status' => 'publish',
1136 - 'tax_query' => array(
1137 - array(
1138 - 'taxonomy' => $taxonomy,
1139 - 'field' => 'term_id',
1140 - 'terms' => $term_id,
1141 - 'operator' => 'AND'
1142 - )
1143 - ),
1144 - 'posts_per_page' => -1
1145 - );
1146 -
1147 - // Order the FAQs inside the group to mirror the FAQ Builder header
1148 - // dropdown. `default`/`most_questions` keep the manual drag-drop order
1149 - // (the others sort live, just like the admin builder does).
1150 - switch ( $this->get_faq_order_key() ) {
1151 - case 'most_recent':
1152 - $args[ 'orderby' ] = 'ID';
1153 - $args[ 'order' ] = 'DESC';
1154 - break;
1155 - case 'least_recent':
1156 - $args[ 'orderby' ] = 'ID';
1157 - $args[ 'order' ] = 'ASC';
1158 - break;
1159 - case 'a_to_z':
1160 - $args[ 'orderby' ] = 'title';
1161 - $args[ 'order' ] = 'ASC';
1162 - break;
1163 - case 'z_to_a':
1164 - $args[ 'orderby' ] = 'title';
1165 - $args[ 'order' ] = 'DESC';
1166 - break;
1167 - case 'default':
1168 - case 'most_questions':
1169 - default:
1170 - $args[ 'orderby' ] = 'post__in';
1171 - $args[ 'post__in' ] = $this->get_faq_orders( $term_id );
1172 - break;
1173 - }
1174 -
1175 - return new WP_Query( $args );
1176 - }
1177 -
1178 - public function get_faq_orders( $term_id = null ) {
1179 - global $wpdb;
1180 - $faq_order = get_term_meta( $term_id, '_betterdocs_faq_order', true );
1181 - $faq_order = explode( ',', $faq_order );
1182 -
1183 - $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}term_relationships WHERE term_taxonomy_id = %d", $term_id );
1184 - $query_key = 'betterdocs_faq_order_' . md5( $query );
1185 -
1186 - if ( ( $results = $this->database->get_cache( $query_key ) ) !== false ) {
1187 - return $results;
1188 - }
1189 -
1190 - if ( ! empty( $faq_order ) ) {
1191 - $new_ids = array();
1192 - // $query is prepared upstream; results are cached via $this->database->get_cache above (line 980).
1193 - $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1194 -
1195 - if ( ! is_null( $results ) && ! empty( $results ) && is_array( $results ) ) {
1196 - $object_ids = array_filter(
1197 - $results,
1198 - function ( $value ) use ( $faq_order ) {
1199 - return ! in_array( $value->object_id, $faq_order );
1200 - }
1201 - );
1202 -
1203 - if ( ! empty( $object_ids ) ) {
1204 - array_walk(
1205 - $object_ids,
1206 - function ( $value ) use ( &$new_ids ) {
1207 - $new_ids[ ] = $value->object_id;
1208 - }
1209 - );
1210 - }
1211 - }
1212 -
1213 - $faq_order = array_merge( $new_ids, $faq_order );
1214 - }
1215 -
1216 - $this->database->set_cache( $query_key, $faq_order, 1 );
1217 -
1218 - return $faq_order;
1219 - }
1220 -
1221 - public function get_faq_terms( $terms = array(), $taxonomy = 'betterdocs_faq_category' ) {
1222 - $_terms = get_terms(
1223 - array(
1224 - 'taxonomy' => $taxonomy,
1225 - 'hide_empty' => true,
1226 - 'orderby' => 'name',
1227 - 'order' => 'ASC',
1228 - 'meta_query' => array(
1229 - array(
1230 - 'key' => 'status',
1231 - 'value' => 1,
1232 - 'compare' => '=='
1233 - )
1234 - )
1235 - )
1236 - );
1237 -
1238 - if ( ! is_wp_error( $_terms ) ) {
1239 - foreach ( $_terms as $term ) {
1240 - $terms[ $term->term_id ] = $term->name;
1241 - }
1242 - }
1243 -
1244 - return $terms;
1245 - }
1246 -
1247 - public function get_doc_terms( $terms = array() ) {
1248 - $_terms = get_terms(
1249 - array(
1250 - 'taxonomy' => 'doc_category',
1251 - 'hide_empty' => true,
1252 - 'orderby' => 'name',
1253 - 'order' => 'ASC'
1254 - )
1255 - );
1256 -
1257 - if ( ! is_wp_error( $_terms ) ) {
1258 - foreach ( $_terms as $term ) {
1259 - $terms[ $term->term_id ] = $term->name;
1260 - }
1261 - }
1262 -
1263 - return $terms;
1264 - }
1265 -
1266 - public function flush_term_counts_cache() {
1267 - $this->database->bump_cache_version( 'betterdocs_term_counts' );
1268 - }
1269 -
1270 - public function flush_term_counts_cache_on_post( $post_id, $post = null ) {
1271 - if ( $post && isset( $post->post_type ) && $post->post_type === 'docs' ) {
1272 - $this->flush_term_counts_cache();
1273 - }
1274 - }
1275 -
1276 - public function flush_term_counts_cache_on_set( $object_id, $terms, $tt_ids, $taxonomy ) {
1277 - if ( $taxonomy === 'doc_category' ) {
1278 - $this->flush_term_counts_cache();
1279 - }
1280 - }
1281 -
1282 - public function get_docs_count( $term, $nested_subcategory = false, $args = array() ) {
1283 - // Validate term object
1284 - if ( ! is_object( $term ) ) {
1285 - return 0;
1286 - }
1287 -
1288 - $counts = isset( $term->count ) ? $term->count : 0;
1289 -
1290 - if ( ! isset( $term->term_id ) || ! is_numeric( $term->term_id ) ) {
1291 - return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args );
1292 - }
1293 -
1294 - $version = $this->database->get_cache_version( 'betterdocs_term_counts' );
1295 - // Counts depend on who is asking: an author sees their own private docs, so the
1296 - // key carries the user id. Logged-out visitors all share user 0.
1297 - $viewer = $this->count_cache_viewer_key();
1298 - $kb_slug = isset( $args['kb_slug'] ) ? $args['kb_slug'] : '';
1299 - $multi = ! empty( $args['multiple_knowledge_base'] ) ? 1 : 0;
1300 - $nested = $nested_subcategory ? 1 : 0;
1301 - $cache_key = "bd_term_counts_v{$version}_docs_count_{$term->term_id}_{$nested}_{$viewer}_{$kb_slug}_{$multi}";
1302 -
1303 - $cached = wp_cache_get( $cache_key, 'betterdocs' );
1304 - if ( false !== $cached ) {
1305 - return apply_filters( 'betterdocs_docs_count', $cached, $term, $nested_subcategory, $args );
1306 - }
1307 -
1308 - if ( false == $nested_subcategory ) {
1309 - // For non-nested categories, we need to recalculate counts based on user capabilities
1310 - // Only proceed if we have a valid term with required properties
1311 - if ( isset( $term->taxonomy ) ) {
1312 - // Get all post IDs for this term
1313 - $post_ids = get_objects_in_term( $term->term_id, $term->taxonomy );
1314 -
1315 - if ( ! empty( $post_ids ) ) {
1316 - _prime_post_caches( $post_ids, false, false );
1317 -
1318 - // Public docs for everyone, plus any private doc this user may read
1319 - // (its own author, or a `read_private_docs` holder).
1320 - $filtered_post_ids = array_filter( $post_ids, function ( $post_id ) {
1321 - return $this->can_read_doc( $post_id );
1322 - } );
1323 -
1324 - $counts = count( $filtered_post_ids );
1325 - } else {
1326 - $counts = 0;
1327 - }
1328 - }
1329 - } else {
1330 - $_child_terms_docs_ids = $this->get_doc_ids_by_term( $term, null, $nested_subcategory );
1331 - if ( is_array( $_child_terms_docs_ids ) ) {
1332 - $counts = count( $_child_terms_docs_ids );
1333 - }
1334 - }
1335 -
1336 - wp_cache_set( $cache_key, $counts, 'betterdocs', HOUR_IN_SECONDS * 6 );
1337 -
1338 - return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args );
1339 - }
1340 -
1341 - public function get_doc_ids_by_term( $term, $optional = null, $nested_subcategory = false ) {
1342 - // Check if term has required properties and is a valid object
1343 - if ( ! is_object( $term ) || ! isset( $term->term_id ) || ! isset( $term->taxonomy ) || ! is_numeric( $term->term_id ) ) {
1344 - return false;
1345 - }
1346 -
1347 - $version = $this->database->get_cache_version( 'betterdocs_term_counts' );
1348 - $viewer = $this->count_cache_viewer_key();
1349 - $nested = $nested_subcategory ? 1 : 0;
1350 - $optional_id = is_object( $optional ) && isset( $optional->term_id ) ? (int) $optional->term_id : 0;
1351 - $cache_key = "bd_term_counts_v{$version}_doc_ids_{$term->term_id}_{$nested}_{$optional_id}_{$viewer}";
1352 -
1353 - $cached = wp_cache_get( $cache_key, 'betterdocs' );
1354 - if ( false !== $cached ) {
1355 - return $cached;
1356 - }
1357 -
1358 - $args = array(
1359 - 'taxonomy' => $term->taxonomy,
1360 - 'include' => $term->term_id,
1361 - );
949 + public function get_doc_ids_by_term( $term, $optional = null, $nested_subcategory = false ) {
950 + $args = ['include' => $term->term_id];
1362 951 if ( $nested_subcategory ) {
1363 - $args[ 'child_of' ] = $term->term_id;
1364 - unset( $args[ 'include' ] );
952 + $args['child_of'] = $term->term_id;
953 + unset( $args['include'] );
1365 954 }
1366 - $_child_terms = get_terms( $args );
955 + $_child_terms = get_terms( $term->taxonomy, $args );
1367 956
1368 957 if ( ! is_array( $_child_terms ) ) {
1369 958 return false;
1370 959 }
@@ -1374,261 +963,252 @@
1374 963 $_child_terms_ids = array_column( $_child_terms, 'term_id' );
1375 964 $_child_terms_taxs = array_column( $_child_terms, 'taxonomy' );
1376 965 $_child_terms_docs_ids = get_objects_in_term( $_child_terms_ids, $_child_terms_taxs );
1377 966
1378 - if ( null !== $optional ) {
967 + if ( $optional !== null ) {
1379 968 $_optional_doc_ids = get_objects_in_term( $optional->term_id, $optional->taxonomy );
1380 969 $_child_terms_docs_ids = array_intersect( $_child_terms_docs_ids, $_optional_doc_ids );
1381 970 }
1382 971
1383 - if ( ! empty( $_child_terms_docs_ids ) ) {
1384 - _prime_post_caches( $_child_terms_docs_ids, false, false );
1385 - }
972 + return array_filter( $_child_terms_docs_ids, function ( $doc_id ) {
973 + if ( ! is_user_logged_in() ) {
974 + return is_post_publicly_viewable( $doc_id );
975 + }
1386 976
1387 - $filtered = array_filter( $_child_terms_docs_ids, function ( $doc_id ) {
1388 - return $this->can_read_doc( $doc_id );
977 + $_status = get_post_status( $doc_id );
978 + return $_status == 'private' || is_post_publicly_viewable( $doc_id );
1389 979 } );
980 + }
1390 981
1391 - wp_cache_set( $cache_key, $filtered, 'betterdocs', HOUR_IN_SECONDS * 6 );
982 + /**
983 + * Get the common query arguments for WP_Query.
984 + *
985 + * @param string $terms The taxonomy.
986 + * @param string $term_slug The taxonomy term slug.
987 + * @param array $additional_args Additional arguments to merge with the common arguments.
988 + * @return array The query arguments.
989 + */
990 + private function tax_query_args( $terms, $term_slug, $additional_args = array() ) {
991 + $common_args = array(
992 + 'post_type' => 'docs',
993 + 'post_status' => 'publish',
994 + 'tax_query' => array(
995 + array(
996 + 'taxonomy' => $terms,
997 + 'field' => 'slug',
998 + 'terms' => $term_slug,
999 + ),
1000 + ),
1001 + );
1002 + return array_merge( $common_args, $additional_args );
1003 + }
1392 1004
1393 - return $filtered;
1394 - }
1005 + /**
1006 + * Get the latest updated date for a specific taxonomy term.
1007 + *
1008 + * @param string $terms The taxonomy.
1009 + * @param string $term_slug The taxonomy term slug.
1010 + * @return string|null The latest modified date or null if no posts found.
1011 + */
1012 + public function latest_updated_date( $terms, $term_slug ) {
1013 + $args = $this->tax_query_args(
1014 + $terms,
1015 + $term_slug,
1016 + array(
1017 + 'posts_per_page' => 1,
1018 + 'orderby' => 'modified',
1019 + 'order' => 'DESC',
1020 + )
1021 + );
1395 1022
1396 - /**
1397 - * Get the common query arguments for WP_Query.
1398 - *
1399 - * @param string $terms The taxonomy.
1400 - * @param string $term_slug The taxonomy term slug.
1401 - * @param array $additional_args Additional arguments to merge with the common arguments.
1402 - * @return array The query arguments.
1403 - */
1404 - private function tax_query_args( $terms, $term_slug, $additional_args = array() ) {
1405 - $common_args = array(
1406 - 'post_type' => 'docs',
1407 - 'post_status' => 'publish',
1408 - 'tax_query' => array(
1409 - array(
1410 - 'taxonomy' => $terms,
1411 - 'field' => 'slug',
1412 - 'terms' => $term_slug
1413 - )
1414 - )
1415 - );
1416 - return array_merge( $common_args, $additional_args );
1417 - }
1023 + $query = new WP_Query( $args );
1418 1024
1419 - /**
1420 - * Get the latest updated date for a specific taxonomy term.
1421 - *
1422 - * @param string $terms The taxonomy.
1423 - * @param string $term_slug The taxonomy term slug.
1424 - * @return string|null The latest modified date or null if no posts found.
1425 - */
1426 - public function latest_updated_date( $terms, $term_slug ) {
1427 - $args = $this->tax_query_args(
1428 - $terms,
1429 - $term_slug,
1430 - array(
1431 - 'posts_per_page' => 1,
1432 - 'orderby' => 'modified',
1433 - 'order' => 'DESC'
1434 - )
1435 - );
1025 + if ( $query->have_posts() ) {
1026 + while ( $query->have_posts() ) {
1027 + $query->the_post();
1028 + $latest_post = get_the_modified_date();
1029 + wp_reset_postdata();
1030 + return $latest_post;
1031 + }
1032 + } else {
1033 + return null;
1034 + }
1035 + }
1436 1036
1437 - $query = new WP_Query( $args );
1037 + /**
1038 + * Check if there are any new posts within the last 7 days for a specific taxonomy term.
1039 + *
1040 + * @param string $terms The taxonomy.
1041 + * @param string $term_slug The taxonomy term slug.
1042 + * @return bool True if there are new posts, false otherwise.
1043 + */
1044 + public function check_new_posts( $terms, $term_slug ) {
1045 + $date_7_days_ago = date( 'Y-m-d H:i:s', strtotime( '-7 days' ) );
1438 1046
1439 - if ( $query->have_posts() ) {
1440 - while ( $query->have_posts() ) {
1441 - $query->the_post();
1442 - $latest_post = get_the_modified_date();
1443 - wp_reset_postdata();
1444 - return $latest_post;
1445 - }
1446 - } else {
1447 - return null;
1448 - }
1449 - }
1047 + $args = $this->tax_query_args(
1048 + $terms,
1049 + $term_slug,
1050 + array(
1051 + 'posts_per_page' => 1,
1052 + 'orderby' => 'modified',
1053 + 'order' => 'DESC',
1054 + 'date_query' => array(
1055 + array(
1056 + 'after' => $date_7_days_ago,
1057 + 'inclusive' => true,
1058 + ),
1059 + ),
1060 + )
1061 + );
1450 1062
1451 - /**
1452 - * Check if there are any new posts within the last 7 days for a specific taxonomy term.
1453 - *
1454 - * @param string $terms The taxonomy.
1455 - * @param string $term_slug The taxonomy term slug.
1456 - * @return bool True if there are new posts, false otherwise.
1457 - */
1458 - public function check_new_posts( $terms, $term_slug ) {
1459 - $date_7_days_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-7 days' ) );
1063 + $query = new WP_Query( $args );
1460 1064
1461 - $args = $this->tax_query_args(
1462 - $terms,
1463 - $term_slug,
1464 - array(
1465 - 'posts_per_page' => 1,
1466 - 'orderby' => 'modified',
1467 - 'order' => 'DESC',
1468 - 'date_query' => array(
1469 - array(
1470 - 'after' => $date_7_days_ago,
1471 - 'inclusive' => true
1472 - )
1473 - )
1474 - )
1475 - );
1065 + $has_new_posts = $query->have_posts();
1476 1066
1477 - $query = new WP_Query( $args );
1067 + wp_reset_postdata();
1478 1068
1479 - $has_new_posts = $query->have_posts();
1069 + return $has_new_posts;
1070 + }
1480 1071
1481 - wp_reset_postdata();
1072 + public function insert_search_keyword( $search_input, $input_not_found ) {
1073 + if ( empty( $search_input ) ) {
1074 + return false;
1075 + }
1482 1076
1483 - return $has_new_posts;
1484 - }
1485 -
1486 - public function insert_search_keyword( $search_input, $input_not_found ) {
1487 - if ( empty( $search_input ) ) {
1488 - return false;
1489 - }
1490 -
1491 - global $wpdb;
1492 -
1493 - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live search-keyword analytics; cache would defeat the purpose.
1494 - // Use BINARY comparison to avoid collation mismatch errors
1495 - // This works across all character sets (latin1, utf8, utf8mb4, etc.)
1496 - $search = $wpdb->get_results(
1497 - $wpdb->prepare(
1498 - "SELECT *
1077 + global $wpdb;
1078 + $search = $wpdb->get_results(
1079 + $wpdb->prepare(
1080 + "SELECT *
1499 1081 FROM {$wpdb->prefix}betterdocs_search_keyword
1500 - WHERE BINARY keyword = %s",
1501 - $search_input
1502 - )
1503 - );
1082 + WHERE keyword = %s",
1083 + $search_input
1084 + )
1085 + );
1504 1086
1505 - if ( ! empty( $search ) ) {
1506 - $search_log = $wpdb->get_results(
1507 - $wpdb->prepare(
1508 - "SELECT *
1087 + if ( ! empty( $search ) ) {
1088 + $search_log = $wpdb->get_results(
1089 + $wpdb->prepare(
1090 + "SELECT *
1509 1091 FROM {$wpdb->prefix}betterdocs_search_log
1510 1092 WHERE created_at = %s AND keyword_id = %d",
1511 - gmdate( 'Y-m-d' ),
1512 - $search[ 0 ]->id
1513 - )
1514 - );
1093 + date( 'Y-m-d' ),
1094 + $search[0]->id
1095 + )
1096 + );
1515 1097
1516 - if ( ! empty( $search_log ) ) {
1517 - if ( ! empty( $input_not_found ) ) {
1518 - $tbl_field = 'not_found_count';
1519 - $count = $search_log[ 0 ]->not_found_count + 1;
1520 - } else {
1521 - $tbl_field = 'count';
1522 - $count = $search_log[ 0 ]->count + 1;
1523 - }
1524 - // $tbl_field is validated immediately above to be either 'count' or
1525 - // 'not_found_count' — safe to interpolate as a column identifier.
1526 - // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $tbl_field is an allowlisted column name ('count'|'not_found_count'); safe to interpolate.
1527 - $insert = $wpdb->query(
1528 - $wpdb->prepare(
1529 - "UPDATE {$wpdb->prefix}betterdocs_search_log SET {$tbl_field} = %d WHERE created_at = %s AND keyword_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1530 - $count,
1531 - $search_log[ 0 ]->created_at,
1532 - $search_log[ 0 ]->keyword_id
1533 - )
1534 - );
1535 - } else {
1536 - if ( ! empty( $input_not_found ) ) {
1537 - $count = 0;
1538 - $not_found_count = 1;
1539 - } else {
1540 - $count = 1;
1541 - $not_found_count = 0;
1542 - }
1543 - $insert = $wpdb->query(
1544 - $wpdb->prepare(
1545 - "INSERT INTO {$wpdb->prefix}betterdocs_search_log
1098 + if ( ! empty( $search_log ) ) {
1099 + if ( ! empty( $input_not_found ) ) {
1100 + $tbl_field = 'not_found_count';
1101 + $count = $search_log[0]->not_found_count + 1;
1102 + } else {
1103 + $tbl_field = 'count';
1104 + $count = $search_log[0]->count + 1;
1105 + }
1106 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1107 + $insert = $wpdb->query(
1108 + $wpdb->prepare(
1109 + "UPDATE {$wpdb->prefix}betterdocs_search_log
1110 + SET " . $tbl_field . ' = ' . $count . '
1111 + WHERE created_at = %s AND keyword_id = %d',
1112 + $search_log[0]->created_at,
1113 + $search_log[0]->keyword_id
1114 + )
1115 + );
1116 + } else {
1117 + if ( ! empty( $input_not_found ) ) {
1118 + $count = 0;
1119 + $not_found_count = 1;
1120 + } else {
1121 + $count = 1;
1122 + $not_found_count = 0;
1123 + }
1124 + $insert = $wpdb->query(
1125 + $wpdb->prepare(
1126 + "INSERT INTO {$wpdb->prefix}betterdocs_search_log
1546 1127 ( keyword_id, count, not_found_count, created_at )
1547 1128 VALUES ( %d, %d, %d, %s )",
1548 - array(
1549 - $search[ 0 ]->id,
1550 - $count,
1551 - $not_found_count,
1552 - gmdate( 'Y-m-d' )
1553 - )
1554 - )
1555 - );
1556 - }
1557 - } else {
1558 - $insert = $wpdb->query(
1559 - $wpdb->prepare(
1560 - "INSERT INTO {$wpdb->prefix}betterdocs_search_keyword
1129 + [
1130 + $search[0]->id,
1131 + $count,
1132 + $not_found_count,
1133 + date( 'Y-m-d' )
1134 + ]
1135 + )
1136 + );
1137 + }
1138 + } else {
1139 + $insert = $wpdb->query(
1140 + $wpdb->prepare(
1141 + "INSERT INTO {$wpdb->prefix}betterdocs_search_keyword
1561 1142 ( keyword )
1562 1143 VALUES ( %s )",
1563 - array(
1564 - $search_input
1565 - )
1566 - )
1567 - );
1144 + [
1145 + $search_input
1146 + ]
1147 + )
1148 + );
1568 1149
1569 - if ( $insert ) {
1570 - if ( ! empty( $input_not_found ) ) {
1571 - $count = 0;
1572 - $not_found_count = 1;
1573 - } else {
1574 - $count = 1;
1575 - $not_found_count = 0;
1576 - }
1577 - $insert = $wpdb->query(
1578 - $wpdb->prepare(
1579 - "INSERT INTO {$wpdb->prefix}betterdocs_search_log
1150 + if ( $insert ) {
1151 + if ( ! empty( $input_not_found ) ) {
1152 + $count = 0;
1153 + $not_found_count = 1;
1154 + } else {
1155 + $count = 1;
1156 + $not_found_count = 0;
1157 + }
1158 + $insert = $wpdb->query(
1159 + $wpdb->prepare(
1160 + "INSERT INTO {$wpdb->prefix}betterdocs_search_log
1580 1161 ( keyword_id, count, not_found_count, created_at )
1581 1162 VALUES ( %d, %d, %d, %s )",
1582 - array(
1583 - $wpdb->insert_id,
1584 - $count,
1585 - $not_found_count,
1586 - gmdate( 'Y-m-d' )
1587 - )
1588 - )
1589 - );
1590 - }
1591 - }
1592 - // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1593 - return $insert;
1594 - }
1163 + [
1164 + $wpdb->insert_id,
1165 + $count,
1166 + $not_found_count,
1167 + date( 'Y-m-d' )
1168 + ]
1169 + )
1170 + );
1171 + }
1172 + }
1173 + return $insert;
1174 + }
1595 1175
1596 - /**
1597 - * Counts the number of 'doc_category' terms assigned to a specific 'knowledge_base' by slug.
1598 - *
1599 - * This method retrieves all terms in the 'doc_category' taxonomy and checks the term meta
1600 - * 'doc_category_knowledge_base' to determine how many 'doc_category' terms are associated
1601 - * with a given 'knowledge_base' slug.
1602 - *
1603 - * @param string $knowledge_base_slug The slug of the 'knowledge_base' to count the assignments for.
1604 - *
1605 - * @return int The count of 'doc_category' terms assigned to the specified 'knowledge_base'.
1606 - */
1607 - public function count_doc_categories_for_knowledge_base( $knowledge_base_slug ) {
1608 - $count = 0;
1176 + /**
1177 + * Counts the number of 'doc_category' terms assigned to a specific 'knowledge_base' by slug.
1178 + *
1179 + * This method retrieves all terms in the 'doc_category' taxonomy and checks the term meta
1180 + * 'doc_category_knowledge_base' to determine how many 'doc_category' terms are associated
1181 + * with a given 'knowledge_base' slug.
1182 + *
1183 + * @param string $knowledge_base_slug The slug of the 'knowledge_base' to count the assignments for.
1184 + *
1185 + * @return int The count of 'doc_category' terms assigned to the specified 'knowledge_base'.
1186 + */
1187 + public function count_doc_categories_for_knowledge_base( $knowledge_base_slug ) {
1188 + $count = 0;
1609 1189
1610 - $doc_categories = get_terms(
1611 - array(
1612 - 'taxonomy' => 'doc_category',
1613 - 'hide_empty' => true
1614 - )
1615 - );
1190 + $doc_categories = get_terms(
1191 + array(
1192 + 'taxonomy' => 'doc_category',
1193 + 'hide_empty' => true,
1194 + )
1195 + );
1616 1196
1617 - if ( ! empty( $doc_categories ) && ! is_wp_error( $doc_categories ) ) {
1618 - foreach ( $doc_categories as $term ) {
1619 - $meta_value = get_term_meta( $term->term_id, 'doc_category_knowledge_base', true );
1197 + if ( ! empty( $doc_categories ) && ! is_wp_error( $doc_categories ) ) {
1198 + foreach ( $doc_categories as $term ) {
1199 + $meta_value = get_term_meta( $term->term_id, 'doc_category_knowledge_base', true );
1620 1200
1621 - if ( $meta_value ) {
1622 - $knowledge_bases = maybe_unserialize( $meta_value );
1201 + if ( $meta_value ) {
1202 + $knowledge_bases = maybe_unserialize( $meta_value );
1623 1203
1624 - // Check if the specific knowledge base slug is present in the meta value array
1625 - if ( is_array( $knowledge_bases ) && in_array( $knowledge_base_slug, $knowledge_bases ) ) {
1626 - ++$count;
1627 - }
1628 - }
1629 - }
1630 - }
1204 + // Check if the specific knowledge base slug is present in the meta value array
1205 + if ( is_array( $knowledge_bases ) && in_array( $knowledge_base_slug, $knowledge_bases ) ) {
1206 + ++$count;
1207 + }
1208 + }
1209 + }
1210 + }
1631 1211
1632 - return $count;
1633 - }
1212 + return $count;
1213 + }
1634 1214 }