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.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
← All changes | includes/Utils/Helper.php +20 -133 4.5.64.2.6 View file →
@@ -1,41 +1,13 @@
1 1 <?php
2 2
3 3 namespace WPDeveloper\BetterDocs\Utils;
4 4
5 -// Helper utilities mix per-language URL detection (read-only $_GET reads),
6 -// dynamic alphabet-letter / glossary queries composed via $wpdb->prepare,
7 -// and meta-key term lookups that are core to BetterDocs functionality.
8 -// phpcs:disable WordPress.Security.NonceVerification.Recommended
9 -// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
10 -// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
11 -// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
12 -// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
13 -// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
14 -// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
15 -// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
16 -// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
17 -
18 5 use function BetterLinksPro\Dependencies\GuzzleHttp\json_decode;
19 6 use function WPML\PHP\Logger\error;
20 7
21 8 class Helper extends Base {
22 9
23 - /**
24 - * Mask an API key for safe display: first 3 chars + 8 asterisks + last 4 chars.
25 - * Fixed asterisk count avoids leaking the real key length.
26 - */
27 - public static function mask_api_key( $key ) {
28 - if ( ! is_string( $key ) || $key === '' ) {
29 - return '';
30 - }
31 - $key = trim( $key );
32 - if ( strlen( $key ) < 8 ) {
33 - return str_repeat( '*', strlen( $key ) );
34 - }
35 - return substr( $key, 0, 3 ) . str_repeat( '*', 8 ) . substr( $key, -4 );
36 - }
37 -
38 10 public static function get_plugins( $plugin_basename = null ) {
39 11 if ( ! function_exists( 'get_plugins' ) ) {
40 12 include_once ABSPATH . 'wp-admin/includes/plugin.php';
41 13 }
@@ -111,14 +83,10 @@
111 83 * @return string
112 84 */
113 85 public static function admin_tab() {
114 86 $admin_ui = 'grid';
115 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin UI selection, no state change.
116 - $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
117 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin UI selection, no state change.
118 - $mode = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : '';
119 - if ( $page === 'betterdocs-admin' && ! empty( $mode ) ) {
120 - $admin_ui = $mode === 'grid' ? 'grid' : 'list';
87 + if ( isset( $_GET['mode'], $_GET['page'] ) && $_GET['page'] === 'betterdocs-admin' && ! empty( $_GET['mode'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
88 + $admin_ui = $_GET['mode'] === 'grid' ? 'grid' : 'list'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
121 89 }
122 90
123 91 return $admin_ui;
124 92 }
@@ -279,9 +247,9 @@
279 247 if ( is_admin() ) {
280 248 // Allow language filtering for REST API requests that are frontend-facing
281 249 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
282 250 // Check if this is a frontend REST request (not admin)
283 - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
251 + $request_uri = $_SERVER['REQUEST_URI'] ?? '';
284 252 // Don't filter admin REST requests for glossaries management
285 253 if ( strpos( $request_uri, '/wp/v2/glossaries' ) !== false ) {
286 254 return false; // Don't filter admin glossaries management
287 255 }
@@ -302,33 +270,15 @@
302 270 */
303 271 public static function get_current_admin_language() {
304 272 $current_language = null;
305 273
306 - // Explicit language passed by the admin client takes priority.
307 - // Covers AJAX (POST) and REST/admin requests (GET) where WPML may
308 - // otherwise resolve to the site's default language instead of the
309 - // admin UI language.
310 - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only UI language hint, sanitized; not a state-changing form submission.
311 - if ( isset( $_POST['lang'] ) && ! empty( $_POST['lang'] ) ) {
312 - return sanitize_text_field( wp_unslash( $_POST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- see note above.
313 - }
314 -
315 - // Limit GET handling to admin/REST contexts so a frontend ?lang= switch
316 - // doesn't hijack admin meta-key resolution.
317 - if ( isset( $_GET['lang'] ) && ! empty( $_GET['lang'] )
318 - && ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) ) {
319 - return sanitize_text_field( wp_unslash( $_GET['lang'] ) );
320 - }
321 -
322 274 // WPML Support - Admin language detection
323 275 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
324 276 global $sitepress;
325 277 if ( $sitepress && $sitepress->is_setup_complete() ) {
326 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
327 - $tag_id = isset( $_GET['tag_ID'] ) ? (int) $_GET['tag_ID'] : 0;
328 278 // For term editing, check if we have a specific term language
329 - if ( $tag_id && function_exists( 'wpml_get_language_information' ) ) {
330 - $term_info = wpml_get_language_information( null, $tag_id );
279 + if ( isset( $_GET['tag_ID'] ) && function_exists( 'wpml_get_language_information' ) ) {
280 + $term_info = wpml_get_language_information( null, (int) $_GET['tag_ID'] );
331 281 if ( ! is_wp_error( $term_info ) && $term_info && isset( $term_info['language_code'] ) ) {
332 282 $current_language = $term_info['language_code'];
333 283 }
334 284
@@ -334,18 +284,12 @@
334 284
335 285 }
336 286
337 287 // Check for language parameter in URL
338 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
339 288 if ( ! $current_language && isset( $_GET['lang'] ) ) {
340 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
289 + $current_language = sanitize_text_field( $_GET['lang'] );
341 290 }
342 291
343 - // Check WPML admin language cookie (persists during AJAX)
344 - if ( ! $current_language && isset( $_COOKIE['_icl_current_admin_language'] ) ) {
345 - $current_language = sanitize_text_field( wp_unslash( $_COOKIE['_icl_current_admin_language'] ) );
346 - }
347 -
348 292 // Fallback to admin language or current language
349 293 if ( ! $current_language ) {
350 294 $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
351 295 }
@@ -352,13 +296,11 @@
352 296 }
353 297 }
354 298 // Polylang Support - Admin language detection
355 299 elseif ( function_exists( 'pll_current_language' ) ) {
356 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
357 - $tag_id = isset( $_GET['tag_ID'] ) ? (int) $_GET['tag_ID'] : 0;
358 300 // For term editing, get language from term ID
359 - if ( $tag_id && function_exists( 'pll_get_term_language' ) ) {
360 - $term_lang = pll_get_term_language( $tag_id );
301 + if ( isset( $_GET['tag_ID'] ) && function_exists( 'pll_get_term_language' ) ) {
302 + $term_lang = pll_get_term_language( (int) $_GET['tag_ID'] );
361 303 if ( $term_lang ) {
362 304 $current_language = $term_lang;
363 305 }
364 306 }
@@ -363,11 +305,10 @@
363 305 }
364 306 }
365 307
366 308 // Check for language parameter in URL
367 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
368 309 if ( ! $current_language && isset( $_GET['lang'] ) ) {
369 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
310 + $current_language = sanitize_text_field( $_GET['lang'] );
370 311 }
371 312
372 313 // Fallback to current admin language
373 314 if ( ! $current_language ) {
@@ -417,37 +358,8 @@
417 358 return $base_key;
418 359 }
419 360
420 361 /**
421 - * Get the meta key to write to.
422 - *
423 - * Unlike `get_meta_key_with_fallback`, this never falls back to the base
424 - * key when the language-specific key is empty — that fallback is what
425 - * caused secondary-language drag-and-drop saves to clobber the base meta
426 - * (and on WPML setups that copy term meta from the original language,
427 - * the next read would re-overwrite it from the primary language).
428 - *
429 - * @param string $base_key The base meta key.
430 - * @param string|null $language Language code, auto-detected when null.
431 - * @return string Language-specific key when multilingual + language known, else base.
432 - */
433 - public static function get_meta_key_for_save( $base_key, $language = null ) {
434 - if ( ! self::is_multilingual_active() ) {
435 - return $base_key;
436 - }
437 -
438 - if ( $language === null ) {
439 - $language = self::get_current_admin_language();
440 - }
441 -
442 - if ( ! $language ) {
443 - return $base_key;
444 - }
445 -
446 - return $base_key . '_' . $language;
447 - }
448 -
449 - /**
450 362 * Get the appropriate meta key with fallback logic
451 363 * This function checks if language-specific meta exists, if not falls back to base key
452 364 *
453 365 * @param string $base_key The base meta key
@@ -484,9 +396,8 @@
484 396 }
485 397
486 398 // For queries without specific term ID, we need to check if ANY terms have language-specific meta
487 399 global $wpdb;
488 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live multilingual meta-key resolution; result varies per active language.
489 400 $has_lang_meta = $wpdb->get_var( $wpdb->prepare(
490 401 "SELECT COUNT(*) FROM {$wpdb->termmeta} tm
491 402 INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
492 403 WHERE tm.meta_key = %s AND tt.taxonomy = 'doc_category' AND tm.meta_value != ''",
@@ -514,9 +425,8 @@
514 425
515 426 global $wpdb;
516 427
517 428 // Get all terms with the base meta key
518 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot multilingual migration; cache would be stale immediately after writes.
519 429 $terms_with_order = $wpdb->get_results( $wpdb->prepare(
520 430 "SELECT tm.term_id, tm.meta_value, t.slug
521 431 FROM {$wpdb->termmeta} tm
522 432 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
@@ -571,9 +481,8 @@
571 481
572 482 global $wpdb;
573 483
574 484 // Get all terms with the base meta key for document ordering
575 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot multilingual migration; cache would be stale immediately after writes.
576 485 $terms_with_docs_order = $wpdb->get_results( $wpdb->prepare(
577 486 "SELECT tm.term_id, tm.meta_value, t.slug
578 487 FROM {$wpdb->termmeta} tm
579 488 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
@@ -650,21 +559,16 @@
650 559
651 560 return $languages;
652 561 }
653 562
654 - public static function get_current_letter_docs( $current_letter, $limit = 0 ) {
563 + public static function get_current_letter_docs( $current_letter, $limit = '' ) {
655 564 global $wpdb;
656 565
657 - $limit = absint( $limit );
658 - $limit_sql = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
659 -
660 566 // Check if the encyclopedia_prefix parameter is set
661 567
662 568 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
663 569 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
664 570 $encyclopedia_root_slug = betterdocs()->settings->get( 'encyclopedia_root_slug', 'encyclopdia' );
665 - // Sanitize values that may be interpolated into raw SQL fragments below.
666 - $encyclopedia_root_slug = sanitize_title( $encyclopedia_root_slug );
667 571
668 572 // if($enable_glossaries && $encyclopeia_suorce === 'glossaries'){
669 573 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
670 574 $lang_join = '';
@@ -672,10 +576,8 @@
672 576
673 577 // Add language filtering if multilingual plugin is active and we should apply filtering
674 578 $current_language = self::get_current_language();
675 579 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
676 - // Restrict language code to a safe character set before SQL interpolation.
677 - $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
678 580 // For WPML, use icl_translations table
679 581 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
680 582 $lang_join = " LEFT JOIN {$wpdb->prefix}icl_translations icl_t ON icl_t.element_id = t.term_id AND icl_t.element_type = 'tax_glossaries'";
681 583 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
@@ -714,9 +616,9 @@
714 616 GROUP BY
715 617 t.term_id
716 618 ORDER BY
717 619 t.name ASC
718 - $limit_sql
620 + $limit
719 621 ";
720 622 } else {
721 623 $lang_join = '';
722 624 $lang_where = '';
@@ -723,10 +625,8 @@
723 625
724 626 // Add language filtering for docs if multilingual plugin is active and we should apply filtering
725 627 $current_language = self::get_current_language();
726 628 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
727 - // Restrict language code to a safe character set before SQL interpolation.
728 - $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
729 629 // For WPML, use icl_translations table
730 630 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
731 631 $lang_join = " LEFT JOIN {$wpdb->prefix}icl_translations icl_t ON icl_t.element_id = {$wpdb->posts}.ID AND icl_t.element_type = 'post_docs'";
732 632 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
@@ -746,9 +646,9 @@
746 646 AND post_status = 'publish'
747 647 AND SUBSTRING(post_title, 1, 1) = %s
748 648 $lang_where
749 649 ORDER BY post_date DESC
750 - $limit_sql
650 + $limit
751 651 ";
752 652 }
753 653
754 654 $current_letter_docs = $wpdb->get_results( $wpdb->prepare( $query, $current_letter ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -766,9 +666,9 @@
766 666 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
767 667 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
768 668
769 669 foreach ( $letters as $letter ) {
770 - $posts = self::get_current_letter_docs( $letter, $limit );
670 + $posts = self::get_current_letter_docs( $letter, "LIMIT $limit" );
771 671
772 672 if ( is_array( $posts ) && ! empty( $posts ) ) {
773 673 foreach ( $posts as $post ) {
774 674 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
@@ -774,24 +674,14 @@
774 674 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
775 675 $glossary_term_description = $description['glossary_term_description'] ?? '';
776 676
777 677 // Remove any <p> tags or other unwanted HTML tags
778 - $glossary_term_description = wp_strip_all_tags( $glossary_term_description );
779 - $post_excerpt = wp_strip_all_tags( $post['post_excerpt'] ?? '' );
678 + $glossary_term_description = strip_tags( $glossary_term_description );
679 + $post_excerpt = strip_tags( $post['post_excerpt'] ?? '' );
780 680
781 681 // Prepare post data
782 682 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
783 683 // For glossaries
784 - $permalink = '';
785 -
786 - if ( isset( $post['slug'] ) ) {
787 - $term_link = get_term_link( $post['slug'], 'glossaries' );
788 -
789 - if ( ! is_wp_error( $term_link ) ) {
790 - $permalink = $term_link;
791 - }
792 - }
793 -
794 684 $post_data = [
795 685 'id' => $post['term_id'] ?? '',
796 686 'post_title' => $post['post_title'] ?? '',
797 687 'post_excerpt' => ! empty( $post_excerpt )
@@ -797,10 +687,10 @@
797 687 'post_excerpt' => ! empty( $post_excerpt )
798 688 ? $post_excerpt
799 689 : ( ! empty( $glossary_term_description )
800 690 ? self::get_custom_excerpt( $glossary_term_description, 15 )
801 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ) ),
802 - 'permalink' => $permalink,
691 + : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ) ),
692 + 'permalink' => isset( $post['slug'] ) ? get_term_link( $post['slug'], 'glossaries' ) : ''
803 693 ];
804 694 } else {
805 695 // For docs
806 696 $post_data = [
@@ -807,9 +697,9 @@
807 697 'id' => $post['ID'] ?? '',
808 698 'post_title' => $post['post_title'] ?? '',
809 699 'post_excerpt' => ! empty( $post_excerpt )
810 700 ? $post_excerpt
811 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ),
701 + : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ),
812 702 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
813 703 ];
814 704 }
815 705
@@ -829,10 +719,8 @@
829 719
830 720 // Add language filtering if multilingual plugin is active and we should apply filtering
831 721 $current_language = self::get_current_language();
832 722 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
833 - // Restrict language code to a safe character set before SQL interpolation.
834 - $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
835 723 // For WPML, use icl_translations table
836 724 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
837 725 $lang_join = " LEFT JOIN {$wpdb->prefix}icl_translations icl_t ON icl_t.element_id = t.term_id AND icl_t.element_type = 'tax_glossaries'";
838 726 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
@@ -959,9 +847,8 @@
959 847 return isset( $terms[0] ) ? $terms[0] : [];
960 848 }
961 849
962 850 public static function delete_specific_faq_posts_by_faq_category( $term_id ) {
963 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- targeted bulk delete by FAQ category; tax filter is required.
964 851 $args = [
965 852 'post_type' => 'betterdocs_faq',
966 853 'posts_per_page' => -1,
967 854 'tax_query' => [
@@ -1118,9 +1005,9 @@
1118 1005 * @return int
1119 1006 */
1120 1007 public static function get_max_doc_category_order_from_term_meta() {
1121 1008 global $wpdb;
1122 - $sql = $wpdb->prepare( "SELECT MAX(CAST(meta_value AS UNSIGNED)) AS max FROM {$wpdb->termmeta} WHERE meta_key = %s ", 'doc_category_order' );
1123 - $result = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is prepared above.
1009 + $sql = $wpdb->prepare( "SELECT MAX(CAST(meta_value AS UNSIGNED)) AS max FROM {$wpdb->prefix}termmeta WHERE meta_key = %s ", 'doc_category_order');
1010 + $result = $wpdb->get_var($sql);
1124 1011 return $result;
1125 1012 }
1126 1013 }