PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.2.6
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.2.6
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 3.5.1 All 200 releases
← All changes | includes/REST/Docs.php +22 -156 4.5.5 → 4.2.6 View file →
@@ -1,13 +1,8 @@
1 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.
2 +
3 3 namespace WPDeveloper\BetterDocs\REST;
4 4
5 -if ( ! defined( 'ABSPATH' ) ) {
6 - exit;
7 -}
8 -
9 -
10 5 use Error;
11 6 use WP_Query;
12 7 use WP_REST_Response;
13 8 use WPDeveloper\BetterDocs\Core\BaseAPI;
@@ -19,9 +14,9 @@
19 14
20 15 public function register() {
21 16 $this->get( 'search', [$this, 'search_posts'], [
22 17 'password' => [
23 - 'description' => __( 'The password for password-protected docs.', 'betterdocs' ),
18 + 'description' => __( 'The password for password-protected docs.' ),
24 19 'type' => 'string',
25 20 ],
26 21 ] );
27 22 $this->get( 'search-insert', [$this, 'search_insert'] );
@@ -28,9 +23,9 @@
28 23 $this->get( 'get-terms', [$this, 'get_terms_name_and_slug'] );
29 24 $this->get( 'months-with-posts', [$this, 'get_months_with_posts'] );
30 25 $this->get( 'order_docs', [$this, 'render_betterdocs_order_docs'], [
31 26 'password' => [
32 - 'description' => __( 'The password for password-protected docs.', 'betterdocs' ),
27 + 'description' => __( 'The password for password-protected docs.' ),
33 28 'type' => 'string',
34 29 ],
35 30 ] );
36 31 $this->register_field( 'docs', 'year_month', [
@@ -75,9 +70,9 @@
75 70
76 71 $posts = betterdocs()->query->get_posts( $args, true );
77 72
78 73 if ( ! $posts->have_posts() ) {
79 - wp_reset_postdata();
74 + wp_reset_query();
80 75 }
81 76
82 77 $post_datas = [];
83 78
@@ -97,9 +92,9 @@
97 92 array_push( $post_datas, $post_data );
98 93 endwhile;
99 94
100 95 wp_reset_postdata();
101 - wp_reset_postdata();
96 + wp_reset_query();
102 97
103 98 return $post_datas;
104 99 }
105 100
@@ -267,9 +262,8 @@
267 262 public function get_months_with_posts() {
268 263 global $wpdb;
269 264
270 265 // 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 266 $results = $wpdb->get_results(
273 267 "SELECT DISTINCT YEAR(post_date) AS year, MONTH(post_date) AS month
274 268 FROM $wpdb->posts
275 269 WHERE post_type = 'docs'
@@ -314,9 +308,9 @@
314 308 return $formatted_date;
315 309 }
316 310
317 311 /**
318 - * Filter the docs query by year_month and status parameters.
312 + * Filter the docs query by year_month parameters.
319 313 *
320 314 * @param array $args The query arguments.
321 315 * @param WP_REST_Request $request The current REST API request.
322 316 * @return array Modified query arguments.
@@ -338,14 +332,8 @@
338 332 ]
339 333 ];
340 334 }
341 335
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 336 return $args;
349 337 }
350 338
351 339
@@ -360,17 +348,16 @@
360 348 }
361 349 }
362 350
363 351 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'];
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'];
373 360
374 361 if( current_user_can( 'read_private_docs' ) ) {
375 362 array_push($post_status, 'private');
376 363 }
@@ -377,9 +364,9 @@
377 364
378 365 // Common query args
379 366 $common_args = [
380 367 '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.
368 + 'suppress_filters' => true,
382 369 'orderby' => 'relevance',
383 370 ];
384 371
385 372 // Exclude password-protected posts unless user has permission
@@ -386,31 +373,16 @@
386 373 if ( ! current_user_can( 'edit_posts' ) ) {
387 374 $common_args['has_password'] = false;
388 375 }
389 376
390 - // Handle WPML multilingual search
391 377 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 - }
378 + $common_args['suppress_filters'] = false;
379 + $common_args['lang'] = ICL_LANGUAGE_CODE;
403 380 }
404 381
405 382 if ( $search_query ) {
406 383 $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;
384 + $common_args['posts_per_page'] = -1;
413 385 } else {
414 386 $common_args['posts_per_page'] = $number;
415 387 }
416 388
@@ -422,18 +394,12 @@
422 394 ]
423 395 );
424 396
425 397 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 398 $docs_args['meta_key'] = '_betterdocs_meta_views';
432 399 $docs_args['orderby'] = 'meta_value_num';
433 400 $docs_args['order'] = 'DESC';
434 401 }
435 - }
436 402
437 403 if ( ! empty( $docs_ids ) ) {
438 404 unset( $docs_args['meta_key'] );
439 405 $docs_args['posts_per_page'] = -1;
@@ -465,15 +431,8 @@
465 431 ],
466 432 ];
467 433 }
468 434
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 435 // FAQ-specific query
477 436 $faq_args = array_merge(
478 437 $common_args,
479 438 [
@@ -494,12 +453,12 @@
494 453 ]
495 454 ];
496 455 }
497 456
457 + // Run individual queries
498 458 $docs_query = betterdocs()->query->get_posts( $docs_args );
459 + $faq_query = new WP_Query( $faq_args );
499 460
500 - $faq_query = new WP_Query( $faq_args );
501 -
502 461 // Process docs posts
503 462 if ( $docs_query->have_posts() ) {
504 463 while ( $docs_query->have_posts() ) {
505 464 $docs_query->the_post();
@@ -519,91 +478,12 @@
519 478 if ( $terms && ! is_wp_error( $terms ) ) {
520 479 $taxonomies = wp_list_pluck( $terms, 'name' );
521 480 }
522 481
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 482 $posts[] = array(
603 - 'title' => $title,
483 + 'title' => get_the_title(),
604 484 'post_type' => get_post_type(),
605 - 'permalink' => $permalink,
485 + 'permalink' => get_the_permalink(),
606 486 'taxonomies' => implode( ', ', $taxonomies ),
607 487 );
608 488 }
609 489 wp_reset_postdata();
@@ -652,22 +532,8 @@
652 532 ];
653 533
654 534 if ( betterdocs()->settings->get( 'child_category_exclude' ) ) { //disable child terms if this is enabled
655 535 $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 536 }
671 537
672 538 // Retrieve all terms for the specified taxonomy, including empty ones
673 539 $terms = get_terms($default_params);