PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.8.9
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.8.9
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 +26 -1056 4.6.13.8.9 View file →
@@ -1,41 +1,12 @@
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 -use function WPML\PHP\Logger\error;
20 6
21 7 class Helper extends Base {
22 8
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 9 public static function get_plugins( $plugin_basename = null ) {
39 10 if ( ! function_exists( 'get_plugins' ) ) {
40 11 include_once ABSPATH . 'wp-admin/includes/plugin.php';
41 12 }
@@ -51,41 +22,8 @@
51 22
52 23 return is_plugin_active( $plugin_basename );
53 24 }
54 25
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 26 public static function get_tax( $tax = '' ) {
89 27 global $wp_query;
90 28
91 29 if ( is_tax( 'knowledge_base' ) ) {
@@ -144,14 +82,10 @@
144 82 * @return string
145 83 */
146 84 public static function admin_tab() {
147 85 $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';
86 + if ( isset( $_GET['mode'], $_GET['page'] ) && $_GET['page'] === 'betterdocs-admin' && ! empty( $_GET['mode'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
87 + $admin_ui = $_GET['mode'] === 'grid' ? 'grid' : 'list'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
154 88 }
155 89
156 90 return $admin_ui;
157 91 }
@@ -252,774 +186,19 @@
252 186 }
253 187 return $excerpt;
254 188 }
255 189
256 - /**
257 - * Get current language from various multilingual plugins
258 - *
259 - * @return string|null Current language code
260 - */
261 - public static function get_current_language() {
262 - $current_language = null;
263 -
264 - // WPML Support
265 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
266 - global $sitepress;
267 - if ( $sitepress && $sitepress->is_setup_complete() ) {
268 - $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
269 - }
270 - }
271 - // Polylang Support
272 - elseif ( function_exists( 'pll_current_language' ) ) {
273 - $current_language = pll_current_language();
274 - }
275 - // qTranslate-X Support
276 - elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
277 - $current_language = qtranxf_getLanguage();
278 - }
279 - // Weglot Support
280 - elseif ( function_exists( 'weglot_get_current_language' ) ) {
281 - $current_language = weglot_get_current_language();
282 - }
283 - // TranslatePress Support
284 - elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
285 - $current_language = trp_get_current_language();
286 - }
287 -
288 - return $current_language;
289 - }
290 -
291 - /**
292 - * Check if any multilingual plugin is active
293 - *
294 - * @return bool
295 - */
296 - public static function is_multilingual_active() {
297 - return is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ||
298 - function_exists( 'pll_current_language' ) ||
299 - function_exists( 'qtranxf_getLanguage' ) ||
300 - function_exists( 'weglot_get_current_language' ) ||
301 - ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) );
302 - }
303 -
304 - /**
305 - * Check if we should apply language filtering
306 - * Only apply on frontend or when specifically requested
307 - *
308 - * @return bool
309 - */
310 - public static function should_apply_language_filtering() {
311 - // Don't apply language filtering in admin context unless it's a frontend request
312 - if ( is_admin() ) {
313 - // Allow language filtering for REST API requests that are frontend-facing
314 - if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
315 - // 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'] ) ) : '';
317 - // Don't filter admin REST requests for glossaries management
318 - if ( strpos( $request_uri, '/wp/v2/glossaries' ) !== false ) {
319 - return false; // Don't filter admin glossaries management
320 - }
321 - }
322 - return false; // Don't filter other admin requests
323 - }
324 -
325 - // Apply filtering on frontend
326 - return true;
327 - }
328 -
329 - /**
330 - * Get current admin language for multilingual sites
331 - * This is specifically for admin context where we need to detect
332 - * the language being used for editing terms/posts
333 - *
334 - * @return string|null Current admin language code
335 - */
336 - public static function get_current_admin_language() {
337 - $current_language = null;
338 -
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 - // WPML Support - Admin language detection
356 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
357 - global $sitepress;
358 - 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 - // 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 );
364 - if ( ! is_wp_error( $term_info ) && $term_info && isset( $term_info['language_code'] ) ) {
365 - $current_language = $term_info['language_code'];
366 - }
367 -
368 - }
369 -
370 - // Check for language parameter in URL
371 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
372 - if ( ! $current_language && isset( $_GET['lang'] ) ) {
373 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
374 - }
375 -
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 - // Fallback to admin language or current language
382 - if ( ! $current_language ) {
383 - $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
384 - }
385 - }
386 - }
387 - // Polylang Support - Admin language detection
388 - 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 - // 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 );
394 - if ( $term_lang ) {
395 - $current_language = $term_lang;
396 - }
397 - }
398 -
399 - // Check for language parameter in URL
400 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
401 - if ( ! $current_language && isset( $_GET['lang'] ) ) {
402 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
403 - }
404 -
405 - // Fallback to current admin language
406 - if ( ! $current_language ) {
407 - $current_language = pll_current_language( 'slug' );
408 - }
409 - }
410 - // Other multilingual plugins
411 - elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
412 - $current_language = qtranxf_getLanguage();
413 - }
414 - elseif ( function_exists( 'weglot_get_current_language' ) ) {
415 - $current_language = weglot_get_current_language();
416 - }
417 - elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
418 - $current_language = trp_get_current_language();
419 - }
420 -
421 - return self::sanitize_language_code( $current_language );
422 - }
423 -
424 - /**
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 - * Generate language-specific meta key for category ordering
446 - * Always falls back to base key if language-specific key doesn't exist
447 - *
448 - * @param string $base_key The base meta key (e.g., 'doc_category_order')
449 - * @param string|null $language Language code, if null will auto-detect
450 - * @return string Language-specific meta key or base key as fallback
451 - */
452 - public static function get_language_specific_meta_key( $base_key, $language = null ) {
453 - // If no multilingual plugin is active, return the base key
454 - if ( ! self::is_multilingual_active() ) {
455 - return $base_key;
456 - }
457 -
458 - // Get current admin language if not provided
459 - if ( $language === null ) {
460 - $language = self::get_current_admin_language();
461 - }
462 -
463 - // If no language detected, return base key for backward compatibility
464 - if ( ! $language ) {
465 - return $base_key;
466 - }
467 -
468 - // Always return base key for now - we'll handle fallback in the query functions
469 - // This ensures compatibility without requiring migration
470 - return $base_key;
471 - }
472 -
473 - /**
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 - * Get the appropriate meta key with fallback logic
504 - * This function checks if language-specific meta exists, if not falls back to base key
505 - *
506 - * @param string $base_key The base meta key
507 - * @param int $term_id The term ID to check
508 - * @param string|null $language Language code
509 - * @return string The meta key to use
510 - */
511 - public static function get_meta_key_with_fallback( $base_key, $term_id = null, $language = null ) {
512 - // If no multilingual plugin is active, return the base key
513 - if ( ! self::is_multilingual_active() ) {
514 - return $base_key;
515 - }
516 -
517 - // Get current admin language if not provided
518 - if ( $language === null ) {
519 - $language = self::get_current_admin_language();
520 - }
521 -
522 - // If no language detected, return base key
523 - if ( ! $language ) {
524 - return $base_key;
525 - }
526 -
527 - $lang_meta_key = $base_key . '_' . $language;
528 -
529 - // If we have a specific term ID, check if language-specific meta exists
530 - if ( $term_id ) {
531 - $lang_value = get_term_meta( $term_id, $lang_meta_key, true );
532 - if ( ! empty( $lang_value ) ) {
533 - return $lang_meta_key;
534 - }
535 - // Fall back to base key if language-specific doesn't exist
536 - return $base_key;
537 - }
538 -
539 - // For queries without specific term ID, we need to check if ANY terms have language-specific meta
190 + public static function get_current_letter_docs( $current_letter, $limit = '' ) {
540 191 global $wpdb;
541 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live multilingual meta-key resolution; result varies per active language.
542 - $has_lang_meta = $wpdb->get_var( $wpdb->prepare(
543 - "SELECT COUNT(*) FROM {$wpdb->termmeta} tm
544 - INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
545 - WHERE tm.meta_key = %s AND tt.taxonomy = 'doc_category' AND tm.meta_value != ''",
546 - $lang_meta_key
547 - ) );
548 192
549 - // If language-specific meta exists for some terms, use it (terms without it will have empty values)
550 - // Otherwise, fall back to base key
551 - return $has_lang_meta > 0 ? $lang_meta_key : $base_key;
552 - }
553 -
554 - /**
555 - * Migrate existing category orders to language-specific meta keys
556 - * This should be called when a multilingual plugin is activated
557 - *
558 - * @param string $base_key The base meta key (e.g., 'doc_category_order')
559 - * @param string $taxonomy The taxonomy to migrate
560 - * @return bool Success status
561 - */
562 - public static function migrate_category_orders_to_multilingual( $base_key = 'doc_category_order', $taxonomy = 'doc_category' ) {
563 - // Only run if multilingual plugin is active
564 - if ( ! self::is_multilingual_active() ) {
565 - return false;
566 - }
567 -
568 - global $wpdb;
569 -
570 - // 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 - $terms_with_order = $wpdb->get_results( $wpdb->prepare(
573 - "SELECT tm.term_id, tm.meta_value, t.slug
574 - FROM {$wpdb->termmeta} tm
575 - INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
576 - INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
577 - WHERE tm.meta_key = %s AND tt.taxonomy = %s",
578 - $base_key,
579 - $taxonomy
580 - ) );
581 -
582 - if ( empty( $terms_with_order ) ) {
583 - return true; // Nothing to migrate
584 - }
585 -
586 - // Get available languages
587 - $languages = self::get_available_languages();
588 -
589 - if ( empty( $languages ) ) {
590 - return false; // No languages found
591 - }
592 -
593 - // Migrate orders for each language
594 - foreach ( $languages as $language ) {
595 - $language_meta_key = $base_key . '_' . $language;
596 -
597 - foreach ( $terms_with_order as $term_data ) {
598 - // Check if language-specific meta already exists
599 - $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
600 -
601 - if ( empty( $existing_value ) ) {
602 - // Copy the base order to language-specific key
603 - update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
604 - }
605 - }
606 - }
607 -
608 - return true;
609 - }
610 -
611 - /**
612 - * Migrate existing document orders to language-specific meta keys
613 - * This should be called when a multilingual plugin is activated
614 - *
615 - * @param string $base_key The base meta key (e.g., '_docs_order')
616 - * @param string $taxonomy The taxonomy to migrate
617 - * @return bool Success status
618 - */
619 - public static function migrate_docs_orders_to_multilingual( $base_key = '_docs_order', $taxonomy = 'doc_category' ) {
620 - // Only run if multilingual plugin is active
621 - if ( ! self::is_multilingual_active() ) {
622 - return false;
623 - }
624 -
625 - global $wpdb;
626 -
627 - // 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 - $terms_with_docs_order = $wpdb->get_results( $wpdb->prepare(
630 - "SELECT tm.term_id, tm.meta_value, t.slug
631 - FROM {$wpdb->termmeta} tm
632 - INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
633 - INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
634 - WHERE tm.meta_key = %s AND tt.taxonomy = %s AND tm.meta_value != ''",
635 - $base_key,
636 - $taxonomy
637 - ) );
638 -
639 - if ( empty( $terms_with_docs_order ) ) {
640 - return true; // Nothing to migrate
641 - }
642 -
643 - // Get available languages
644 - $languages = self::get_available_languages();
645 -
646 - if ( empty( $languages ) ) {
647 - return false; // No languages found
648 - }
649 -
650 - // Migrate document orders for each language
651 - foreach ( $languages as $language ) {
652 - $language_meta_key = $base_key . '_' . $language;
653 -
654 - foreach ( $terms_with_docs_order as $term_data ) {
655 - // Check if language-specific meta already exists
656 - $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
657 -
658 - if ( empty( $existing_value ) ) {
659 - // Copy the base document order to language-specific key
660 - update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
661 - }
662 - }
663 - }
664 -
665 - return true;
666 - }
667 -
668 - /**
669 - * Migrate both category and document orders to multilingual format
670 - * This is a convenience method that runs both migrations
671 - *
672 - * @return bool Success status
673 - */
674 - public static function migrate_all_orders_to_multilingual() {
675 - $category_result = self::migrate_category_orders_to_multilingual();
676 - $docs_result = self::migrate_docs_orders_to_multilingual();
677 -
678 - return $category_result && $docs_result;
679 - }
680 -
681 - /**
682 - * Get available languages from multilingual plugins
683 - *
684 - * @return array Array of language codes
685 - */
686 - public static function get_available_languages() {
687 - $languages = [];
688 -
689 - // WPML Support
690 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
691 - global $sitepress;
692 - if ( $sitepress && $sitepress->is_setup_complete() ) {
693 - $active_languages = $sitepress->get_active_languages();
694 - if ( is_array( $active_languages ) ) {
695 - $languages = array_keys( $active_languages );
696 - }
697 - }
698 - }
699 - // Polylang Support
700 - elseif ( function_exists( 'pll_languages_list' ) ) {
701 - $languages = pll_languages_list();
702 - }
703 -
704 - return $languages;
705 - }
706 -
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 ) {
987 - global $wpdb;
988 -
989 - $limit = absint( $limit );
990 - $limit_sql = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
991 -
992 193 // Check if the encyclopedia_prefix parameter is set
993 194
994 195 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
995 196 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
996 197 $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 198
1000 199 // if($enable_glossaries && $encyclopeia_suorce === 'glossaries'){
1001 200 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
1002 - $lang_join = '';
1003 - $lang_where = '';
1004 -
1005 - // Add language filtering if multilingual plugin is active and we should apply filtering
1006 - $current_language = self::get_current_language();
1007 - 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 - // For WPML, use icl_translations table
1011 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1012 - $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 - $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
1014 - }
1015 - // For Polylang, use term_relationships with language taxonomy
1016 - elseif ( function_exists( 'pll_current_language' ) ) {
1017 - $lang_join = " LEFT JOIN {$wpdb->term_relationships} tr ON t.term_id = tr.object_id LEFT JOIN {$wpdb->term_taxonomy} tt_lang ON tr.term_taxonomy_id = tt_lang.term_taxonomy_id AND tt_lang.taxonomy = 'language' LEFT JOIN {$wpdb->terms} t_lang ON tt_lang.term_id = t_lang.term_id";
1018 - $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
1019 - }
1020 - }
1021 -
1022 201 $query = "
1023 202 SELECT
1024 203 t.term_id,
1025 204 t.name AS post_title,
@@ -1036,51 +215,27 @@
1036 215 INNER JOIN
1037 216 {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
1038 217 LEFT JOIN
1039 218 {$wpdb->termmeta} m ON t.term_id = m.term_id
1040 - $lang_join
1041 219 WHERE
1042 220 tt.taxonomy = 'glossaries'
1043 221 AND
1044 - SUBSTRING(t.name, 1, 1) = %s
1045 - $lang_where
222 + LEFT(t.name, 1) = %s
1046 223 GROUP BY
1047 224 t.term_id
1048 225 ORDER BY
1049 226 t.name ASC
1050 - $limit_sql
227 + $limit
1051 228 ";
1052 229 } else {
1053 - $lang_join = '';
1054 - $lang_where = '';
1055 -
1056 - // Add language filtering for docs if multilingual plugin is active and we should apply filtering
1057 - $current_language = self::get_current_language();
1058 - 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 - // For WPML, use icl_translations table
1062 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1063 - $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 - $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
1065 - }
1066 - // For Polylang, use term_relationships with language taxonomy
1067 - elseif ( function_exists( 'pll_current_language' ) ) {
1068 - $lang_join = " LEFT JOIN {$wpdb->term_relationships} tr ON {$wpdb->posts}.ID = tr.object_id LEFT JOIN {$wpdb->term_taxonomy} tt_lang ON tr.term_taxonomy_id = tt_lang.term_taxonomy_id AND tt_lang.taxonomy = 'language' LEFT JOIN {$wpdb->terms} t_lang ON tt_lang.term_id = t_lang.term_id";
1069 - $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
1070 - }
1071 - }
1072 -
1073 230 $query = "
1074 231 SELECT ID, post_title, post_excerpt, guid, post_content
1075 232 FROM {$wpdb->posts}
1076 - $lang_join
1077 233 WHERE post_type = 'docs'
1078 234 AND post_status = 'publish'
1079 - AND SUBSTRING(post_title, 1, 1) = %s
1080 - $lang_where
235 + AND LEFT(post_title, 1) = %s
1081 236 ORDER BY post_date DESC
1082 - $limit_sql
237 + $limit
1083 238 ";
1084 239 }
1085 240
1086 241 $current_letter_docs = $wpdb->get_results( $wpdb->prepare( $query, $current_letter ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -1098,9 +253,9 @@
1098 253 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
1099 254 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
1100 255
1101 256 foreach ( $letters as $letter ) {
1102 - $posts = self::get_current_letter_docs( $letter, $limit );
257 + $posts = self::get_current_letter_docs( $letter, "LIMIT $limit" );
1103 258
1104 259 if ( is_array( $posts ) && ! empty( $posts ) ) {
1105 260 foreach ( $posts as $post ) {
1106 261 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
@@ -1106,44 +261,26 @@
1106 261 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
1107 262 $glossary_term_description = $description['glossary_term_description'] ?? '';
1108 263
1109 264 // 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'] ?? '' );
265 + $glossary_term_description = strip_tags( $glossary_term_description );
266 + $post_excerpt = strip_tags( $post['post_excerpt'] ?? '' );
1112 267
1113 268 // Prepare post data
269 + $post_data = [
270 + 'id' => $post['ID'] ?? '',
271 + 'post_title' => $post['post_title'] ?? '',
272 + 'post_excerpt' => ! empty( $post_excerpt )
273 + ? $post_excerpt
274 + : ( ! empty( $glossary_term_description )
275 + ? self::get_custom_excerpt( $glossary_term_description, 15 )
276 + : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ) )
277 + ];
278 +
1114 279 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
1115 - // 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 - $post_data = [
1127 - 'id' => $post['term_id'] ?? '',
1128 - 'post_title' => $post['post_title'] ?? '',
1129 - 'post_excerpt' => ! empty( $post_excerpt )
1130 - ? $post_excerpt
1131 - : ( ! empty( $glossary_term_description )
1132 - ? self::get_custom_excerpt( $glossary_term_description, 15 )
1133 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ) ),
1134 - 'permalink' => $permalink,
1135 - ];
280 + $post_data['permalink'] = isset( $post['slug'] ) ? get_term_link( $post['slug'], 'glossaries' ) : '';
1136 281 } else {
1137 - // For docs
1138 - $post_data = [
1139 - 'id' => $post['ID'] ?? '',
1140 - 'post_title' => $post['post_title'] ?? '',
1141 - 'post_excerpt' => ! empty( $post_excerpt )
1142 - ? $post_excerpt
1143 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ),
1144 - 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
1145 - ];
282 + $post_data['permalink'] = isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : '';
1146 283 }
1147 284
1148 285 $docs_by_letter[$letter][] = $post_data;
1149 286 }
@@ -1155,35 +292,13 @@
1155 292
1156 293 public static function get_glossaries() {
1157 294 global $wpdb;
1158 295
1159 - $lang_join = '';
1160 - $lang_where = '';
1161 -
1162 - // Add language filtering if multilingual plugin is active and we should apply filtering
1163 - $current_language = self::get_current_language();
1164 - 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 - // For WPML, use icl_translations table
1168 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1169 - $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 - $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
1171 - }
1172 - // For Polylang, use term_relationships with language taxonomy
1173 - elseif ( function_exists( 'pll_current_language' ) ) {
1174 - $lang_join = " LEFT JOIN {$wpdb->term_relationships} tr ON t.term_id = tr.object_id LEFT JOIN {$wpdb->term_taxonomy} tt_lang ON tr.term_taxonomy_id = tt_lang.term_taxonomy_id AND tt_lang.taxonomy = 'language' LEFT JOIN {$wpdb->terms} t_lang ON tt_lang.term_id = t_lang.term_id";
1175 - $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
1176 - }
1177 - }
1178 -
1179 296 $query = "
1180 297 SELECT t.name
1181 298 FROM {$wpdb->terms} t
1182 299 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
1183 - $lang_join
1184 300 WHERE tt.taxonomy = 'glossaries'
1185 - $lang_where
1186 301 ORDER BY t.name ASC
1187 302 ";
1188 303
1189 304 $glossaries = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -1197,12 +312,8 @@
1197 312 * @param string $layout
1198 313 * @return string $layout
1199 314 */
1200 315 public static function determine_search_layout( $layout ) {
1201 - if ( $layout ) {
1202 - return $layout;
1203 - }
1204 -
1205 316 $search_layout = betterdocs()->customizer->defaults->get( 'betterdocs_search_layout_select' );
1206 317 $docs_layout = betterdocs()->customizer->defaults->get( 'betterdocs_docs_layout_select' );
1207 318 $archive_page_layout = betterdocs()->customizer->defaults->get( 'betterdocs_archive_layout_select' );
1208 319 $single_layout = betterdocs()->customizer->defaults->get( 'betterdocs_single_layout_select' );
@@ -1215,11 +326,11 @@
1215 326 }
1216 327 } else if ( is_tax( 'doc_tag' ) && ! $search_layout ) {
1217 328 $layout = 'layout-1';
1218 329 } else if ( is_tax( 'doc_category' ) ) {
1219 - if ( $archive_page_layout != 'layout-7' && $archive_page_layout != 'layout-8' && ! $search_layout ) {
330 + if ( $archive_page_layout != 'layout-7' && ! $search_layout ) {
1220 331 $layout = 'layout-1';
1221 - } else if ( ( $archive_page_layout == 'layout-7' && ! $search_layout ) || ( $archive_page_layout == 'layout-8' && ! $search_layout ) ) {
332 + } else if ( $archive_page_layout == 'layout-7' && ! $search_layout ) {
1222 333 $layout = 'layout-2';
1223 334 }
1224 335 } else if ( is_singular( 'docs' ) ) {
1225 336 if ( $single_layout != 'layout-8' && $single_layout != 'layout-9' && ! $search_layout ) {
@@ -1290,16 +401,15 @@
1290 401 ] );
1291 402 return isset( $terms[0] ) ? $terms[0] : [];
1292 403 }
1293 404
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.
405 + public static function delete_specific_faq_posts_by_faq_category( $term_id ) {
1296 406 $args = [
1297 407 'post_type' => 'betterdocs_faq',
1298 408 'posts_per_page' => -1,
1299 409 'tax_query' => [
1300 410 [
1301 - 'taxonomy' => $taxonomy,
411 + 'taxonomy' => 'betterdocs_faq_category',
1302 412 'field' => 'id',
1303 413 'terms' => $term_id,
1304 414 'operator' => 'IN'
1305 415 ]
@@ -1314,145 +424,5 @@
1314 424 wp_delete_post( $doc_id, true );
1315 425 }
1316 426 }
1317 427 }
1318 -
1319 - /**
1320 - * Function To Normalize Repeater Field For Quick Builder
1321 - *
1322 - * @param array $fields
1323 - * @param array $include_field_keys
1324 - *
1325 - * @return array
1326 - */
1327 - public static function normalize_repeater_field( $fields, $include_field_keys = [] ) {
1328 - if( empty( $include_field_keys ) ) {
1329 - return $fields;
1330 - }
1331 -
1332 - $normalized_fields = [];
1333 -
1334 - foreach( $fields as $field ) {
1335 - foreach( $include_field_keys as $field_key ) {
1336 - if( ! isset( $normalized_fields[$field_key] ) ) {
1337 - $normalized_fields[$field_key] = isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [];
1338 - } else {
1339 - array_push( $normalized_fields[$field_key], ...( isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [] ) );
1340 - $normalized_fields[$field_key] = array_unique( $normalized_fields[$field_key] );
1341 - }
1342 - }
1343 - }
1344 -
1345 - return $normalized_fields;
1346 - }
1347 -
1348 - public static function get_local_plugin_data( $basename = '' ) {
1349 - if ( empty( $basename ) ) {
1350 - return false;
1351 - }
1352 -
1353 - if ( !function_exists( 'get_plugins' ) ) {
1354 - include_once ABSPATH . 'wp-admin/includes/plugin.php';
1355 - }
1356 -
1357 - $plugins = get_plugins();
1358 -
1359 - if ( !isset( $plugins[ $basename ] ) ) {
1360 - return false;
1361 - }
1362 -
1363 - return $plugins[ $basename ];
1364 - }
1365 -
1366 - /**
1367 - * Get default file icon based on programming language
1368 - *
1369 - * @param string $language Programming language identifier
1370 - * @return string Emoji icon for the language
1371 - */
1372 - public static function get_file_icon_by_language( $language ) {
1373 - $icons = [
1374 - 'javascript' => '📄',
1375 - 'typescript' => '📘',
1376 - 'jsx' => '⚛️',
1377 - 'tsx' => '⚛️',
1378 - 'html' => '🌐',
1379 - 'css' => '🎨',
1380 - 'scss' => '🎨',
1381 - 'sass' => '🎨',
1382 - 'less' => '🎨',
1383 - 'php' => '🐘',
1384 - 'python' => '🐍',
1385 - 'java' => '☕',
1386 - 'csharp' => '🔷',
1387 - 'cpp' => '⚙️',
1388 - 'c' => '⚙️',
1389 - 'ruby' => '💎',
1390 - 'go' => '🐹',
1391 - 'rust' => '🦀',
1392 - 'swift' => '🦉',
1393 - 'kotlin' => '🎯',
1394 - 'sql' => '🗃️',
1395 - 'json' => '📋',
1396 - 'yaml' => '📋',
1397 - 'xml' => '📄',
1398 - 'markdown' => '📝',
1399 - 'bash' => '💻',
1400 - 'shell' => '💻',
1401 - 'powershell' => '💻',
1402 - 'dockerfile' => '🐳',
1403 - ];
1404 -
1405 - return isset( $icons[$language] ) ? $icons[$language] : '📄';
1406 - }
1407 -
1408 - /**
1409 - * Check if AI Chatbot is enabled
1410 - *
1411 - * @return bool
1412 - */
1413 - public function is_ai_chatbot_enabled() {
1414 - $chatbot_active = is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
1415 - $chatbot_license_valid = get_option( 'betterdocs_chatbot_software__license_status' ) === 'valid';
1416 - $chatbot_enabled = betterdocs()->settings->get( 'enable_ai_chatbot', false );
1417 -
1418 - // AI Search Suggestions are enabled if all conditions are met
1419 - return $chatbot_active && $chatbot_license_valid && $chatbot_enabled;
1420 - }
1421 -
1422 - /**
1423 - * Check if tags are enabled and post has tags
1424 - *
1425 - * @return bool
1426 - */
1427 - public function is_tag_enabled() {
1428 - global $post;
1429 - $product_terms = wp_get_object_terms( $post->ID, 'doc_tag' );
1430 - $enable_tags = betterdocs()->settings->get( 'enable_tags', false );
1431 - return ! empty( $product_terms ) && $enable_tags;
1432 - }
1433 -
1434 - /**
1435 - * Check if AI Search Suggestions are enabled
1436 - *
1437 - * @return bool
1438 - */
1439 - public function is_ai_search_suggestions_enabled() {
1440 - $ai_search_suggestions_active = is_plugin_active( 'betterdocs-ai-search-suggestions/betterdocs-ai-search-suggestions.php' );
1441 - $ai_search_suggestions_license_valid = get_option( 'betterdocs_ai_search_suggestions_software__license_status' ) === 'valid';
1442 - $ai_search_suggestions_enabled = betterdocs()->settings->get( 'enable_ai_powered_search', false );
1443 -
1444 - return $ai_search_suggestions_active && $ai_search_suggestions_license_valid && $ai_search_suggestions_enabled;
1445 - }
1446 -
1447 - /**
1448 - * Get the maximum order value from the 'doc_category_order' term meta
1449 - *
1450 - * @return int
1451 - */
1452 - public static function get_max_doc_category_order_from_term_meta() {
1453 - 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.
1456 - return $result;
1457 - }
1458 428 }