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
betterdocs / includes / REST / FaqSearchedTerms.php

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

225 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\REST;
4
5 use WP_Error;
6 use WPDeveloper\BetterDocs\Core\BaseAPI;
7
8 class FaqSearchedTerms extends BaseAPI {
9 public function permission_check() {
10 // Only allow logged-in users with FAQ management capabilities
11 return current_user_can( 'read_faq_builder' );
12 }
13
14 public function register() {
15 $this->get( 'faq-terms-by-keyword-search', [$this, 'search_logic'], [
16 'password' => [
17 'description' => __( 'The password for password-protected FAQs.', 'betterdocs' ),
18 'type' => 'string',
19 ],
20 'taxonomy' => [
21 'description' => __( 'The FAQ taxonomy to search within.', 'betterdocs' ),
22 'type' => 'string',
23 ],
24 ] );
25 $this->post( 'faq-accordion-toggle', [$this, 'toggle_enable_disable'] );
26 }
27
28 public function search_logic( $request ) {
29 $keyword = $request->get_param( 'search' );
30
31 if ( empty( $keyword ) ) {
32 $error = new WP_Error( 400, __( 'FAQ Search Parameter Cannot Be Empty', 'betterdocs' ) );
33 return rest_ensure_response( $error );
34 }
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
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 = [];
48
49 // Determine allowed post statuses based on user permissions
50 $post_status = ['publish'];
51 if( current_user_can( 'read_private_docs' ) ) {
52 $post_status[] = 'private';
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 }
61
62 $args = [
63 'post_type' => 'betterdocs_faq',
64 'post_status' => $post_status,
65 's' => $keyword,
66 'posts_per_page' => -1
67 ];
68
69 // Exclude password-protected posts unless user has permission
70 if ( ! current_user_can( 'edit_posts' ) ) {
71 $args['has_password'] = false;
72 }
73
74 $query = new \WP_Query( $args );
75 if ( $query->have_posts() ) {
76 while ( $query->have_posts() ) {
77 $query->the_post();
78 $post_obj = get_post( get_the_ID() );
79
80 // Check if user can access password-protected content
81 if ( ! empty( $post_obj->post_password ) ) {
82 $can_access = $this->can_access_password_content( $post_obj, $request );
83 if ( ! $can_access ) {
84 continue; // Skip this FAQ
85 }
86 }
87
88 $categories = get_the_terms( get_the_ID(), $taxonomy );
89
90 if ( $categories ) {
91 foreach ( $categories as $category ) {
92 $term_ids[] = $category->term_id;
93 $term_faq_map[ $category->term_id ][] = (int) get_the_ID();
94 }
95 }
96 }
97 wp_reset_postdata();
98 }
99
100 $terms = get_terms(
101 [
102 'taxonomy' => $taxonomy,
103 'hide_empty' => false,
104 'search' => $keyword
105 ]
106 );
107
108 foreach ( $terms as $term ) {
109 $term_ids[] = $term->term_id;
110 }
111
112 $term_ids = array_unique( $term_ids );
113
114 if ( empty( $term_ids ) ) {
115 return rest_ensure_response( [] );
116 }
117
118 $terms_with_meta = [];
119
120 $terms_payload = get_terms(
121 [
122 'taxonomy' => $taxonomy,
123 'hide_empty' => false,
124 'include' => $term_ids
125 ]
126 );
127
128 foreach ( $terms_payload as $term ) {
129 $meta = get_term_meta( $term->term_id );
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 )];
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 : [];
137 array_push( $terms_with_meta, $term );
138 }
139
140 return rest_ensure_response( $terms_with_meta );
141 }
142
143 public function toggle_enable_disable( $request ) {
144 $body_params = json_decode( $request->get_body() );
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';
160
161 if ( $faq_id != 0 ) {
162 // Security check: Verify user can edit this FAQ post
163 if ( ! current_user_can( 'edit_post', $faq_id ) ) {
164 return new WP_Error(
165 'rest_cannot_edit',
166 __( 'Sorry, you are not allowed to edit this FAQ.', 'betterdocs' ),
167 array( 'status' => 403 )
168 );
169 }
170
171 // Verify the post is actually a FAQ post type
172 $post = get_post( $faq_id );
173 if ( ! $post || $post->post_type !== 'betterdocs_faq' ) {
174 return new WP_Error(
175 'rest_post_invalid_id',
176 __( 'Invalid FAQ ID.', 'betterdocs' ),
177 array( 'status' => 404 )
178 );
179 }
180
181 $previous_toggle = get_post_meta( $faq_id, 'faq_open_by_default', true );
182 return update_post_meta( $faq_id, 'faq_open_by_default', $toggle, $previous_toggle );
183 }
184
185 return new WP_Error(
186 'rest_missing_callback_param',
187 __( 'FAQ ID is required.', 'betterdocs' ),
188 array( 'status' => 400 )
189 );
190 }
191
192 /**
193 * Checks if the user can access password-protected content.
194 *
195 * This method determines whether we need to override the regular password
196 * check in core with a filter.
197 *
198 * @param WP_Post $post Post to check against.
199 * @param WP_REST_Request $request Request data to check.
200 * @return bool True if the user can access password-protected content, otherwise false.
201 */
202 public function can_access_password_content( $post, $request ) {
203 if ( empty( $post->post_password ) ) {
204 // No filter required.
205 return true;
206 }
207
208 /*
209 * Users always get access to password protected content if they have
210 * the `edit_post` meta capability.
211 */
212 if ( current_user_can( 'edit_post', $post->ID ) ) {
213 return true;
214 }
215
216 // No password provided in request, no auth.
217 if ( empty( $request ) || empty( $request['password'] ) ) {
218 return false;
219 }
220
221 // Double-check the request password.
222 return hash_equals( $post->post_password, $request['password'] );
223 }
224 }
225