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

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