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

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