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

1,387 lines 50.2 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 public function faq_terms_query_args( $includes = '', $excludes = '', $args = array() ) {
918 $_args = array(
919 'taxonomy' => 'betterdocs_faq_category',
920 'meta_key' => 'order',
921 'orderby' => 'meta_value_num',
922 'order' => 'ASC',
923 'include' => $includes,
924 'exclude' => $excludes,
925 'meta_query' => array(
926 array(
927 'key' => 'status',
928 'value' => 1,
929 'compare' => '=='
930 )
931 )
932 );
933
934 if ( 'all' == $_args[ 'include' ] ) {
935 unset( $_args[ 'include' ] );
936 unset( $_args[ 'exclude' ] );
937 }
938
939 if ( empty( $_args[ 'exclude' ] ) ) {
940 unset( $_args[ 'exclude' ] );
941 }
942
943 if ( empty( $_args[ 'include' ] ) ) {
944 unset( $_args[ 'include' ] );
945 }
946
947 return wp_parse_args( $args, $_args );
948 }
949
950 public function get_faq_by_term( $term_id ) {
951 global $wpdb;
952
953 $args = array(
954 'post_type' => 'betterdocs_faq',
955 'post_status' => 'publish',
956 'tax_query' => array(
957 array(
958 'taxonomy' => 'betterdocs_faq_category',
959 'field' => 'term_id',
960 'terms' => $term_id,
961 'operator' => 'AND'
962 )
963 ),
964 'posts_per_page' => -1
965 );
966
967 $args[ 'orderby' ] = 'post__in';
968 $args[ 'post__in' ] = $this->get_faq_orders( $term_id );
969
970 return new WP_Query( $args );
971 }
972
973 public function get_faq_orders( $term_id = null ) {
974 global $wpdb;
975 $faq_order = get_term_meta( $term_id, '_betterdocs_faq_order', true );
976 $faq_order = explode( ',', $faq_order );
977
978 $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}term_relationships WHERE term_taxonomy_id = %d", $term_id );
979 $query_key = 'betterdocs_faq_order_' . md5( $query );
980
981 if ( ( $results = $this->database->get_cache( $query_key ) ) !== false ) {
982 return $results;
983 }
984
985 if ( ! empty( $faq_order ) ) {
986 $new_ids = array();
987 // $query is prepared upstream; results are cached via $this->database->get_cache above (line 980).
988 $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
989
990 if ( ! is_null( $results ) && ! empty( $results ) && is_array( $results ) ) {
991 $object_ids = array_filter(
992 $results,
993 function ( $value ) use ( $faq_order ) {
994 return ! in_array( $value->object_id, $faq_order );
995 }
996 );
997
998 if ( ! empty( $object_ids ) ) {
999 array_walk(
1000 $object_ids,
1001 function ( $value ) use ( &$new_ids ) {
1002 $new_ids[ ] = $value->object_id;
1003 }
1004 );
1005 }
1006 }
1007
1008 $faq_order = array_merge( $new_ids, $faq_order );
1009 }
1010
1011 $this->database->set_cache( $query_key, $faq_order, 1 );
1012
1013 return $faq_order;
1014 }
1015
1016 public function get_faq_terms( $terms = array() ) {
1017 $_terms = get_terms(
1018 array(
1019 'taxonomy' => 'betterdocs_faq_category',
1020 'hide_empty' => true,
1021 'orderby' => 'name',
1022 'order' => 'ASC',
1023 'meta_query' => array(
1024 array(
1025 'key' => 'status',
1026 'value' => 1,
1027 'compare' => '=='
1028 )
1029 )
1030 )
1031 );
1032
1033 if ( ! is_wp_error( $_terms ) ) {
1034 foreach ( $_terms as $term ) {
1035 $terms[ $term->term_id ] = $term->name;
1036 }
1037 }
1038
1039 return $terms;
1040 }
1041
1042 public function get_doc_terms( $terms = array() ) {
1043 $_terms = get_terms(
1044 array(
1045 'taxonomy' => 'doc_category',
1046 'hide_empty' => true,
1047 'orderby' => 'name',
1048 'order' => 'ASC'
1049 )
1050 );
1051
1052 if ( ! is_wp_error( $_terms ) ) {
1053 foreach ( $_terms as $term ) {
1054 $terms[ $term->term_id ] = $term->name;
1055 }
1056 }
1057
1058 return $terms;
1059 }
1060
1061 public function get_docs_count( $term, $nested_subcategory = false, $args = array() ) {
1062 // Validate term object
1063 if ( ! is_object( $term ) ) {
1064 return 0;
1065 }
1066
1067 $counts = isset( $term->count ) ? $term->count : 0;
1068
1069 if ( false == $nested_subcategory ) {
1070 // For non-nested categories, we need to recalculate counts based on user capabilities
1071 // Only proceed if we have a valid term with required properties
1072 if ( isset( $term->term_id ) && isset( $term->taxonomy ) && is_numeric( $term->term_id ) ) {
1073 // Get all post IDs for this term
1074 $post_ids = get_objects_in_term( $term->term_id, $term->taxonomy );
1075
1076 if ( ! empty( $post_ids ) ) {
1077 if ( current_user_can( 'read_private_docs' ) ) {
1078 // For users with read_private_docs capability, include both private and public posts
1079 $filtered_post_ids = array_filter( $post_ids, function ( $post_id ) {
1080 $post_status = get_post_status( $post_id );
1081 return 'private' === $post_status || is_post_publicly_viewable( $post_id );
1082 } );
1083 } else {
1084 // For users without read_private_docs capability, only include public posts
1085 $filtered_post_ids = array_filter( $post_ids, function ( $post_id ) {
1086 return is_post_publicly_viewable( $post_id );
1087 } );
1088 }
1089
1090 $counts = count( $filtered_post_ids );
1091 } else {
1092 $counts = 0;
1093 }
1094 }
1095
1096 return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args );
1097 }
1098
1099 $_child_terms_docs_ids = $this->get_doc_ids_by_term( $term, null, $nested_subcategory );
1100 if ( is_array( $_child_terms_docs_ids ) ) {
1101 $counts = count( $_child_terms_docs_ids );
1102 }
1103
1104 return apply_filters( 'betterdocs_docs_count', $counts, $term, $nested_subcategory, $args );
1105 }
1106
1107 public function get_doc_ids_by_term( $term, $optional = null, $nested_subcategory = false ) {
1108 // Check if term has required properties and is a valid object
1109 if ( ! is_object( $term ) || ! isset( $term->term_id ) || ! isset( $term->taxonomy ) || ! is_numeric( $term->term_id ) ) {
1110 return false;
1111 }
1112
1113 $args = array(
1114 'taxonomy' => $term->taxonomy,
1115 'include' => $term->term_id,
1116 );
1117 if ( $nested_subcategory ) {
1118 $args[ 'child_of' ] = $term->term_id;
1119 unset( $args[ 'include' ] );
1120 }
1121 $_child_terms = get_terms( $args );
1122
1123 if ( ! is_array( $_child_terms ) ) {
1124 return false;
1125 }
1126
1127 array_unshift( $_child_terms, $term );
1128
1129 $_child_terms_ids = array_column( $_child_terms, 'term_id' );
1130 $_child_terms_taxs = array_column( $_child_terms, 'taxonomy' );
1131 $_child_terms_docs_ids = get_objects_in_term( $_child_terms_ids, $_child_terms_taxs );
1132
1133 if ( null !== $optional ) {
1134 $_optional_doc_ids = get_objects_in_term( $optional->term_id, $optional->taxonomy );
1135 $_child_terms_docs_ids = array_intersect( $_child_terms_docs_ids, $_optional_doc_ids );
1136 }
1137
1138 return array_filter( $_child_terms_docs_ids, function ( $doc_id ) {
1139 if ( ! current_user_can( 'read_private_docs' ) ) {
1140 return is_post_publicly_viewable( $doc_id );
1141 }
1142
1143 $_status = get_post_status( $doc_id );
1144 return 'private' == $_status || is_post_publicly_viewable( $doc_id );
1145 } );
1146 }
1147
1148 /**
1149 * Get the common query arguments for WP_Query.
1150 *
1151 * @param string $terms The taxonomy.
1152 * @param string $term_slug The taxonomy term slug.
1153 * @param array $additional_args Additional arguments to merge with the common arguments.
1154 * @return array The query arguments.
1155 */
1156 private function tax_query_args( $terms, $term_slug, $additional_args = array() ) {
1157 $common_args = array(
1158 'post_type' => 'docs',
1159 'post_status' => 'publish',
1160 'tax_query' => array(
1161 array(
1162 'taxonomy' => $terms,
1163 'field' => 'slug',
1164 'terms' => $term_slug
1165 )
1166 )
1167 );
1168 return array_merge( $common_args, $additional_args );
1169 }
1170
1171 /**
1172 * Get the latest updated date for a specific taxonomy term.
1173 *
1174 * @param string $terms The taxonomy.
1175 * @param string $term_slug The taxonomy term slug.
1176 * @return string|null The latest modified date or null if no posts found.
1177 */
1178 public function latest_updated_date( $terms, $term_slug ) {
1179 $args = $this->tax_query_args(
1180 $terms,
1181 $term_slug,
1182 array(
1183 'posts_per_page' => 1,
1184 'orderby' => 'modified',
1185 'order' => 'DESC'
1186 )
1187 );
1188
1189 $query = new WP_Query( $args );
1190
1191 if ( $query->have_posts() ) {
1192 while ( $query->have_posts() ) {
1193 $query->the_post();
1194 $latest_post = get_the_modified_date();
1195 wp_reset_postdata();
1196 return $latest_post;
1197 }
1198 } else {
1199 return null;
1200 }
1201 }
1202
1203 /**
1204 * Check if there are any new posts within the last 7 days for a specific taxonomy term.
1205 *
1206 * @param string $terms The taxonomy.
1207 * @param string $term_slug The taxonomy term slug.
1208 * @return bool True if there are new posts, false otherwise.
1209 */
1210 public function check_new_posts( $terms, $term_slug ) {
1211 $date_7_days_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-7 days' ) );
1212
1213 $args = $this->tax_query_args(
1214 $terms,
1215 $term_slug,
1216 array(
1217 'posts_per_page' => 1,
1218 'orderby' => 'modified',
1219 'order' => 'DESC',
1220 'date_query' => array(
1221 array(
1222 'after' => $date_7_days_ago,
1223 'inclusive' => true
1224 )
1225 )
1226 )
1227 );
1228
1229 $query = new WP_Query( $args );
1230
1231 $has_new_posts = $query->have_posts();
1232
1233 wp_reset_postdata();
1234
1235 return $has_new_posts;
1236 }
1237
1238 public function insert_search_keyword( $search_input, $input_not_found ) {
1239 if ( empty( $search_input ) ) {
1240 return false;
1241 }
1242
1243 global $wpdb;
1244
1245 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live search-keyword analytics; cache would defeat the purpose.
1246 // Use BINARY comparison to avoid collation mismatch errors
1247 // This works across all character sets (latin1, utf8, utf8mb4, etc.)
1248 $search = $wpdb->get_results(
1249 $wpdb->prepare(
1250 "SELECT *
1251 FROM {$wpdb->prefix}betterdocs_search_keyword
1252 WHERE BINARY keyword = %s",
1253 $search_input
1254 )
1255 );
1256
1257 if ( ! empty( $search ) ) {
1258 $search_log = $wpdb->get_results(
1259 $wpdb->prepare(
1260 "SELECT *
1261 FROM {$wpdb->prefix}betterdocs_search_log
1262 WHERE created_at = %s AND keyword_id = %d",
1263 gmdate( 'Y-m-d' ),
1264 $search[ 0 ]->id
1265 )
1266 );
1267
1268 if ( ! empty( $search_log ) ) {
1269 if ( ! empty( $input_not_found ) ) {
1270 $tbl_field = 'not_found_count';
1271 $count = $search_log[ 0 ]->not_found_count + 1;
1272 } else {
1273 $tbl_field = 'count';
1274 $count = $search_log[ 0 ]->count + 1;
1275 }
1276 // $tbl_field is validated immediately above to be either 'count' or
1277 // 'not_found_count' — safe to interpolate as a column identifier.
1278 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $tbl_field is an allowlisted column name ('count'|'not_found_count'); safe to interpolate.
1279 $insert = $wpdb->query(
1280 $wpdb->prepare(
1281 "UPDATE {$wpdb->prefix}betterdocs_search_log SET {$tbl_field} = %d WHERE created_at = %s AND keyword_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1282 $count,
1283 $search_log[ 0 ]->created_at,
1284 $search_log[ 0 ]->keyword_id
1285 )
1286 );
1287 } else {
1288 if ( ! empty( $input_not_found ) ) {
1289 $count = 0;
1290 $not_found_count = 1;
1291 } else {
1292 $count = 1;
1293 $not_found_count = 0;
1294 }
1295 $insert = $wpdb->query(
1296 $wpdb->prepare(
1297 "INSERT INTO {$wpdb->prefix}betterdocs_search_log
1298 ( keyword_id, count, not_found_count, created_at )
1299 VALUES ( %d, %d, %d, %s )",
1300 array(
1301 $search[ 0 ]->id,
1302 $count,
1303 $not_found_count,
1304 gmdate( 'Y-m-d' )
1305 )
1306 )
1307 );
1308 }
1309 } else {
1310 $insert = $wpdb->query(
1311 $wpdb->prepare(
1312 "INSERT INTO {$wpdb->prefix}betterdocs_search_keyword
1313 ( keyword )
1314 VALUES ( %s )",
1315 array(
1316 $search_input
1317 )
1318 )
1319 );
1320
1321 if ( $insert ) {
1322 if ( ! empty( $input_not_found ) ) {
1323 $count = 0;
1324 $not_found_count = 1;
1325 } else {
1326 $count = 1;
1327 $not_found_count = 0;
1328 }
1329 $insert = $wpdb->query(
1330 $wpdb->prepare(
1331 "INSERT INTO {$wpdb->prefix}betterdocs_search_log
1332 ( keyword_id, count, not_found_count, created_at )
1333 VALUES ( %d, %d, %d, %s )",
1334 array(
1335 $wpdb->insert_id,
1336 $count,
1337 $not_found_count,
1338 gmdate( 'Y-m-d' )
1339 )
1340 )
1341 );
1342 }
1343 }
1344 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1345 return $insert;
1346 }
1347
1348 /**
1349 * Counts the number of 'doc_category' terms assigned to a specific 'knowledge_base' by slug.
1350 *
1351 * This method retrieves all terms in the 'doc_category' taxonomy and checks the term meta
1352 * 'doc_category_knowledge_base' to determine how many 'doc_category' terms are associated
1353 * with a given 'knowledge_base' slug.
1354 *
1355 * @param string $knowledge_base_slug The slug of the 'knowledge_base' to count the assignments for.
1356 *
1357 * @return int The count of 'doc_category' terms assigned to the specified 'knowledge_base'.
1358 */
1359 public function count_doc_categories_for_knowledge_base( $knowledge_base_slug ) {
1360 $count = 0;
1361
1362 $doc_categories = get_terms(
1363 array(
1364 'taxonomy' => 'doc_category',
1365 'hide_empty' => true
1366 )
1367 );
1368
1369 if ( ! empty( $doc_categories ) && ! is_wp_error( $doc_categories ) ) {
1370 foreach ( $doc_categories as $term ) {
1371 $meta_value = get_term_meta( $term->term_id, 'doc_category_knowledge_base', true );
1372
1373 if ( $meta_value ) {
1374 $knowledge_bases = maybe_unserialize( $meta_value );
1375
1376 // Check if the specific knowledge base slug is present in the meta value array
1377 if ( is_array( $knowledge_bases ) && in_array( $knowledge_base_slug, $knowledge_bases ) ) {
1378 ++$count;
1379 }
1380 }
1381 }
1382 }
1383
1384 return $count;
1385 }
1386 }
1387