PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.3
4.9.3 4.9.2 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 All 201 releases
← All changes | includes/REST/FaqSearchedTerms.php +48 -7 4.4.0 → 4.9.3 View file →
@@ -13,11 +13,15 @@
13 13
14 14 public function register() {
15 15 $this->get( 'faq-terms-by-keyword-search', [$this, 'search_logic'], [
16 16 'password' => [
17 - 'description' => __( 'The password for password-protected FAQs.' ),
17 + 'description' => __( 'The password for password-protected FAQs.', 'betterdocs' ),
18 18 'type' => 'string',
19 19 ],
20 + 'taxonomy' => [
21 + 'description' => __( 'The FAQ taxonomy to search within.', 'betterdocs' ),
22 + 'type' => 'string',
23 + ],
20 24 ] );
21 25 $this->post( 'faq-accordion-toggle', [$this, 'toggle_enable_disable'] );
22 26 }
23 27
@@ -28,9 +32,20 @@
28 32 $error = new WP_Error( 400, __( 'FAQ Search Parameter Cannot Be Empty', 'betterdocs' ) );
29 33 return rest_ensure_response( $error );
30 34 }
31 35
36 + // Scope the search to the requested FAQ taxonomy (General vs. WooCommerce
37 + // Product FAQ Groups). Whitelisted so an arbitrary taxonomy can't be queried.
38 + $allowed_taxonomies = [ 'betterdocs_faq_category', 'betterdocs_product_faq_category' ];
39 + $taxonomy = $request->get_param( 'taxonomy' );
40 + if ( ! in_array( $taxonomy, $allowed_taxonomies, true ) ) {
41 + $taxonomy = 'betterdocs_faq_category';
42 + }
43 +
32 44 $term_ids = [];
45 + // Map of category term_id => [ matched FAQ ids ] so the UI can show
46 + // only the FAQs that matched the keyword (not the whole group).
47 + $term_faq_map = [];
33 48
34 49 // Determine allowed post statuses based on user permissions
35 50 $post_status = ['publish'];
36 51 if( current_user_can( 'read_private_docs' ) ) {
@@ -35,8 +50,15 @@
35 50 $post_status = ['publish'];
36 51 if( current_user_can( 'read_private_docs' ) ) {
37 52 $post_status[] = 'private';
38 53 }
54 + // The FAQ Builder is an editor-facing tool, so draft FAQs must be
55 + // searchable too. QA-006: gate drafts on `edit_others_posts` (Editor+)
56 + // rather than `edit_posts` (Author) — the query has no author scope, so
57 + // an Author-level user would otherwise see every author's draft FAQs.
58 + if( current_user_can( 'edit_others_posts' ) ) {
59 + $post_status[] = 'draft';
60 + }
39 61
40 62 $args = [
41 63 'post_type' => 'betterdocs_faq',
42 64 'post_status' => $post_status,
@@ -62,13 +84,14 @@
62 84 continue; // Skip this FAQ
63 85 }
64 86 }
65 87
66 - $categories = get_the_terms( get_the_ID(), 'betterdocs_faq_category' );
88 + $categories = get_the_terms( get_the_ID(), $taxonomy );
67 89
68 90 if ( $categories ) {
69 91 foreach ( $categories as $category ) {
70 - $term_ids[] = $category->term_id;
92 + $term_ids[] = $category->term_id;
93 + $term_faq_map[ $category->term_id ][] = (int) get_the_ID();
71 94 }
72 95 }
73 96 }
74 97 wp_reset_postdata();
@@ -75,9 +98,9 @@
75 98 }
76 99
77 100 $terms = get_terms(
78 101 [
79 - 'taxonomy' => 'betterdocs_faq_category',
102 + 'taxonomy' => $taxonomy,
80 103 'hide_empty' => false,
81 104 'search' => $keyword
82 105 ]
83 106 );
@@ -95,9 +118,9 @@
95 118 $terms_with_meta = [];
96 119
97 120 $terms_payload = get_terms(
98 121 [
99 - 'taxonomy' => 'betterdocs_faq_category',
122 + 'taxonomy' => $taxonomy,
100 123 'hide_empty' => false,
101 124 'include' => $term_ids
102 125 ]
103 126 );
@@ -105,8 +128,13 @@
105 128 foreach ( $terms_payload as $term ) {
106 129 $meta = get_term_meta( $term->term_id );
107 130 $meta['_betterdocs_faq_order'] = empty( get_term_meta( $term->term_id, '_betterdocs_faq_order', true ) ) ? [] : [get_term_meta( $term->term_id, '_betterdocs_faq_order', true )];
108 131 $term->meta = $meta;
132 + // FAQs in this group that matched the keyword. Empty when the group
133 + // was matched only by its name (then the UI shows the whole group).
134 + $term->matched_faqs = isset( $term_faq_map[ $term->term_id ] )
135 + ? array_values( array_unique( $term_faq_map[ $term->term_id ] ) )
136 + : [];
109 137 array_push( $terms_with_meta, $term );
110 138 }
111 139
112 140 return rest_ensure_response( $terms_with_meta );
@@ -113,10 +141,23 @@
113 141 }
114 142
115 143 public function toggle_enable_disable( $request ) {
116 144 $body_params = json_decode( $request->get_body() );
117 - $faq_id = isset( $body_params->faq_id ) ? $body_params->faq_id : 0;
118 - $toggle = $body_params->toggle;
145 +
146 + // QA-005: reject invalid / empty JSON instead of dereferencing null
147 + // (a PHP 8+ fatal: "Attempt to read property on null").
148 + if ( ! is_object( $body_params ) ) {
149 + return new WP_Error(
150 + 'rest_invalid_json',
151 + __( 'Invalid request body.', 'betterdocs' ),
152 + array( 'status' => 400 )
153 + );
154 + }
155 +
156 + $faq_id = isset( $body_params->faq_id ) ? absint( $body_params->faq_id ) : 0;
157 + // QA-005: normalize to a stored boolean ('1'/'0') — never write the raw
158 + // request value straight to post meta.
159 + $toggle = ( isset( $body_params->toggle ) && $body_params->toggle ) ? '1' : '0';
119 160
120 161 if ( $faq_id != 0 ) {
121 162 // Security check: Verify user can edit this FAQ post
122 163 if ( ! current_user_can( 'edit_post', $faq_id ) ) {