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