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/Utils/Helper.php +23 -468 4.6.14.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 }
@@ -51,41 +23,8 @@
51 23
52 24 return is_plugin_active( $plugin_basename );
53 25 }
54 26
55 - /**
56 - * Whether an SEO plugin already emits FAQPage schema on the current page.
57 - *
58 - * True only when Yoast or Rank Math is active AND its FAQ block is present
59 - * in the post's content, so BetterDocs can skip its own FAQPage JSON-LD and
60 - * avoid duplicate structured data. Defaults to the queried object when no
61 - * post is given.
62 - *
63 - * @param int|\WP_Post|null $post
64 - * @return bool
65 - */
66 - public static function seo_plugin_outputs_faq_schema( $post = null ) {
67 - if ( null === $post ) {
68 - $post = get_queried_object();
69 - }
70 -
71 - $post = get_post( $post );
72 - if ( ! $post instanceof \WP_Post ) {
73 - return false;
74 - }
75 -
76 - if ( self::is_plugin_active( 'wordpress-seo/wp-seo.php' ) && has_block( 'yoast/faq-block', $post ) ) {
77 - return true;
78 - }
79 -
80 - if ( self::is_plugin_active( 'seo-by-rank-math/rank-math.php' ) && has_block( 'rank-math/faq-block', $post ) ) {
81 - return true;
82 - }
83 -
84 - // Extension seam for Pro / other SEO integrations.
85 - return (bool) apply_filters( 'betterdocs_seo_plugin_outputs_faq_schema', false, $post );
86 - }
87 -
88 27 public static function get_tax( $tax = '' ) {
89 28 global $wp_query;
90 29
91 30 if ( is_tax( 'knowledge_base' ) ) {
@@ -144,14 +83,10 @@
144 83 * @return string
145 84 */
146 85 public static function admin_tab() {
147 86 $admin_ui = 'grid';
148 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin UI selection, no state change.
149 - $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
150 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin UI selection, no state change.
151 - $mode = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : '';
152 - if ( $page === 'betterdocs-admin' && ! empty( $mode ) ) {
153 - $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
154 89 }
155 90
156 91 return $admin_ui;
157 92 }
@@ -312,9 +247,9 @@
312 247 if ( is_admin() ) {
313 248 // Allow language filtering for REST API requests that are frontend-facing
314 249 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
315 250 // Check if this is a frontend REST request (not admin)
316 - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
251 + $request_uri = $_SERVER['REQUEST_URI'] ?? '';
317 252 // Don't filter admin REST requests for glossaries management
318 253 if ( strpos( $request_uri, '/wp/v2/glossaries' ) !== false ) {
319 254 return false; // Don't filter admin glossaries management
320 255 }
@@ -335,33 +270,15 @@
335 270 */
336 271 public static function get_current_admin_language() {
337 272 $current_language = null;
338 273
339 - // Explicit language passed by the admin client takes priority.
340 - // Covers AJAX (POST) and REST/admin requests (GET) where WPML may
341 - // otherwise resolve to the site's default language instead of the
342 - // admin UI language.
343 - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only UI language hint, sanitized; not a state-changing form submission.
344 - if ( isset( $_POST['lang'] ) && ! empty( $_POST['lang'] ) ) {
345 - return self::sanitize_language_code( wp_unslash( $_POST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- see note above.
346 - }
347 -
348 - // Limit GET handling to admin/REST contexts so a frontend ?lang= switch
349 - // doesn't hijack admin meta-key resolution.
350 - if ( isset( $_GET['lang'] ) && ! empty( $_GET['lang'] )
351 - && ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) ) {
352 - return self::sanitize_language_code( wp_unslash( $_GET['lang'] ) );
353 - }
354 -
355 274 // WPML Support - Admin language detection
356 275 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
357 276 global $sitepress;
358 277 if ( $sitepress && $sitepress->is_setup_complete() ) {
359 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
360 - $tag_id = isset( $_GET['tag_ID'] ) ? (int) $_GET['tag_ID'] : 0;
361 278 // For term editing, check if we have a specific term language
362 - if ( $tag_id && function_exists( 'wpml_get_language_information' ) ) {
363 - $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'] );
364 281 if ( ! is_wp_error( $term_info ) && $term_info && isset( $term_info['language_code'] ) ) {
365 282 $current_language = $term_info['language_code'];
366 283 }
367 284
@@ -367,18 +284,12 @@
367 284
368 285 }
369 286
370 287 // Check for language parameter in URL
371 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
372 288 if ( ! $current_language && isset( $_GET['lang'] ) ) {
373 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
289 + $current_language = sanitize_text_field( $_GET['lang'] );
374 290 }
375 291
376 - // Check WPML admin language cookie (persists during AJAX)
377 - if ( ! $current_language && isset( $_COOKIE['_icl_current_admin_language'] ) ) {
378 - $current_language = sanitize_text_field( wp_unslash( $_COOKIE['_icl_current_admin_language'] ) );
379 - }
380 -
381 292 // Fallback to admin language or current language
382 293 if ( ! $current_language ) {
383 294 $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
384 295 }
@@ -385,13 +296,11 @@
385 296 }
386 297 }
387 298 // Polylang Support - Admin language detection
388 299 elseif ( function_exists( 'pll_current_language' ) ) {
389 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
390 - $tag_id = isset( $_GET['tag_ID'] ) ? (int) $_GET['tag_ID'] : 0;
391 300 // For term editing, get language from term ID
392 - if ( $tag_id && function_exists( 'pll_get_term_language' ) ) {
393 - $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'] );
394 303 if ( $term_lang ) {
395 304 $current_language = $term_lang;
396 305 }
397 306 }
@@ -396,11 +305,10 @@
396 305 }
397 306 }
398 307
399 308 // Check for language parameter in URL
400 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
401 309 if ( ! $current_language && isset( $_GET['lang'] ) ) {
402 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
310 + $current_language = sanitize_text_field( $_GET['lang'] );
403 311 }
404 312
405 313 // Fallback to current admin language
406 314 if ( ! $current_language ) {
@@ -417,32 +325,12 @@
417 325 elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
418 326 $current_language = trp_get_current_language();
419 327 }
420 328
421 - return self::sanitize_language_code( $current_language );
329 + return $current_language;
422 330 }
423 331
424 332 /**
425 - * Normalize a language code to the character set real language codes use
426 - * (`en`, `en_US`, `zh-Hans`). Values reach this from `?lang=`, `$_POST['lang']`
427 - * and the WPML admin cookie, and `sanitize_text_field()` leaves quotes intact —
428 - * so anything used to build a meta key or SQL fragment must be narrowed here.
429 - * Defense in depth: callers that reach SQL must still bind their values.
430 - *
431 - * @param string|null $language Raw language code.
432 - * @return string|null Normalized code, or null when nothing usable remains.
433 - */
434 - private static function sanitize_language_code( $language ) {
435 - if ( ! is_string( $language ) || '' === $language ) {
436 - return null;
437 - }
438 -
439 - $language = preg_replace( '/[^A-Za-z0-9_-]/', '', $language );
440 -
441 - return '' !== $language ? $language : null;
442 - }
443 -
444 - /**
445 333 * Generate language-specific meta key for category ordering
446 334 * Always falls back to base key if language-specific key doesn't exist
447 335 *
448 336 * @param string $base_key The base meta key (e.g., 'doc_category_order')
@@ -470,37 +358,8 @@
470 358 return $base_key;
471 359 }
472 360
473 361 /**
474 - * Get the meta key to write to.
475 - *
476 - * Unlike `get_meta_key_with_fallback`, this never falls back to the base
477 - * key when the language-specific key is empty — that fallback is what
478 - * caused secondary-language drag-and-drop saves to clobber the base meta
479 - * (and on WPML setups that copy term meta from the original language,
480 - * the next read would re-overwrite it from the primary language).
481 - *
482 - * @param string $base_key The base meta key.
483 - * @param string|null $language Language code, auto-detected when null.
484 - * @return string Language-specific key when multilingual + language known, else base.
485 - */
486 - public static function get_meta_key_for_save( $base_key, $language = null ) {
487 - if ( ! self::is_multilingual_active() ) {
488 - return $base_key;
489 - }
490 -
491 - if ( $language === null ) {
492 - $language = self::get_current_admin_language();
493 - }
494 -
495 - if ( ! $language ) {
496 - return $base_key;
497 - }
498 -
499 - return $base_key . '_' . $language;
500 - }
501 -
502 - /**
503 362 * Get the appropriate meta key with fallback logic
504 363 * This function checks if language-specific meta exists, if not falls back to base key
505 364 *
506 365 * @param string $base_key The base meta key
@@ -537,9 +396,8 @@
537 396 }
538 397
539 398 // For queries without specific term ID, we need to check if ANY terms have language-specific meta
540 399 global $wpdb;
541 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live multilingual meta-key resolution; result varies per active language.
542 400 $has_lang_meta = $wpdb->get_var( $wpdb->prepare(
543 401 "SELECT COUNT(*) FROM {$wpdb->termmeta} tm
544 402 INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
545 403 WHERE tm.meta_key = %s AND tt.taxonomy = 'doc_category' AND tm.meta_value != ''",
@@ -567,9 +425,8 @@
567 425
568 426 global $wpdb;
569 427
570 428 // Get all terms with the base meta key
571 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot multilingual migration; cache would be stale immediately after writes.
572 429 $terms_with_order = $wpdb->get_results( $wpdb->prepare(
573 430 "SELECT tm.term_id, tm.meta_value, t.slug
574 431 FROM {$wpdb->termmeta} tm
575 432 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
@@ -624,9 +481,8 @@
624 481
625 482 global $wpdb;
626 483
627 484 // Get all terms with the base meta key for document ordering
628 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot multilingual migration; cache would be stale immediately after writes.
629 485 $terms_with_docs_order = $wpdb->get_results( $wpdb->prepare(
630 486 "SELECT tm.term_id, tm.meta_value, t.slug
631 487 FROM {$wpdb->termmeta} tm
632 488 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
@@ -703,300 +559,16 @@
703 559
704 560 return $languages;
705 561 }
706 562
707 - /**
708 - * Rich list of active site languages for the React admin language bar.
709 - *
710 - * @return array<int,array{code:string,label:string,native:string,flag:string}>
711 - * Empty when no supported multilingual plugin is active.
712 - */
713 - public static function get_admin_languages() {
714 - $languages = [];
715 -
716 - // WPML
717 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
718 - global $sitepress;
719 - if ( $sitepress && $sitepress->is_setup_complete() ) {
720 - $active = $sitepress->get_active_languages();
721 - if ( is_array( $active ) ) {
722 - foreach ( $active as $code => $lang ) {
723 - $languages[] = [
724 - 'code' => $code,
725 - 'label' => isset( $lang['english_name'] ) ? $lang['english_name'] : $code,
726 - 'native' => isset( $lang['native_name'] ) ? $lang['native_name'] : ( isset( $lang['display_name'] ) ? $lang['display_name'] : $code ),
727 - 'flag' => isset( $lang['country_flag_url'] ) ? $lang['country_flag_url'] : '',
728 - ];
729 - }
730 - }
731 - }
732 - }
733 - // Polylang
734 - elseif ( function_exists( 'pll_languages_list' ) ) {
735 - $list = pll_languages_list( [ 'fields' => '' ] ); // full PLL_Language objects
736 - if ( is_array( $list ) ) {
737 - foreach ( $list as $lang ) {
738 - if ( ! is_object( $lang ) ) {
739 - continue;
740 - }
741 - $languages[] = [
742 - 'code' => isset( $lang->slug ) ? $lang->slug : '',
743 - 'label' => isset( $lang->name ) ? $lang->name : ( isset( $lang->slug ) ? $lang->slug : '' ),
744 - 'native' => isset( $lang->name ) ? $lang->name : '',
745 - 'flag' => isset( $lang->flag_url ) ? $lang->flag_url : '',
746 - ];
747 - }
748 - }
749 - }
750 -
751 - return $languages;
752 - }
753 -
754 - /**
755 - * Read a term's language code via the active multilingual plugin.
756 - *
757 - * @param \WP_Term $term
758 - * @return string Language code, or '' when unavailable.
759 - */
760 - public static function get_term_language( $term ) {
761 - if ( ! is_object( $term ) || empty( $term->term_id ) ) {
762 - return '';
763 - }
764 -
765 - // Polylang — takes the term_id.
766 - if ( function_exists( 'pll_get_term_language' ) ) {
767 - $lang = pll_get_term_language( $term->term_id, 'slug' );
768 - return $lang ? $lang : '';
769 - }
770 -
771 - // WPML — element_id is the term_taxonomy_id (NOT the term_id); WPML
772 - // normalizes the element_type to `tax_<taxonomy>` internally.
773 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
774 - $lang = apply_filters( 'wpml_element_language_code', null, [
775 - 'element_id' => $term->term_taxonomy_id,
776 - 'element_type' => isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category',
777 - ] );
778 - return $lang ? $lang : '';
779 - }
780 -
781 - return '';
782 - }
783 -
784 - /**
785 - * Stamp a term's language via the active multilingual plugin. Standalone
786 - * assignment only — it sets/re-stamps the term's own language and does not
787 - * link it into an existing translation group.
788 - *
789 - * @param \WP_Term $term
790 - * @param string $lang_code
791 - */
792 - public static function set_term_language( $term, $lang_code ) {
793 - $lang_code = sanitize_text_field( (string) $lang_code );
794 - if ( $lang_code === '' || ! is_object( $term ) || empty( $term->term_id ) ) {
795 - return;
796 - }
797 -
798 - // Polylang
799 - if ( function_exists( 'pll_set_term_language' ) ) {
800 - pll_set_term_language( $term->term_id, $lang_code );
801 - return;
802 - }
803 -
804 - // WPML — element_id is the term_taxonomy_id; element_type is tax_<taxonomy>;
805 - // trid=null sets it as a standalone original in the chosen language.
806 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
807 - $taxonomy = isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category';
808 - do_action( 'wpml_set_element_language_details', [
809 - 'element_id' => $term->term_taxonomy_id,
810 - 'element_type' => 'tax_' . $taxonomy,
811 - 'trid' => null,
812 - 'language_code' => $lang_code,
813 - 'source_language_code' => null,
814 - ] );
815 - }
816 - }
817 -
818 - /**
819 - * The site's default language code, or '' when no multilingual plugin is active.
820 - */
821 - public static function get_default_language() {
822 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
823 - global $sitepress;
824 - if ( $sitepress ) {
825 - return (string) $sitepress->get_default_language();
826 - }
827 - }
828 - if ( function_exists( 'pll_default_language' ) ) {
829 - return (string) pll_default_language( 'slug' );
830 - }
831 - return '';
832 - }
833 -
834 - /**
835 - * All terms in a term's translation group, keyed by language code.
836 - *
837 - * @param \WP_Term $term
838 - * @return array<string,array{term_id:int,name:string}>
839 - */
840 - public static function get_term_translations( $term ) {
841 - if ( ! is_object( $term ) || empty( $term->term_id ) ) {
842 - return [];
843 - }
844 - $taxonomy = isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category';
845 - $out = [];
846 -
847 - // Polylang
848 - if ( function_exists( 'pll_get_term_translations' ) ) {
849 - $group = pll_get_term_translations( $term->term_id ); // [lang => term_id]
850 - if ( is_array( $group ) ) {
851 - foreach ( $group as $lang => $tid ) {
852 - $t = get_term( (int) $tid, $taxonomy );
853 - if ( $t && ! is_wp_error( $t ) ) {
854 - $out[ $lang ] = [ 'term_id' => (int) $tid, 'name' => $t->name ];
855 - }
856 - }
857 - }
858 - return $out;
859 - }
860 -
861 - // WPML
862 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
863 - $el_type = 'tax_' . $taxonomy;
864 - $trid = apply_filters( 'wpml_element_trid', null, $term->term_taxonomy_id, $el_type );
865 - if ( ! $trid ) {
866 - return $out;
867 - }
868 - $translations = apply_filters( 'wpml_get_element_translations', null, $trid, $el_type );
869 - if ( is_array( $translations ) ) {
870 - foreach ( $translations as $lang => $tr ) {
871 - $tid = isset( $tr->term_id ) ? (int) $tr->term_id : 0;
872 - if ( ! $tid ) {
873 - continue;
874 - }
875 - $t = get_term( $tid, $taxonomy );
876 - $out[ $lang ] = [
877 - 'term_id' => $tid,
878 - 'name' => ( $t && ! is_wp_error( $t ) ) ? $t->name : ( isset( $tr->name ) ? $tr->name : '' ),
879 - ];
880 - }
881 - }
882 - }
883 -
884 - return $out;
885 - }
886 -
887 - /**
888 - * Candidate source terms for the "This is a translation of" dropdown — terms in
889 - * $source_lang (default language) that aren't yet translated into $target_lang.
890 - *
891 - * @return array<int,array{term_id:int,name:string}>
892 - */
893 - public static function get_translation_candidates( $taxonomy, $target_lang, $source_lang ) {
894 - $candidates = [];
895 - $target_lang = sanitize_text_field( (string) $target_lang );
896 - $source_lang = sanitize_text_field( (string) $source_lang );
897 - if ( $taxonomy === '' || $source_lang === '' ) {
898 - return $candidates;
899 - }
900 -
901 - // WPML
902 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
903 - global $sitepress;
904 - if ( $sitepress && method_exists( $sitepress, 'get_elements_without_translations' ) ) {
905 - $ttids = $sitepress->get_elements_without_translations( 'tax_' . $taxonomy, $target_lang, $source_lang );
906 - foreach ( (array) $ttids as $ttid ) {
907 - $t = get_term_by( 'term_taxonomy_id', (int) $ttid, $taxonomy );
908 - if ( $t && ! is_wp_error( $t ) ) {
909 - $candidates[] = [ 'term_id' => (int) $t->term_id, 'name' => $t->name ];
910 - }
911 - }
912 - }
913 - return $candidates;
914 - }
915 -
916 - // Polylang — source-lang terms whose group lacks the target language.
917 - if ( function_exists( 'pll_get_term_translations' ) && function_exists( 'pll_get_term_language' ) ) {
918 - $terms = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => false, 'lang' => $source_lang ] );
919 - foreach ( (array) $terms as $t ) {
920 - if ( is_wp_error( $t ) ) {
921 - continue;
922 - }
923 - $group = pll_get_term_translations( $t->term_id );
924 - if ( ! isset( $group[ $target_lang ] ) ) {
925 - $candidates[] = [ 'term_id' => (int) $t->term_id, 'name' => $t->name ];
926 - }
927 - }
928 - }
929 -
930 - return $candidates;
931 - }
932 -
933 - /**
934 - * Set a term's language and (optionally) link it into the translation group of
935 - * $translation_of_term_id. Empty $translation_of_term_id = standalone.
936 - *
937 - * @param \WP_Term $term
938 - * @param string $lang_code
939 - * @param int $translation_of_term_id
940 - */
941 - public static function link_term_translation( $term, $lang_code, $translation_of_term_id = 0 ) {
942 - $lang_code = sanitize_text_field( (string) $lang_code );
943 - if ( $lang_code === '' || ! is_object( $term ) || empty( $term->term_id ) ) {
944 - return;
945 - }
946 - $taxonomy = isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category';
947 - $translation_of_term_id = (int) $translation_of_term_id;
948 -
949 - // Polylang
950 - if ( function_exists( 'pll_set_term_language' ) ) {
951 - pll_set_term_language( $term->term_id, $lang_code );
952 - if ( $translation_of_term_id && function_exists( 'pll_save_term_translations' ) ) {
953 - $group = function_exists( 'pll_get_term_translations' )
954 - ? (array) pll_get_term_translations( $translation_of_term_id )
955 - : [];
956 - $group[ $lang_code ] = $term->term_id;
957 - pll_save_term_translations( $group );
958 - }
959 - return;
960 - }
961 -
962 - // WPML
963 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
964 - $el_type = 'tax_' . $taxonomy;
965 - $trid = null;
966 - $src = null;
967 -
968 - if ( $translation_of_term_id ) {
969 - $source = get_term( $translation_of_term_id, $taxonomy );
970 - if ( $source && ! is_wp_error( $source ) ) {
971 - $trid = apply_filters( 'wpml_element_trid', null, $source->term_taxonomy_id, $el_type );
972 - $src = self::get_term_language( $source );
973 - }
974 - }
975 -
976 - do_action( 'wpml_set_element_language_details', [
977 - 'element_id' => $term->term_taxonomy_id,
978 - 'element_type' => $el_type,
979 - 'trid' => $trid,
980 - 'language_code' => $lang_code,
981 - 'source_language_code' => $src,
982 - ] );
983 - }
984 - }
985 -
986 - public static function get_current_letter_docs( $current_letter, $limit = 0 ) {
563 + public static function get_current_letter_docs( $current_letter, $limit = '' ) {
987 564 global $wpdb;
988 565
989 - $limit = absint( $limit );
990 - $limit_sql = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
991 -
992 566 // Check if the encyclopedia_prefix parameter is set
993 567
994 568 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
995 569 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
996 570 $encyclopedia_root_slug = betterdocs()->settings->get( 'encyclopedia_root_slug', 'encyclopdia' );
997 - // Sanitize values that may be interpolated into raw SQL fragments below.
998 - $encyclopedia_root_slug = sanitize_title( $encyclopedia_root_slug );
999 571
1000 572 // if($enable_glossaries && $encyclopeia_suorce === 'glossaries'){
1001 573 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
1002 574 $lang_join = '';
@@ -1004,10 +576,8 @@
1004 576
1005 577 // Add language filtering if multilingual plugin is active and we should apply filtering
1006 578 $current_language = self::get_current_language();
1007 579 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
1008 - // Restrict language code to a safe character set before SQL interpolation.
1009 - $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
1010 580 // For WPML, use icl_translations table
1011 581 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1012 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'";
1013 583 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
@@ -1046,9 +616,9 @@
1046 616 GROUP BY
1047 617 t.term_id
1048 618 ORDER BY
1049 619 t.name ASC
1050 - $limit_sql
620 + $limit
1051 621 ";
1052 622 } else {
1053 623 $lang_join = '';
1054 624 $lang_where = '';
@@ -1055,10 +625,8 @@
1055 625
1056 626 // Add language filtering for docs if multilingual plugin is active and we should apply filtering
1057 627 $current_language = self::get_current_language();
1058 628 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
1059 - // Restrict language code to a safe character set before SQL interpolation.
1060 - $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
1061 629 // For WPML, use icl_translations table
1062 630 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1063 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'";
1064 632 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
@@ -1078,9 +646,9 @@
1078 646 AND post_status = 'publish'
1079 647 AND SUBSTRING(post_title, 1, 1) = %s
1080 648 $lang_where
1081 649 ORDER BY post_date DESC
1082 - $limit_sql
650 + $limit
1083 651 ";
1084 652 }
1085 653
1086 654 $current_letter_docs = $wpdb->get_results( $wpdb->prepare( $query, $current_letter ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -1098,9 +666,9 @@
1098 666 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
1099 667 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
1100 668
1101 669 foreach ( $letters as $letter ) {
1102 - $posts = self::get_current_letter_docs( $letter, $limit );
670 + $posts = self::get_current_letter_docs( $letter, "LIMIT $limit" );
1103 671
1104 672 if ( is_array( $posts ) && ! empty( $posts ) ) {
1105 673 foreach ( $posts as $post ) {
1106 674 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
@@ -1106,24 +674,14 @@
1106 674 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
1107 675 $glossary_term_description = $description['glossary_term_description'] ?? '';
1108 676
1109 677 // Remove any <p> tags or other unwanted HTML tags
1110 - $glossary_term_description = wp_strip_all_tags( $glossary_term_description );
1111 - $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'] ?? '' );
1112 680
1113 681 // Prepare post data
1114 682 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
1115 683 // For glossaries
1116 - $permalink = '';
1117 -
1118 - if ( isset( $post['slug'] ) ) {
1119 - $term_link = get_term_link( $post['slug'], 'glossaries' );
1120 -
1121 - if ( ! is_wp_error( $term_link ) ) {
1122 - $permalink = $term_link;
1123 - }
1124 - }
1125 -
1126 684 $post_data = [
1127 685 'id' => $post['term_id'] ?? '',
1128 686 'post_title' => $post['post_title'] ?? '',
1129 687 'post_excerpt' => ! empty( $post_excerpt )
@@ -1129,10 +687,10 @@
1129 687 'post_excerpt' => ! empty( $post_excerpt )
1130 688 ? $post_excerpt
1131 689 : ( ! empty( $glossary_term_description )
1132 690 ? self::get_custom_excerpt( $glossary_term_description, 15 )
1133 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ) ),
1134 - 'permalink' => $permalink,
691 + : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ) ),
692 + 'permalink' => isset( $post['slug'] ) ? get_term_link( $post['slug'], 'glossaries' ) : ''
1135 693 ];
1136 694 } else {
1137 695 // For docs
1138 696 $post_data = [
@@ -1139,9 +697,9 @@
1139 697 'id' => $post['ID'] ?? '',
1140 698 'post_title' => $post['post_title'] ?? '',
1141 699 'post_excerpt' => ! empty( $post_excerpt )
1142 700 ? $post_excerpt
1143 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ),
701 + : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ),
1144 702 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
1145 703 ];
1146 704 }
1147 705
@@ -1161,10 +719,8 @@
1161 719
1162 720 // Add language filtering if multilingual plugin is active and we should apply filtering
1163 721 $current_language = self::get_current_language();
1164 722 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
1165 - // Restrict language code to a safe character set before SQL interpolation.
1166 - $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
1167 723 // For WPML, use icl_translations table
1168 724 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1169 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'";
1170 726 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
@@ -1290,16 +846,15 @@
1290 846 ] );
1291 847 return isset( $terms[0] ) ? $terms[0] : [];
1292 848 }
1293 849
1294 - public static function delete_specific_faq_posts_by_faq_category( $term_id, $taxonomy = 'betterdocs_faq_category' ) {
1295 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- targeted bulk delete by FAQ category; tax filter is required.
850 + public static function delete_specific_faq_posts_by_faq_category( $term_id ) {
1296 851 $args = [
1297 852 'post_type' => 'betterdocs_faq',
1298 853 'posts_per_page' => -1,
1299 854 'tax_query' => [
1300 855 [
1301 - 'taxonomy' => $taxonomy,
856 + 'taxonomy' => 'betterdocs_faq_category',
1302 857 'field' => 'id',
1303 858 'terms' => $term_id,
1304 859 'operator' => 'IN'
1305 860 ]
@@ -1450,9 +1005,9 @@
1450 1005 * @return int
1451 1006 */
1452 1007 public static function get_max_doc_category_order_from_term_meta() {
1453 1008 global $wpdb;
1454 - $sql = $wpdb->prepare( "SELECT MAX(CAST(meta_value AS UNSIGNED)) AS max FROM {$wpdb->termmeta} WHERE meta_key = %s ", 'doc_category_order' );
1455 - $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);
1456 1011 return $result;
1457 1012 }
1458 1013 }