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

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