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