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 +24 -722 4.5.53.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 }
@@ -111,14 +82,10 @@
111 82 * @return string
112 83 */
113 84 public static function admin_tab() {
114 85 $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';
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
121 88 }
122 89
123 90 return $admin_ui;
124 91 }
@@ -219,475 +186,19 @@
219 186 }
220 187 return $excerpt;
221 188 }
222 189
223 - /**
224 - * Get current language from various multilingual plugins
225 - *
226 - * @return string|null Current language code
227 - */
228 - public static function get_current_language() {
229 - $current_language = null;
230 -
231 - // WPML Support
232 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
233 - global $sitepress;
234 - if ( $sitepress && $sitepress->is_setup_complete() ) {
235 - $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
236 - }
237 - }
238 - // Polylang Support
239 - elseif ( function_exists( 'pll_current_language' ) ) {
240 - $current_language = pll_current_language();
241 - }
242 - // qTranslate-X Support
243 - elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
244 - $current_language = qtranxf_getLanguage();
245 - }
246 - // Weglot Support
247 - elseif ( function_exists( 'weglot_get_current_language' ) ) {
248 - $current_language = weglot_get_current_language();
249 - }
250 - // TranslatePress Support
251 - elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
252 - $current_language = trp_get_current_language();
253 - }
254 -
255 - return $current_language;
256 - }
257 -
258 - /**
259 - * Check if any multilingual plugin is active
260 - *
261 - * @return bool
262 - */
263 - public static function is_multilingual_active() {
264 - return is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ||
265 - function_exists( 'pll_current_language' ) ||
266 - function_exists( 'qtranxf_getLanguage' ) ||
267 - function_exists( 'weglot_get_current_language' ) ||
268 - ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) );
269 - }
270 -
271 - /**
272 - * Check if we should apply language filtering
273 - * Only apply on frontend or when specifically requested
274 - *
275 - * @return bool
276 - */
277 - public static function should_apply_language_filtering() {
278 - // Don't apply language filtering in admin context unless it's a frontend request
279 - if ( is_admin() ) {
280 - // Allow language filtering for REST API requests that are frontend-facing
281 - if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
282 - // 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'] ) ) : '';
284 - // Don't filter admin REST requests for glossaries management
285 - if ( strpos( $request_uri, '/wp/v2/glossaries' ) !== false ) {
286 - return false; // Don't filter admin glossaries management
287 - }
288 - }
289 - return false; // Don't filter other admin requests
290 - }
291 -
292 - // Apply filtering on frontend
293 - return true;
294 - }
295 -
296 - /**
297 - * Get current admin language for multilingual sites
298 - * This is specifically for admin context where we need to detect
299 - * the language being used for editing terms/posts
300 - *
301 - * @return string|null Current admin language code
302 - */
303 - public static function get_current_admin_language() {
304 - $current_language = null;
305 -
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 - // WPML Support - Admin language detection
323 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
324 - global $sitepress;
325 - 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 - // 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 );
331 - if ( ! is_wp_error( $term_info ) && $term_info && isset( $term_info['language_code'] ) ) {
332 - $current_language = $term_info['language_code'];
333 - }
334 -
335 - }
336 -
337 - // Check for language parameter in URL
338 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
339 - if ( ! $current_language && isset( $_GET['lang'] ) ) {
340 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
341 - }
342 -
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 - // Fallback to admin language or current language
349 - if ( ! $current_language ) {
350 - $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
351 - }
352 - }
353 - }
354 - // Polylang Support - Admin language detection
355 - 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 - // 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 );
361 - if ( $term_lang ) {
362 - $current_language = $term_lang;
363 - }
364 - }
365 -
366 - // Check for language parameter in URL
367 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
368 - if ( ! $current_language && isset( $_GET['lang'] ) ) {
369 - $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
370 - }
371 -
372 - // Fallback to current admin language
373 - if ( ! $current_language ) {
374 - $current_language = pll_current_language( 'slug' );
375 - }
376 - }
377 - // Other multilingual plugins
378 - elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
379 - $current_language = qtranxf_getLanguage();
380 - }
381 - elseif ( function_exists( 'weglot_get_current_language' ) ) {
382 - $current_language = weglot_get_current_language();
383 - }
384 - elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
385 - $current_language = trp_get_current_language();
386 - }
387 -
388 - return $current_language;
389 - }
390 -
391 - /**
392 - * Generate language-specific meta key for category ordering
393 - * Always falls back to base key if language-specific key doesn't exist
394 - *
395 - * @param string $base_key The base meta key (e.g., 'doc_category_order')
396 - * @param string|null $language Language code, if null will auto-detect
397 - * @return string Language-specific meta key or base key as fallback
398 - */
399 - public static function get_language_specific_meta_key( $base_key, $language = null ) {
400 - // If no multilingual plugin is active, return the base key
401 - if ( ! self::is_multilingual_active() ) {
402 - return $base_key;
403 - }
404 -
405 - // Get current admin language if not provided
406 - if ( $language === null ) {
407 - $language = self::get_current_admin_language();
408 - }
409 -
410 - // If no language detected, return base key for backward compatibility
411 - if ( ! $language ) {
412 - return $base_key;
413 - }
414 -
415 - // Always return base key for now - we'll handle fallback in the query functions
416 - // This ensures compatibility without requiring migration
417 - return $base_key;
418 - }
419 -
420 - /**
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 - * Get the appropriate meta key with fallback logic
451 - * This function checks if language-specific meta exists, if not falls back to base key
452 - *
453 - * @param string $base_key The base meta key
454 - * @param int $term_id The term ID to check
455 - * @param string|null $language Language code
456 - * @return string The meta key to use
457 - */
458 - public static function get_meta_key_with_fallback( $base_key, $term_id = null, $language = null ) {
459 - // If no multilingual plugin is active, return the base key
460 - if ( ! self::is_multilingual_active() ) {
461 - return $base_key;
462 - }
463 -
464 - // Get current admin language if not provided
465 - if ( $language === null ) {
466 - $language = self::get_current_admin_language();
467 - }
468 -
469 - // If no language detected, return base key
470 - if ( ! $language ) {
471 - return $base_key;
472 - }
473 -
474 - $lang_meta_key = $base_key . '_' . $language;
475 -
476 - // If we have a specific term ID, check if language-specific meta exists
477 - if ( $term_id ) {
478 - $lang_value = get_term_meta( $term_id, $lang_meta_key, true );
479 - if ( ! empty( $lang_value ) ) {
480 - return $lang_meta_key;
481 - }
482 - // Fall back to base key if language-specific doesn't exist
483 - return $base_key;
484 - }
485 -
486 - // 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 = '' ) {
487 191 global $wpdb;
488 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live multilingual meta-key resolution; result varies per active language.
489 - $has_lang_meta = $wpdb->get_var( $wpdb->prepare(
490 - "SELECT COUNT(*) FROM {$wpdb->termmeta} tm
491 - INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
492 - WHERE tm.meta_key = %s AND tt.taxonomy = 'doc_category' AND tm.meta_value != ''",
493 - $lang_meta_key
494 - ) );
495 192
496 - // If language-specific meta exists for some terms, use it (terms without it will have empty values)
497 - // Otherwise, fall back to base key
498 - return $has_lang_meta > 0 ? $lang_meta_key : $base_key;
499 - }
500 -
501 - /**
502 - * Migrate existing category orders to language-specific meta keys
503 - * This should be called when a multilingual plugin is activated
504 - *
505 - * @param string $base_key The base meta key (e.g., 'doc_category_order')
506 - * @param string $taxonomy The taxonomy to migrate
507 - * @return bool Success status
508 - */
509 - public static function migrate_category_orders_to_multilingual( $base_key = 'doc_category_order', $taxonomy = 'doc_category' ) {
510 - // Only run if multilingual plugin is active
511 - if ( ! self::is_multilingual_active() ) {
512 - return false;
513 - }
514 -
515 - global $wpdb;
516 -
517 - // 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 - $terms_with_order = $wpdb->get_results( $wpdb->prepare(
520 - "SELECT tm.term_id, tm.meta_value, t.slug
521 - FROM {$wpdb->termmeta} tm
522 - INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
523 - INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
524 - WHERE tm.meta_key = %s AND tt.taxonomy = %s",
525 - $base_key,
526 - $taxonomy
527 - ) );
528 -
529 - if ( empty( $terms_with_order ) ) {
530 - return true; // Nothing to migrate
531 - }
532 -
533 - // Get available languages
534 - $languages = self::get_available_languages();
535 -
536 - if ( empty( $languages ) ) {
537 - return false; // No languages found
538 - }
539 -
540 - // Migrate orders for each language
541 - foreach ( $languages as $language ) {
542 - $language_meta_key = $base_key . '_' . $language;
543 -
544 - foreach ( $terms_with_order as $term_data ) {
545 - // Check if language-specific meta already exists
546 - $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
547 -
548 - if ( empty( $existing_value ) ) {
549 - // Copy the base order to language-specific key
550 - update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
551 - }
552 - }
553 - }
554 -
555 - return true;
556 - }
557 -
558 - /**
559 - * Migrate existing document orders to language-specific meta keys
560 - * This should be called when a multilingual plugin is activated
561 - *
562 - * @param string $base_key The base meta key (e.g., '_docs_order')
563 - * @param string $taxonomy The taxonomy to migrate
564 - * @return bool Success status
565 - */
566 - public static function migrate_docs_orders_to_multilingual( $base_key = '_docs_order', $taxonomy = 'doc_category' ) {
567 - // Only run if multilingual plugin is active
568 - if ( ! self::is_multilingual_active() ) {
569 - return false;
570 - }
571 -
572 - global $wpdb;
573 -
574 - // 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 - $terms_with_docs_order = $wpdb->get_results( $wpdb->prepare(
577 - "SELECT tm.term_id, tm.meta_value, t.slug
578 - FROM {$wpdb->termmeta} tm
579 - INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
580 - INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
581 - WHERE tm.meta_key = %s AND tt.taxonomy = %s AND tm.meta_value != ''",
582 - $base_key,
583 - $taxonomy
584 - ) );
585 -
586 - if ( empty( $terms_with_docs_order ) ) {
587 - return true; // Nothing to migrate
588 - }
589 -
590 - // Get available languages
591 - $languages = self::get_available_languages();
592 -
593 - if ( empty( $languages ) ) {
594 - return false; // No languages found
595 - }
596 -
597 - // Migrate document orders for each language
598 - foreach ( $languages as $language ) {
599 - $language_meta_key = $base_key . '_' . $language;
600 -
601 - foreach ( $terms_with_docs_order as $term_data ) {
602 - // Check if language-specific meta already exists
603 - $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
604 -
605 - if ( empty( $existing_value ) ) {
606 - // Copy the base document order to language-specific key
607 - update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
608 - }
609 - }
610 - }
611 -
612 - return true;
613 - }
614 -
615 - /**
616 - * Migrate both category and document orders to multilingual format
617 - * This is a convenience method that runs both migrations
618 - *
619 - * @return bool Success status
620 - */
621 - public static function migrate_all_orders_to_multilingual() {
622 - $category_result = self::migrate_category_orders_to_multilingual();
623 - $docs_result = self::migrate_docs_orders_to_multilingual();
624 -
625 - return $category_result && $docs_result;
626 - }
627 -
628 - /**
629 - * Get available languages from multilingual plugins
630 - *
631 - * @return array Array of language codes
632 - */
633 - public static function get_available_languages() {
634 - $languages = [];
635 -
636 - // WPML Support
637 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
638 - global $sitepress;
639 - if ( $sitepress && $sitepress->is_setup_complete() ) {
640 - $active_languages = $sitepress->get_active_languages();
641 - if ( is_array( $active_languages ) ) {
642 - $languages = array_keys( $active_languages );
643 - }
644 - }
645 - }
646 - // Polylang Support
647 - elseif ( function_exists( 'pll_languages_list' ) ) {
648 - $languages = pll_languages_list();
649 - }
650 -
651 - return $languages;
652 - }
653 -
654 - public static function get_current_letter_docs( $current_letter, $limit = 0 ) {
655 - global $wpdb;
656 -
657 - $limit = absint( $limit );
658 - $limit_sql = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
659 -
660 193 // Check if the encyclopedia_prefix parameter is set
661 194
662 195 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
663 196 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
664 197 $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 198
668 199 // if($enable_glossaries && $encyclopeia_suorce === 'glossaries'){
669 200 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
670 - $lang_join = '';
671 - $lang_where = '';
672 -
673 - // Add language filtering if multilingual plugin is active and we should apply filtering
674 - $current_language = self::get_current_language();
675 - 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 - // For WPML, use icl_translations table
679 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
680 - $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 - $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
682 - }
683 - // For Polylang, use term_relationships with language taxonomy
684 - elseif ( function_exists( 'pll_current_language' ) ) {
685 - $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";
686 - $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
687 - }
688 - }
689 -
690 201 $query = "
691 202 SELECT
692 203 t.term_id,
693 204 t.name AS post_title,
@@ -704,51 +215,27 @@
704 215 INNER JOIN
705 216 {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
706 217 LEFT JOIN
707 218 {$wpdb->termmeta} m ON t.term_id = m.term_id
708 - $lang_join
709 219 WHERE
710 220 tt.taxonomy = 'glossaries'
711 221 AND
712 - SUBSTRING(t.name, 1, 1) = %s
713 - $lang_where
222 + LEFT(t.name, 1) = %s
714 223 GROUP BY
715 224 t.term_id
716 225 ORDER BY
717 226 t.name ASC
718 - $limit_sql
227 + $limit
719 228 ";
720 229 } else {
721 - $lang_join = '';
722 - $lang_where = '';
723 -
724 - // Add language filtering for docs if multilingual plugin is active and we should apply filtering
725 - $current_language = self::get_current_language();
726 - 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 - // For WPML, use icl_translations table
730 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
731 - $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 - $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
733 - }
734 - // For Polylang, use term_relationships with language taxonomy
735 - elseif ( function_exists( 'pll_current_language' ) ) {
736 - $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";
737 - $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
738 - }
739 - }
740 -
741 230 $query = "
742 231 SELECT ID, post_title, post_excerpt, guid, post_content
743 232 FROM {$wpdb->posts}
744 - $lang_join
745 233 WHERE post_type = 'docs'
746 234 AND post_status = 'publish'
747 - AND SUBSTRING(post_title, 1, 1) = %s
748 - $lang_where
235 + AND LEFT(post_title, 1) = %s
749 236 ORDER BY post_date DESC
750 - $limit_sql
237 + $limit
751 238 ";
752 239 }
753 240
754 241 $current_letter_docs = $wpdb->get_results( $wpdb->prepare( $query, $current_letter ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -766,9 +253,9 @@
766 253 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
767 254 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
768 255
769 256 foreach ( $letters as $letter ) {
770 - $posts = self::get_current_letter_docs( $letter, $limit );
257 + $posts = self::get_current_letter_docs( $letter, "LIMIT $limit" );
771 258
772 259 if ( is_array( $posts ) && ! empty( $posts ) ) {
773 260 foreach ( $posts as $post ) {
774 261 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
@@ -774,44 +261,26 @@
774 261 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
775 262 $glossary_term_description = $description['glossary_term_description'] ?? '';
776 263
777 264 // 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'] ?? '' );
265 + $glossary_term_description = strip_tags( $glossary_term_description );
266 + $post_excerpt = strip_tags( $post['post_excerpt'] ?? '' );
780 267
781 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 +
782 279 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
783 - // 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 - $post_data = [
795 - 'id' => $post['term_id'] ?? '',
796 - 'post_title' => $post['post_title'] ?? '',
797 - 'post_excerpt' => ! empty( $post_excerpt )
798 - ? $post_excerpt
799 - : ( ! empty( $glossary_term_description )
800 - ? self::get_custom_excerpt( $glossary_term_description, 15 )
801 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ) ),
802 - 'permalink' => $permalink,
803 - ];
280 + $post_data['permalink'] = isset( $post['slug'] ) ? get_term_link( $post['slug'], 'glossaries' ) : '';
804 281 } else {
805 - // For docs
806 - $post_data = [
807 - 'id' => $post['ID'] ?? '',
808 - 'post_title' => $post['post_title'] ?? '',
809 - 'post_excerpt' => ! empty( $post_excerpt )
810 - ? $post_excerpt
811 - : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ),
812 - 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
813 - ];
282 + $post_data['permalink'] = isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : '';
814 283 }
815 284
816 285 $docs_by_letter[$letter][] = $post_data;
817 286 }
@@ -823,35 +292,13 @@
823 292
824 293 public static function get_glossaries() {
825 294 global $wpdb;
826 295
827 - $lang_join = '';
828 - $lang_where = '';
829 -
830 - // Add language filtering if multilingual plugin is active and we should apply filtering
831 - $current_language = self::get_current_language();
832 - 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 - // For WPML, use icl_translations table
836 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
837 - $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 - $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
839 - }
840 - // For Polylang, use term_relationships with language taxonomy
841 - elseif ( function_exists( 'pll_current_language' ) ) {
842 - $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";
843 - $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
844 - }
845 - }
846 -
847 296 $query = "
848 297 SELECT t.name
849 298 FROM {$wpdb->terms} t
850 299 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
851 - $lang_join
852 300 WHERE tt.taxonomy = 'glossaries'
853 - $lang_where
854 301 ORDER BY t.name ASC
855 302 ";
856 303
857 304 $glossaries = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -865,12 +312,8 @@
865 312 * @param string $layout
866 313 * @return string $layout
867 314 */
868 315 public static function determine_search_layout( $layout ) {
869 - if ( $layout ) {
870 - return $layout;
871 - }
872 -
873 316 $search_layout = betterdocs()->customizer->defaults->get( 'betterdocs_search_layout_select' );
874 317 $docs_layout = betterdocs()->customizer->defaults->get( 'betterdocs_docs_layout_select' );
875 318 $archive_page_layout = betterdocs()->customizer->defaults->get( 'betterdocs_archive_layout_select' );
876 319 $single_layout = betterdocs()->customizer->defaults->get( 'betterdocs_single_layout_select' );
@@ -883,11 +326,11 @@
883 326 }
884 327 } else if ( is_tax( 'doc_tag' ) && ! $search_layout ) {
885 328 $layout = 'layout-1';
886 329 } else if ( is_tax( 'doc_category' ) ) {
887 - if ( $archive_page_layout != 'layout-7' && $archive_page_layout != 'layout-8' && ! $search_layout ) {
330 + if ( $archive_page_layout != 'layout-7' && ! $search_layout ) {
888 331 $layout = 'layout-1';
889 - } 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 ) {
890 333 $layout = 'layout-2';
891 334 }
892 335 } else if ( is_singular( 'docs' ) ) {
893 336 if ( $single_layout != 'layout-8' && $single_layout != 'layout-9' && ! $search_layout ) {
@@ -959,9 +402,8 @@
959 402 return isset( $terms[0] ) ? $terms[0] : [];
960 403 }
961 404
962 405 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 406 $args = [
965 407 'post_type' => 'betterdocs_faq',
966 408 'posts_per_page' => -1,
967 409 'tax_query' => [
@@ -982,145 +424,5 @@
982 424 wp_delete_post( $doc_id, true );
983 425 }
984 426 }
985 427 }
986 -
987 - /**
988 - * Function To Normalize Repeater Field For Quick Builder
989 - *
990 - * @param array $fields
991 - * @param array $include_field_keys
992 - *
993 - * @return array
994 - */
995 - public static function normalize_repeater_field( $fields, $include_field_keys = [] ) {
996 - if( empty( $include_field_keys ) ) {
997 - return $fields;
998 - }
999 -
1000 - $normalized_fields = [];
1001 -
1002 - foreach( $fields as $field ) {
1003 - foreach( $include_field_keys as $field_key ) {
1004 - if( ! isset( $normalized_fields[$field_key] ) ) {
1005 - $normalized_fields[$field_key] = isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [];
1006 - } else {
1007 - array_push( $normalized_fields[$field_key], ...( isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [] ) );
1008 - $normalized_fields[$field_key] = array_unique( $normalized_fields[$field_key] );
1009 - }
1010 - }
1011 - }
1012 -
1013 - return $normalized_fields;
1014 - }
1015 -
1016 - public static function get_local_plugin_data( $basename = '' ) {
1017 - if ( empty( $basename ) ) {
1018 - return false;
1019 - }
1020 -
1021 - if ( !function_exists( 'get_plugins' ) ) {
1022 - include_once ABSPATH . 'wp-admin/includes/plugin.php';
1023 - }
1024 -
1025 - $plugins = get_plugins();
1026 -
1027 - if ( !isset( $plugins[ $basename ] ) ) {
1028 - return false;
1029 - }
1030 -
1031 - return $plugins[ $basename ];
1032 - }
1033 -
1034 - /**
1035 - * Get default file icon based on programming language
1036 - *
1037 - * @param string $language Programming language identifier
1038 - * @return string Emoji icon for the language
1039 - */
1040 - public static function get_file_icon_by_language( $language ) {
1041 - $icons = [
1042 - 'javascript' => '📄',
1043 - 'typescript' => '📘',
1044 - 'jsx' => '⚛️',
1045 - 'tsx' => '⚛️',
1046 - 'html' => '🌐',
1047 - 'css' => '🎨',
1048 - 'scss' => '🎨',
1049 - 'sass' => '🎨',
1050 - 'less' => '🎨',
1051 - 'php' => '🐘',
1052 - 'python' => '🐍',
1053 - 'java' => '☕',
1054 - 'csharp' => '🔷',
1055 - 'cpp' => '⚙️',
1056 - 'c' => '⚙️',
1057 - 'ruby' => '💎',
1058 - 'go' => '🐹',
1059 - 'rust' => '🦀',
1060 - 'swift' => '🦉',
1061 - 'kotlin' => '🎯',
1062 - 'sql' => '🗃️',
1063 - 'json' => '📋',
1064 - 'yaml' => '📋',
1065 - 'xml' => '📄',
1066 - 'markdown' => '📝',
1067 - 'bash' => '💻',
1068 - 'shell' => '💻',
1069 - 'powershell' => '💻',
1070 - 'dockerfile' => '🐳',
1071 - ];
1072 -
1073 - return isset( $icons[$language] ) ? $icons[$language] : '📄';
1074 - }
1075 -
1076 - /**
1077 - * Check if AI Chatbot is enabled
1078 - *
1079 - * @return bool
1080 - */
1081 - public function is_ai_chatbot_enabled() {
1082 - $chatbot_active = is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
1083 - $chatbot_license_valid = get_option( 'betterdocs_chatbot_software__license_status' ) === 'valid';
1084 - $chatbot_enabled = betterdocs()->settings->get( 'enable_ai_chatbot', false );
1085 -
1086 - // AI Search Suggestions are enabled if all conditions are met
1087 - return $chatbot_active && $chatbot_license_valid && $chatbot_enabled;
1088 - }
1089 -
1090 - /**
1091 - * Check if tags are enabled and post has tags
1092 - *
1093 - * @return bool
1094 - */
1095 - public function is_tag_enabled() {
1096 - global $post;
1097 - $product_terms = wp_get_object_terms( $post->ID, 'doc_tag' );
1098 - $enable_tags = betterdocs()->settings->get( 'enable_tags', false );
1099 - return ! empty( $product_terms ) && $enable_tags;
1100 - }
1101 -
1102 - /**
1103 - * Check if AI Search Suggestions are enabled
1104 - *
1105 - * @return bool
1106 - */
1107 - public function is_ai_search_suggestions_enabled() {
1108 - $ai_search_suggestions_active = is_plugin_active( 'betterdocs-ai-search-suggestions/betterdocs-ai-search-suggestions.php' );
1109 - $ai_search_suggestions_license_valid = get_option( 'betterdocs_ai_search_suggestions_software__license_status' ) === 'valid';
1110 - $ai_search_suggestions_enabled = betterdocs()->settings->get( 'enable_ai_powered_search', false );
1111 -
1112 - return $ai_search_suggestions_active && $ai_search_suggestions_license_valid && $ai_search_suggestions_enabled;
1113 - }
1114 -
1115 - /**
1116 - * Get the maximum order value from the 'doc_category_order' term meta
1117 - *
1118 - * @return int
1119 - */
1120 - public static function get_max_doc_category_order_from_term_meta() {
1121 - 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.
1124 - return $result;
1125 - }
1126 428 }