PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.7.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.7.0
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
betterdocs / includes / Core / Query.php

Query.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.7.0, at includes/Core/Query.php

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