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

565 lines 16.9 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 Error;
6 use WP_Query;
7 use WP_REST_Response;
8 use WPDeveloper\BetterDocs\Core\BaseAPI;
9
10 class Docs extends BaseAPI {
11 public function permission_check(): bool {
12 return true;
13 }
14
15 public function register() {
16 $this->get( 'search', [$this, 'search_posts'], [
17 'password' => [
18 'description' => __( 'The password for password-protected docs.' ),
19 'type' => 'string',
20 ],
21 ] );
22 $this->get( 'search-insert', [$this, 'search_insert'] );
23 $this->get( 'get-terms', [$this, 'get_terms_name_and_slug'] );
24 $this->get( 'months-with-posts', [$this, 'get_months_with_posts'] );
25 $this->get( 'order_docs', [$this, 'render_betterdocs_order_docs'], [
26 'password' => [
27 'description' => __( 'The password for password-protected docs.' ),
28 'type' => 'string',
29 ],
30 ] );
31 $this->register_field( 'docs', 'year_month', [
32 'get_callback' => [$this, 'year_month']
33 ] );
34
35 $this->register_field(
36 'docs',
37 'password',
38 [
39 'get_callback' => [ $this, 'get_post_password' ]
40 ]
41 );
42
43 add_filter( 'rest_docs_query', [ $this, 'filter_docs_query' ], 10, 2 );
44 $this->get( 'docs-faq-count', [ $this, 'get_docs_faq_counts' ] );
45 }
46
47 public function render_betterdocs_order_docs($request) {
48 $doc_category = $request->get_param('doc_category');
49 $order = $request->get_param('order');
50 $orderby = $request->get_param('orderby');
51 $per_page = $request->get_param('per_page');
52
53 if( empty( $doc_category) ) {
54 return [];
55 }
56
57 $args = [
58 'term_id' => $doc_category,
59 'orderby' => $orderby,
60 'order' => $order,
61 'posts_per_page' => $per_page
62 ];
63
64 $args = betterdocs()->query->docs_query_args($args);
65
66 // Exclude password-protected posts unless user has permission or provided password
67 if ( ! current_user_can( 'edit_posts' ) ) {
68 $args['has_password'] = false;
69 }
70
71 $posts = betterdocs()->query->get_posts( $args, true );
72
73 if ( ! $posts->have_posts() ) {
74 wp_reset_query();
75 }
76
77 $post_datas = [];
78
79 while ( $posts->have_posts() ):
80 $posts->the_post();
81 $post_obj = get_post( get_the_ID() );
82
83 // Double-check password protection for individual posts
84 if ( ! empty( $post_obj->post_password ) ) {
85 $can_access = $this->can_access_password_content( $post_obj, $request );
86 if ( ! $can_access ) {
87 continue; // Skip this post
88 }
89 }
90
91 $post_data = $this->get_doc_data( get_the_ID(), $request );
92 array_push( $post_datas, $post_data );
93 endwhile;
94
95 wp_reset_postdata();
96 wp_reset_query();
97
98 return $post_datas;
99 }
100
101 public function get_docs_faq_counts() {
102 // Initialize the return array
103 $counts = [
104 'created_docs' => 0,
105 'published_docs' => 0,
106 'created_faq' => 0,
107 'published_faq' => 0
108 ];
109
110 // Get all docs (any status)
111 $all_docs_query = new WP_Query([
112 'post_type' => 'docs',
113 'post_status' => 'any',
114 'posts_per_page' => -1,
115 'fields' => 'ids',
116 'no_found_rows' => true,
117 ]);
118 $counts['created_docs'] = $all_docs_query->post_count;
119
120 // Get published docs only
121 $published_docs_query = new WP_Query([
122 'post_type' => 'docs',
123 'post_status' => 'publish',
124 'posts_per_page' => -1,
125 'fields' => 'ids',
126 'no_found_rows' => true,
127 ]);
128 $counts['published_docs'] = $published_docs_query->post_count;
129
130 // Get all FAQs (any status)
131 $all_faq_query = new WP_Query([
132 'post_type' => 'betterdocs_faq',
133 'post_status' => 'any',
134 'posts_per_page' => -1,
135 'fields' => 'ids',
136 'no_found_rows' => true,
137 ]);
138 $counts['created_faq'] = $all_faq_query->post_count;
139
140 // Get published FAQs only
141 $published_faq_query = new WP_Query([
142 'post_type' => 'betterdocs_faq',
143 'post_status' => 'publish',
144 'posts_per_page' => -1,
145 'fields' => 'ids',
146 'no_found_rows' => true,
147 ]);
148 $counts['published_faq'] = $published_faq_query->post_count;
149
150 return $counts;
151 }
152
153 /**
154 * Get Doc Data Based On Doc ID
155 *
156 * @param int $id Post ID
157 * @param WP_REST_Request $request REST request object
158 * @return array
159 */
160 public function get_doc_data( $id, $request = null ) {
161 $post_data = get_post( $id );
162
163 // Check if user can access password-protected content
164 $can_access_password_content = $this->can_access_password_content( $post_data, $request );
165
166 $data = [
167 'author' => (int) $post_data->post_author,
168 'author_info' => [
169 'name' => get_the_author_meta( 'display_name', $post_data->post_author ),
170 'author_nicename' => get_the_author_meta( 'nicename', $post_data->post_author ),
171 'author_url' => get_author_posts_url( $post_data->post_author )
172 ],
173 'unique_id' => uniqid( 'doc' ),
174 'id' => $post_data->ID,
175 'title' => [
176 'rendered' => $post_data->post_title
177 ],
178 'slug' => get_post_field( 'post_name', $id ),
179 'link' => get_permalink( $id ),
180 'status' => get_post_status(),
181 'date' => $post_data->post_date,
182 'date_gmt' => $post_data->post_date_gmt,
183 'doc_category' => wp_get_post_terms( $id, 'doc_category', ["fields" => "ids"] ),
184 'doc_tag' => wp_get_post_terms( $id, 'doc_tag', ["fields" => "ids"] ),
185 'comment_status' => $post_data->comment_status
186 ];
187
188 // Only include password field if user has edit permissions
189 if ( current_user_can( 'edit_post', $id ) ) {
190 $data['password'] = $post_data->post_password;
191 }
192
193 // Add password protection indicator
194 if ( ! empty( $post_data->post_password ) ) {
195 $data['password_protected'] = true;
196
197 // If user cannot access password-protected content, hide sensitive data
198 if ( ! $can_access_password_content ) {
199 // Keep basic info but indicate it's protected
200 $data['title']['rendered'] = $post_data->post_title; // WordPress doesn't prefix in REST API
201 $data['excerpt'] = ''; // Hide excerpt for password-protected posts
202 }
203 } else {
204 $data['password_protected'] = false;
205 }
206
207 if ( taxonomy_exists( 'knowledge_base' ) ) {
208 $data['knowledge_base'] = wp_get_post_terms( $id, 'knowledge_base', ["fields" => "ids"] );
209 }
210
211 return $data;
212 }
213
214 /**
215 * Checks if the user can access password-protected content.
216 *
217 * This method determines whether we need to override the regular password
218 * check in core with a filter.
219 *
220 * @param WP_Post $post Post to check against.
221 * @param WP_REST_Request $request Request data to check.
222 * @return bool True if the user can access password-protected content, otherwise false.
223 */
224 public function can_access_password_content( $post, $request ) {
225 if ( empty( $post->post_password ) ) {
226 // No filter required.
227 return true;
228 }
229
230 /*
231 * Users always get access to password protected content if they have
232 * the `edit_post` meta capability.
233 */
234 if ( current_user_can( 'edit_post', $post->ID ) ) {
235 return true;
236 }
237
238 // No password provided in request, no auth.
239 if ( empty( $request ) || empty( $request['password'] ) ) {
240 return false;
241 }
242
243 // Double-check the request password.
244 return hash_equals( $post->post_password, $request['password'] );
245 }
246
247 /**
248 * Retrieves the months and years that have posts of the type 'docs' and formats them.
249 *
250 * This function queries the WordPress database for all unique months and years
251 * in which 'docs' post type posts have been published. The results are then
252 * formatted into an array of associative arrays, where each entry contains an
253 * 'id' and a 'name'.
254 *
255 * The 'id' is a string formatted as 'month-year' (e.g., 'may-2024') to provide
256 * a unique identifier that is easy to work with in JavaScript and HTML. The 'name'
257 * is a more human-readable string formatted as 'Month Year' (e.g., 'May 2024') to
258 * display to users.
259 *
260 * @return WP_REST_Response A response containing the formatted months and years.
261 */
262 public function get_months_with_posts() {
263 global $wpdb;
264
265 // Query to get distinct year and month from posts of type 'docs'
266 $results = $wpdb->get_results(
267 "SELECT DISTINCT YEAR(post_date) AS year, MONTH(post_date) AS month
268 FROM $wpdb->posts
269 WHERE post_type = 'docs'
270 ORDER BY post_date DESC"
271 );
272
273 $formatted_months = [];
274
275 foreach ( $results as $result ) {
276 $year = $result->year;
277 $month = $result->month;
278
279 // Create a DateTime object to format the month
280 $date = \DateTime::createFromFormat( '!m', $month );
281 $month_name = $date->format( 'F' ); // Full month name
282 $month_number = $date->format( 'm' ); // Month number with leading zero
283
284 // Format the months and years into wp rest api structure like wp-json/wp/v2/doc_category
285 $formatted_months[] = [
286 'id' => "$year-$month_number", // e.g., '2024-05'
287 'name' => "$month_name $year" // e.g., 'May 2024'
288 ];
289 }
290
291 return rest_ensure_response( $formatted_months );
292 }
293
294 /**
295 * Callback function to retrieve 'year_month' field value.
296 *
297 * @param object $post The REST API response object.
298 * @return string The formatted date (e.g., '2024-05').
299 */
300 public function year_month( $post ) {
301 $date_string = isset( $post->post_date ) ? $post->post_date : '';
302
303 $date = new \DateTime( $date_string );
304
305 // Format the date to 'Y-m' (e.g., '2024-05')
306 $formatted_date = $date->format( 'Y-m' );
307
308 return $formatted_date;
309 }
310
311 /**
312 * Filter the docs query by year_month parameters.
313 *
314 * @param array $args The query arguments.
315 * @param WP_REST_Request $request The current REST API request.
316 * @return array Modified query arguments.
317 */
318 public function filter_docs_query( $args, $request ) {
319 // Filter by year_month
320 if ( isset( $request['year_month'] ) ) {
321 $formatted_date = $request['year_month'];
322
323 // Parse the formatted_date to year and month
324 $year = substr( $formatted_date, 0, 4 );
325 $month = substr( $formatted_date, 5, 2 );
326
327 // Add date query arguments
328 $args['date_query'] = [
329 [
330 'year' => $year,
331 'month' => $month
332 ]
333 ];
334 }
335
336 return $args;
337 }
338
339
340 public function get_post_password( $object, $field_name, $request ) {
341 // Suppress unused parameter warnings
342 unset( $field_name, $request );
343
344 if ( current_user_can( 'edit_docs' ) ) {
345 return isset( $object['password'] ) ? $object['password'] : '';
346 } else {
347 return '';
348 }
349 }
350
351 public function search_posts( $request ) {
352 $search_query = sanitize_text_field( $request->get_param( 's' ) );
353 $doc_category = sanitize_text_field( $request->get_param( 'doc_category' ) );
354 $number = (int) $request->get_param( 'per_page' ) ? (int) $request->get_param( 'per_page' ) : 5;
355 $docs_ids = ! empty( $request->get_param( 'doc_ids' ) ) ? explode( ',', $request->get_param( 'doc_ids' ) ) : [];
356 $doc_term_ids = ! empty( $request->get_param( 'doc_categories_ids' ) ) ? explode( ',', $request->get_param( 'doc_categories_ids' ) ) : [];
357 $faq_term_ids = ! empty( $request->get_param( 'faq_categories_ids' ) ) ? explode( ',', $request->get_param( 'faq_categories_ids' ) ) : [];
358 $posts = array();
359 $post_status = ['publish'];
360
361 if( current_user_can( 'read_private_docs' ) ) {
362 array_push($post_status, 'private');
363 }
364
365 // Common query args
366 $common_args = [
367 'post_status' => $post_status,
368 'suppress_filters' => true,
369 'orderby' => 'relevance',
370 ];
371
372 // Exclude password-protected posts unless user has permission
373 if ( ! current_user_can( 'edit_posts' ) ) {
374 $common_args['has_password'] = false;
375 }
376
377 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
378 $common_args['suppress_filters'] = false;
379 $common_args['lang'] = ICL_LANGUAGE_CODE;
380 }
381
382 if ( $search_query ) {
383 $common_args['s'] = $search_query;
384 $common_args['posts_per_page'] = -1;
385 } else {
386 $common_args['posts_per_page'] = $number;
387 }
388
389 // Docs-specific query
390 $docs_args = array_merge(
391 $common_args,
392 [
393 'post_type' => 'docs'
394 ]
395 );
396
397 if ( ! $search_query ) {
398 $docs_args['meta_key'] = '_betterdocs_meta_views';
399 $docs_args['orderby'] = 'meta_value_num';
400 $docs_args['order'] = 'DESC';
401 }
402
403 if ( ! empty( $docs_ids ) ) {
404 unset( $docs_args['meta_key'] );
405 $docs_args['posts_per_page'] = -1;
406 $docs_args['post__in'] = $docs_ids;
407 }
408
409 if ( ! empty( $doc_term_ids ) ) {
410 unset( $docs_args['meta_key'] );
411 $docs_args['posts_per_page'] = -1;
412 $docs_args['tax_query'] = [
413 [
414 'taxonomy' => 'doc_category',
415 'field' => 'term_id',
416 'terms' => $doc_term_ids,
417 'operator' => 'IN',
418 ]
419 ];
420 }
421
422 // Taxonomy filter for docs
423 if ( $doc_category ) {
424 $docs_args['tax_query'] = [
425 [
426 'taxonomy' => 'doc_category',
427 'field' => 'slug',
428 'terms' => $doc_category,
429 'operator' => 'AND',
430 'include_children' => true,
431 ],
432 ];
433 }
434
435 // FAQ-specific query
436 $faq_args = array_merge(
437 $common_args,
438 [
439 'post_type' => 'betterdocs_faq',
440 'orderby' => 'date',
441 'order' => 'DESC',
442 ]
443 );
444
445 if ( ! empty( $faq_term_ids ) ) {
446 $faq_args['posts_per_page'] = -1;
447 $faq_args['tax_query'] = [
448 [
449 'taxonomy' => 'betterdocs_faq_category',
450 'field' => 'term_id',
451 'terms' => $faq_term_ids,
452 'operator' => 'IN',
453 ]
454 ];
455 }
456
457 // Run individual queries
458 $docs_query = betterdocs()->query->get_posts( $docs_args );
459 $faq_query = new WP_Query( $faq_args );
460
461 // Process docs posts
462 if ( $docs_query->have_posts() ) {
463 while ( $docs_query->have_posts() ) {
464 $docs_query->the_post();
465
466 $post_obj = get_post( get_the_ID() );
467
468 // Check if user can access password-protected content
469 $can_access = $this->can_access_password_content( $post_obj, $request );
470
471 // Skip password-protected posts if user cannot access them
472 if ( ! empty( $post_obj->post_password ) && ! $can_access ) {
473 continue;
474 }
475
476 $taxonomies = array();
477 $terms = get_the_terms( get_the_ID(), 'doc_category' );
478 if ( $terms && ! is_wp_error( $terms ) ) {
479 $taxonomies = wp_list_pluck( $terms, 'name' );
480 }
481
482 $posts[] = array(
483 'title' => get_the_title(),
484 'post_type' => get_post_type(),
485 'permalink' => get_the_permalink(),
486 'taxonomies' => implode( ', ', $taxonomies ),
487 );
488 }
489 wp_reset_postdata();
490 }
491
492 // Process FAQ posts with content
493 if ( $faq_query->have_posts() ) {
494 while ( $faq_query->have_posts() ) {
495 $faq_query->the_post();
496
497 $terms = get_the_terms( get_the_ID(), 'betterdocs_faq_category' );
498 $taxonomies = array();
499 if ( $terms && ! is_wp_error( $terms ) ) {
500 $taxonomies = wp_list_pluck( $terms, 'name' );
501 }
502
503 $posts[] = array(
504 'title' => get_the_title(),
505 'content' => get_the_content(), // Include post content for FAQ posts
506 'post_type' => get_post_type(),
507 'permalink' => get_the_permalink(),
508 'taxonomies' => implode( ', ', $taxonomies ),
509 );
510 }
511 wp_reset_postdata();
512 }
513
514 return $posts;
515 }
516
517
518
519 public function search_insert( $request ) {
520 $search_input = sanitize_text_field( $request->get_param( 's' ) );
521 $no_result = sanitize_text_field( $request->get_param( 'no_result' ) );
522
523 return betterdocs()->query->insert_search_keyword( $search_input, $no_result );
524 }
525
526
527 public function get_terms_name_and_slug( $request ) {
528 $default_params = [
529 'taxonomy' => $request->get_param( 'taxonomy' ),
530 'hide_empty' => false,
531 'fields' => 'all',
532 ];
533
534 if ( betterdocs()->settings->get( 'child_category_exclude' ) ) { //disable child terms if this is enabled
535 $default_params['parent'] = 0;
536 }
537
538 // Retrieve all terms for the specified taxonomy, including empty ones
539 $terms = get_terms($default_params);
540
541 // Initialize an empty array to hold the term data
542 $term_data = [];
543
544 // Loop through each term and extract the name and slug
545 $term_data = array_map(
546 function ( $term ) {
547 return [
548 'name' => $term->name,
549 'slug' => $term->slug,
550 'parent' => $term->parent,
551 ];
552 },
553 $terms
554 );
555
556 // Return the array of term data
557 return $term_data;
558 }
559
560 public function get_faq_categories( $request ) {
561 // Suppress unused parameter warning
562 unset( $request );
563 }
564 }
565