PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.6
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.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 / REST / Docs.php

Docs.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.5.6, at includes/REST/Docs.php

699 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- core docs REST endpoints; meta/tax filtering required.
3 namespace WPDeveloper\BetterDocs\REST;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9
10 use Error;
11 use WP_Query;
12 use WP_REST_Response;
13 use WPDeveloper\BetterDocs\Core\BaseAPI;
14
15 class Docs extends BaseAPI {
16 public function permission_check(): bool {
17 return true;
18 }
19
20 public function register() {
21 $this->get( 'search', [$this, 'search_posts'], [
22 'password' => [
23 'description' => __( 'The password for password-protected docs.', 'betterdocs' ),
24 'type' => 'string',
25 ],
26 ] );
27 $this->get( 'search-insert', [$this, 'search_insert'] );
28 $this->get( 'get-terms', [$this, 'get_terms_name_and_slug'] );
29 $this->get( 'months-with-posts', [$this, 'get_months_with_posts'] );
30 $this->get( 'order_docs', [$this, 'render_betterdocs_order_docs'], [
31 'password' => [
32 'description' => __( 'The password for password-protected docs.', 'betterdocs' ),
33 'type' => 'string',
34 ],
35 ] );
36 $this->register_field( 'docs', 'year_month', [
37 'get_callback' => [$this, 'year_month']
38 ] );
39
40 $this->register_field(
41 'docs',
42 'password',
43 [
44 'get_callback' => [ $this, 'get_post_password' ]
45 ]
46 );
47
48 add_filter( 'rest_docs_query', [ $this, 'filter_docs_query' ], 10, 2 );
49 $this->get( 'docs-faq-count', [ $this, 'get_docs_faq_counts' ] );
50 }
51
52 public function render_betterdocs_order_docs($request) {
53 $doc_category = $request->get_param('doc_category');
54 $order = $request->get_param('order');
55 $orderby = $request->get_param('orderby');
56 $per_page = $request->get_param('per_page');
57
58 if( empty( $doc_category) ) {
59 return [];
60 }
61
62 $args = [
63 'term_id' => $doc_category,
64 'orderby' => $orderby,
65 'order' => $order,
66 'posts_per_page' => $per_page
67 ];
68
69 $args = betterdocs()->query->docs_query_args($args);
70
71 // Exclude password-protected posts unless user has permission or provided password
72 if ( ! current_user_can( 'edit_posts' ) ) {
73 $args['has_password'] = false;
74 }
75
76 $posts = betterdocs()->query->get_posts( $args, true );
77
78 if ( ! $posts->have_posts() ) {
79 wp_reset_postdata();
80 }
81
82 $post_datas = [];
83
84 while ( $posts->have_posts() ):
85 $posts->the_post();
86 $post_obj = get_post( get_the_ID() );
87
88 // Double-check password protection for individual posts
89 if ( ! empty( $post_obj->post_password ) ) {
90 $can_access = $this->can_access_password_content( $post_obj, $request );
91 if ( ! $can_access ) {
92 continue; // Skip this post
93 }
94 }
95
96 $post_data = $this->get_doc_data( get_the_ID(), $request );
97 array_push( $post_datas, $post_data );
98 endwhile;
99
100 wp_reset_postdata();
101 wp_reset_postdata();
102
103 return $post_datas;
104 }
105
106 public function get_docs_faq_counts() {
107 // Initialize the return array
108 $counts = [
109 'created_docs' => 0,
110 'published_docs' => 0,
111 'created_faq' => 0,
112 'published_faq' => 0
113 ];
114
115 // Get all docs (any status)
116 $all_docs_query = new WP_Query([
117 'post_type' => 'docs',
118 'post_status' => 'any',
119 'posts_per_page' => -1,
120 'fields' => 'ids',
121 'no_found_rows' => true,
122 ]);
123 $counts['created_docs'] = $all_docs_query->post_count;
124
125 // Get published docs only
126 $published_docs_query = new WP_Query([
127 'post_type' => 'docs',
128 'post_status' => 'publish',
129 'posts_per_page' => -1,
130 'fields' => 'ids',
131 'no_found_rows' => true,
132 ]);
133 $counts['published_docs'] = $published_docs_query->post_count;
134
135 // Get all FAQs (any status)
136 $all_faq_query = new WP_Query([
137 'post_type' => 'betterdocs_faq',
138 'post_status' => 'any',
139 'posts_per_page' => -1,
140 'fields' => 'ids',
141 'no_found_rows' => true,
142 ]);
143 $counts['created_faq'] = $all_faq_query->post_count;
144
145 // Get published FAQs only
146 $published_faq_query = new WP_Query([
147 'post_type' => 'betterdocs_faq',
148 'post_status' => 'publish',
149 'posts_per_page' => -1,
150 'fields' => 'ids',
151 'no_found_rows' => true,
152 ]);
153 $counts['published_faq'] = $published_faq_query->post_count;
154
155 return $counts;
156 }
157
158 /**
159 * Get Doc Data Based On Doc ID
160 *
161 * @param int $id Post ID
162 * @param WP_REST_Request $request REST request object
163 * @return array
164 */
165 public function get_doc_data( $id, $request = null ) {
166 $post_data = get_post( $id );
167
168 // Check if user can access password-protected content
169 $can_access_password_content = $this->can_access_password_content( $post_data, $request );
170
171 $data = [
172 'author' => (int) $post_data->post_author,
173 'author_info' => [
174 'name' => get_the_author_meta( 'display_name', $post_data->post_author ),
175 'author_nicename' => get_the_author_meta( 'nicename', $post_data->post_author ),
176 'author_url' => get_author_posts_url( $post_data->post_author )
177 ],
178 'unique_id' => uniqid( 'doc' ),
179 'id' => $post_data->ID,
180 'title' => [
181 'rendered' => $post_data->post_title
182 ],
183 'slug' => get_post_field( 'post_name', $id ),
184 'link' => get_permalink( $id ),
185 'status' => get_post_status(),
186 'date' => $post_data->post_date,
187 'date_gmt' => $post_data->post_date_gmt,
188 'doc_category' => wp_get_post_terms( $id, 'doc_category', ["fields" => "ids"] ),
189 'doc_tag' => wp_get_post_terms( $id, 'doc_tag', ["fields" => "ids"] ),
190 'comment_status' => $post_data->comment_status
191 ];
192
193 // Only include password field if user has edit permissions
194 if ( current_user_can( 'edit_post', $id ) ) {
195 $data['password'] = $post_data->post_password;
196 }
197
198 // Add password protection indicator
199 if ( ! empty( $post_data->post_password ) ) {
200 $data['password_protected'] = true;
201
202 // If user cannot access password-protected content, hide sensitive data
203 if ( ! $can_access_password_content ) {
204 // Keep basic info but indicate it's protected
205 $data['title']['rendered'] = $post_data->post_title; // WordPress doesn't prefix in REST API
206 $data['excerpt'] = ''; // Hide excerpt for password-protected posts
207 }
208 } else {
209 $data['password_protected'] = false;
210 }
211
212 if ( taxonomy_exists( 'knowledge_base' ) ) {
213 $data['knowledge_base'] = wp_get_post_terms( $id, 'knowledge_base', ["fields" => "ids"] );
214 }
215
216 return $data;
217 }
218
219 /**
220 * Checks if the user can access password-protected content.
221 *
222 * This method determines whether we need to override the regular password
223 * check in core with a filter.
224 *
225 * @param WP_Post $post Post to check against.
226 * @param WP_REST_Request $request Request data to check.
227 * @return bool True if the user can access password-protected content, otherwise false.
228 */
229 public function can_access_password_content( $post, $request ) {
230 if ( empty( $post->post_password ) ) {
231 // No filter required.
232 return true;
233 }
234
235 /*
236 * Users always get access to password protected content if they have
237 * the `edit_post` meta capability.
238 */
239 if ( current_user_can( 'edit_post', $post->ID ) ) {
240 return true;
241 }
242
243 // No password provided in request, no auth.
244 if ( empty( $request ) || empty( $request['password'] ) ) {
245 return false;
246 }
247
248 // Double-check the request password.
249 return hash_equals( $post->post_password, $request['password'] );
250 }
251
252 /**
253 * Retrieves the months and years that have posts of the type 'docs' and formats them.
254 *
255 * This function queries the WordPress database for all unique months and years
256 * in which 'docs' post type posts have been published. The results are then
257 * formatted into an array of associative arrays, where each entry contains an
258 * 'id' and a 'name'.
259 *
260 * The 'id' is a string formatted as 'month-year' (e.g., 'may-2024') to provide
261 * a unique identifier that is easy to work with in JavaScript and HTML. The 'name'
262 * is a more human-readable string formatted as 'Month Year' (e.g., 'May 2024') to
263 * display to users.
264 *
265 * @return WP_REST_Response A response containing the formatted months and years.
266 */
267 public function get_months_with_posts() {
268 global $wpdb;
269
270 // Query to get distinct year and month from posts of type 'docs'
271 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- aggregation across the posts table; no user input.
272 $results = $wpdb->get_results(
273 "SELECT DISTINCT YEAR(post_date) AS year, MONTH(post_date) AS month
274 FROM $wpdb->posts
275 WHERE post_type = 'docs'
276 ORDER BY post_date DESC"
277 );
278
279 $formatted_months = [];
280
281 foreach ( $results as $result ) {
282 $year = $result->year;
283 $month = $result->month;
284
285 // Create a DateTime object to format the month
286 $date = \DateTime::createFromFormat( '!m', $month );
287 $month_name = $date->format( 'F' ); // Full month name
288 $month_number = $date->format( 'm' ); // Month number with leading zero
289
290 // Format the months and years into wp rest api structure like wp-json/wp/v2/doc_category
291 $formatted_months[] = [
292 'id' => "$year-$month_number", // e.g., '2024-05'
293 'name' => "$month_name $year" // e.g., 'May 2024'
294 ];
295 }
296
297 return rest_ensure_response( $formatted_months );
298 }
299
300 /**
301 * Callback function to retrieve 'year_month' field value.
302 *
303 * @param object $post The REST API response object.
304 * @return string The formatted date (e.g., '2024-05').
305 */
306 public function year_month( $post ) {
307 $date_string = isset( $post->post_date ) ? $post->post_date : '';
308
309 $date = new \DateTime( $date_string );
310
311 // Format the date to 'Y-m' (e.g., '2024-05')
312 $formatted_date = $date->format( 'Y-m' );
313
314 return $formatted_date;
315 }
316
317 /**
318 * Filter the docs query by year_month and status parameters.
319 *
320 * @param array $args The query arguments.
321 * @param WP_REST_Request $request The current REST API request.
322 * @return array Modified query arguments.
323 */
324 public function filter_docs_query( $args, $request ) {
325 // Filter by year_month
326 if ( isset( $request['year_month'] ) ) {
327 $formatted_date = $request['year_month'];
328
329 // Parse the formatted_date to year and month
330 $year = substr( $formatted_date, 0, 4 );
331 $month = substr( $formatted_date, 5, 2 );
332
333 // Add date query arguments
334 $args['date_query'] = [
335 [
336 'year' => $year,
337 'month' => $month
338 ]
339 ];
340 }
341
342 // Filter by post status
343 // When status is 'any' and user has edit_docs capability, show all post statuses
344 if ( isset( $request['status'] ) && $request['status'] === 'any' && current_user_can( 'edit_docs' ) ) {
345 $args['post_status'] = [ 'publish', 'draft', 'pending', 'private', 'future' ];
346 }
347
348 return $args;
349 }
350
351
352 public function get_post_password( $object, $field_name, $request ) {
353 // Suppress unused parameter warnings
354 unset( $field_name, $request );
355
356 if ( current_user_can( 'edit_docs' ) ) {
357 return isset( $object['password'] ) ? $object['password'] : '';
358 } else {
359 return '';
360 }
361 }
362
363 public function search_posts( $request ) {
364 $search_query = sanitize_text_field( $request->get_param( 's' ) );
365 $doc_category = sanitize_text_field( $request->get_param( 'doc_category' ) );
366 $kb_slug = sanitize_text_field( $request->get_param( 'knowledge_base' ) );
367 $number = (int) $request->get_param( 'per_page' ) ? (int) $request->get_param( 'per_page' ) : 5;
368 $docs_ids = ! empty( $request->get_param( 'doc_ids' ) ) ? explode( ',', $request->get_param( 'doc_ids' ) ) : [];
369 $doc_term_ids = ! empty( $request->get_param( 'doc_categories_ids' ) ) ? explode( ',', $request->get_param( 'doc_categories_ids' ) ) : [];
370 $faq_term_ids = ! empty( $request->get_param( 'faq_categories_ids' ) ) ? explode( ',', $request->get_param( 'faq_categories_ids' ) ) : [];
371 $posts = array();
372 $post_status = ['publish'];
373
374 if( current_user_can( 'read_private_docs' ) ) {
375 array_push($post_status, 'private');
376 }
377
378 // Common query args
379 $common_args = [
380 'post_status' => $post_status,
381 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Hooks.PreGetPosts.PreGetPosts,WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- search bypasses content filters; WPML override below.
382 'orderby' => 'relevance',
383 ];
384
385 // Exclude password-protected posts unless user has permission
386 if ( ! current_user_can( 'edit_posts' ) ) {
387 $common_args['has_password'] = false;
388 }
389
390 // Handle WPML multilingual search
391 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
392 // If search term contains non-ASCII characters (e.g., Chinese, Japanese, Bangla),
393 // search across all languages to find translated posts
394 if ( $search_query && preg_match('/[^\x00-\x7F]/', $search_query) ) {
395 // Non-ASCII search: bypass ALL filters including WPML language filtering
396 // This allows searching across all languages
397 $common_args['suppress_filters'] = true; // phpcs:ignore WordPressVIPMinimum.Hooks.PreGetPosts.PreGetPosts,WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- non-ASCII search must reach all WPML translations.
398 } else {
399 // ASCII-only search (English), use WPML filters to restrict to current language
400 $common_args['suppress_filters'] = false;
401 $common_args['lang'] = ICL_LANGUAGE_CODE;
402 }
403 }
404
405 if ( $search_query ) {
406 $common_args['s'] = $search_query;
407 // Respect per_page from the client; cap at 50 so a slow LIKE query can't load thousands of rows.
408 $common_args['posts_per_page'] = $number > 0 ? min( $number, 50 ) : 20;
409
410 // SearchExtender's posts_search filter must run so docs with matching
411 // tag/category term names are included in results.
412 $common_args['suppress_filters'] = false;
413 } else {
414 $common_args['posts_per_page'] = $number;
415 }
416
417 // Docs-specific query
418 $docs_args = array_merge(
419 $common_args,
420 [
421 'post_type' => 'docs'
422 ]
423 );
424
425 if ( ! $search_query ) {
426 // Use date ordering when KB filter is present to avoid analytics query conflicts
427 if ( ! empty( $kb_slug ) ) {
428 $docs_args['orderby'] = 'date';
429 $docs_args['order'] = 'DESC';
430 } else {
431 $docs_args['meta_key'] = '_betterdocs_meta_views';
432 $docs_args['orderby'] = 'meta_value_num';
433 $docs_args['order'] = 'DESC';
434 }
435 }
436
437 if ( ! empty( $docs_ids ) ) {
438 unset( $docs_args['meta_key'] );
439 $docs_args['posts_per_page'] = -1;
440 $docs_args['post__in'] = $docs_ids;
441 }
442
443 if ( ! empty( $doc_term_ids ) ) {
444 unset( $docs_args['meta_key'] );
445 $docs_args['posts_per_page'] = -1;
446 $docs_args['tax_query'] = [
447 [
448 'taxonomy' => 'doc_category',
449 'field' => 'term_id',
450 'terms' => $doc_term_ids,
451 'operator' => 'IN',
452 ]
453 ];
454 }
455
456 // Taxonomy filter for docs
457 if ( $doc_category ) {
458 $docs_args['tax_query'] = [
459 [
460 'taxonomy' => 'doc_category',
461 'field' => 'slug',
462 'terms' => $doc_category,
463 'operator' => 'AND',
464 'include_children' => true,
465 ],
466 ];
467 }
468
469 // Knowledge base filter for docs
470 // Pass kb_slug in args to let MultipleKB filter handle it (avoid duplicate filters)
471 if ( ! empty( $kb_slug ) && taxonomy_exists( 'knowledge_base' ) ) {
472 $docs_args['kb_slug'] = $kb_slug;
473 }
474
475
476 // FAQ-specific query
477 $faq_args = array_merge(
478 $common_args,
479 [
480 'post_type' => 'betterdocs_faq',
481 'orderby' => 'date',
482 'order' => 'DESC',
483 ]
484 );
485
486 if ( ! empty( $faq_term_ids ) ) {
487 $faq_args['posts_per_page'] = -1;
488 $faq_args['tax_query'] = [
489 [
490 'taxonomy' => 'betterdocs_faq_category',
491 'field' => 'term_id',
492 'terms' => $faq_term_ids,
493 'operator' => 'IN',
494 ]
495 ];
496 }
497
498 $docs_query = betterdocs()->query->get_posts( $docs_args );
499
500 $faq_query = new WP_Query( $faq_args );
501
502 // Process docs posts
503 if ( $docs_query->have_posts() ) {
504 while ( $docs_query->have_posts() ) {
505 $docs_query->the_post();
506
507 $post_obj = get_post( get_the_ID() );
508
509 // Check if user can access password-protected content
510 $can_access = $this->can_access_password_content( $post_obj, $request );
511
512 // Skip password-protected posts if user cannot access them
513 if ( ! empty( $post_obj->post_password ) && ! $can_access ) {
514 continue;
515 }
516
517 $taxonomies = array();
518 $terms = get_the_terms( get_the_ID(), 'doc_category' );
519 if ( $terms && ! is_wp_error( $terms ) ) {
520 $taxonomies = wp_list_pluck( $terms, 'name' );
521 }
522
523 // Get the correct permalink with language parameter if needed
524 $post_id = get_the_ID();
525 $permalink = get_the_permalink( $post_id );
526
527 // If WPML is active and post language differs from site language, add lang parameter
528 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
529 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML public integration filter.
530 $post_language = apply_filters( 'wpml_element_language_code', null, array( 'element_id' => $post_id, 'element_type' => 'post_docs' ) );
531
532 if ( $post_language ) {
533 global $sitepress;
534 $current_lang = $sitepress ? $sitepress->get_current_language() : '';
535
536 // If post language is different from current site language, add language parameter
537 if ( $post_language !== $current_lang ) {
538 $permalink = add_query_arg( 'lang', $post_language, $permalink );
539 }
540 }
541 }
542 // Handle TranslatePress permalinks
543 elseif ( class_exists( '\TRP_Translate_Press' ) ) {
544 global $TRP_LANGUAGE;
545 $trp = \TRP_Translate_Press::get_trp_instance();
546 if ( isset( $trp ) && method_exists( $trp, 'get_component' ) ) {
547 $trp_settings = $trp->get_component( 'settings' );
548 $trp_url_converter = $trp->get_component( 'url_converter' );
549
550 if ( $trp_settings && $trp_url_converter && isset( $TRP_LANGUAGE ) ) {
551 $settings = $trp_settings->get_settings();
552 $default_lang = isset( $settings['default-language'] ) ? $settings['default-language'] : 'en_US';
553
554 // If we're not on the default language, ensure the URL has the language prefix
555 if ( $TRP_LANGUAGE && $TRP_LANGUAGE !== $default_lang ) {
556 $permalink = $trp_url_converter->get_url_for_language( $TRP_LANGUAGE, $permalink );
557 // Remove the #TRPLINKPROCESSED marker that TranslatePress adds
558 $permalink = str_replace( '#TRPLINKPROCESSED', '', $permalink );
559 }
560 }
561 }
562 }
563
564
565 // Get the title - apply TranslatePress translation if active
566 $title = get_the_title();
567 if ( class_exists( '\TRP_Translate_Press' ) ) {
568 global $TRP_LANGUAGE, $wpdb;
569 if ( isset( $TRP_LANGUAGE ) ) {
570 $trp = \TRP_Translate_Press::get_trp_instance();
571 if ( isset( $trp ) && method_exists( $trp, 'get_component' ) ) {
572 $trp_settings = $trp->get_component( 'settings' );
573 if ( $trp_settings ) {
574 $settings = $trp_settings->get_settings();
575 $default_lang = isset( $settings['default-language'] ) ? strtolower( $settings['default-language'] ) : 'en_us';
576 $current_lang = strtolower( $TRP_LANGUAGE );
577
578 // Only query translation if not on default language
579 if ( $default_lang !== $current_lang ) {
580 $default_lang = preg_replace( '/[^a-z0-9_]/', '', $default_lang );
581 $current_lang = preg_replace( '/[^a-z0-9_]/', '', $current_lang );
582 $trp_table = $wpdb->prefix . 'trp_dictionary_' . $default_lang . '_' . $current_lang;
583
584 // Query the translation dictionary for this title.
585 // $trp_table is composed from $wpdb->prefix + sanitized lang slugs (preg_replace allowlist above), safe to interpolate.
586 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- TranslatePress dynamic dictionary table; cache would defeat live translation lookup.
587 $translated = $wpdb->get_var( $wpdb->prepare(
588 "SELECT translated FROM {$trp_table} WHERE original = %s AND status != 2 LIMIT 1",
589 $title
590 ) );
591 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
592
593 if ( $translated && ! empty( $translated ) ) {
594 $title = $translated;
595 }
596 }
597 }
598 }
599 }
600 }
601
602 $posts[] = array(
603 'title' => $title,
604 'post_type' => get_post_type(),
605 'permalink' => $permalink,
606 'taxonomies' => implode( ', ', $taxonomies ),
607 );
608 }
609 wp_reset_postdata();
610 }
611
612 // Process FAQ posts with content
613 if ( $faq_query->have_posts() ) {
614 while ( $faq_query->have_posts() ) {
615 $faq_query->the_post();
616
617 $terms = get_the_terms( get_the_ID(), 'betterdocs_faq_category' );
618 $taxonomies = array();
619 if ( $terms && ! is_wp_error( $terms ) ) {
620 $taxonomies = wp_list_pluck( $terms, 'name' );
621 }
622
623 $posts[] = array(
624 'title' => get_the_title(),
625 'content' => get_the_content(), // Include post content for FAQ posts
626 'post_type' => get_post_type(),
627 'permalink' => get_the_permalink(),
628 'taxonomies' => implode( ', ', $taxonomies ),
629 );
630 }
631 wp_reset_postdata();
632 }
633
634 return $posts;
635 }
636
637
638
639 public function search_insert( $request ) {
640 $search_input = sanitize_text_field( $request->get_param( 's' ) );
641 $no_result = sanitize_text_field( $request->get_param( 'no_result' ) );
642
643 return betterdocs()->query->insert_search_keyword( $search_input, $no_result );
644 }
645
646
647 public function get_terms_name_and_slug( $request ) {
648 $default_params = [
649 'taxonomy' => $request->get_param( 'taxonomy' ),
650 'hide_empty' => false,
651 'fields' => 'all',
652 ];
653
654 if ( betterdocs()->settings->get( 'child_category_exclude' ) ) { //disable child terms if this is enabled
655 $default_params['parent'] = 0;
656 }
657
658 // Add KB filtering if knowledge_base parameter is provided
659 $kb_slug = $request->get_param( 'knowledge_base' );
660 if ( ! empty( $kb_slug ) && $request->get_param( 'taxonomy' ) === 'doc_category' ) {
661 // Categories can belong to multiple KBs (stored as serialized array in doc_category_knowledge_base)
662 // We need to filter categories that have the KB slug in their serialized array
663 $default_params['meta_query'] = [
664 [
665 'key' => 'doc_category_knowledge_base',
666 'value' => serialize(strval($kb_slug)),
667 'compare' => 'LIKE'
668 ]
669 ];
670 }
671
672 // Retrieve all terms for the specified taxonomy, including empty ones
673 $terms = get_terms($default_params);
674
675 // Initialize an empty array to hold the term data
676 $term_data = [];
677
678 // Loop through each term and extract the name and slug
679 $term_data = array_map(
680 function ( $term ) {
681 return [
682 'name' => $term->name,
683 'slug' => $term->slug,
684 'parent' => $term->parent,
685 ];
686 },
687 $terms
688 );
689
690 // Return the array of term data
691 return $term_data;
692 }
693
694 public function get_faq_categories( $request ) {
695 // Suppress unused parameter warning
696 unset( $request );
697 }
698 }
699