PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.0
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / REST / Docs.php

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

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