PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.4.1
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.4.1
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Utils / Helper.php

Helper.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.4.1, at includes/Utils/Helper.php

1,032 lines 35.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Utils;
4
5 use function BetterLinksPro\Dependencies\GuzzleHttp\json_decode;
6 use function WPML\PHP\Logger\error;
7
8 class Helper extends Base {
9
10 /**
11 * Mask an API key for safe display: first 3 chars + 8 asterisks + last 4 chars.
12 * Fixed asterisk count avoids leaking the real key length.
13 */
14 public static function mask_api_key( $key ) {
15 if ( ! is_string( $key ) || $key === '' ) {
16 return '';
17 }
18 $key = trim( $key );
19 if ( strlen( $key ) < 8 ) {
20 return str_repeat( '*', strlen( $key ) );
21 }
22 return substr( $key, 0, 3 ) . str_repeat( '*', 8 ) . substr( $key, -4 );
23 }
24
25 public static function get_plugins( $plugin_basename = null ) {
26 if ( ! function_exists( 'get_plugins' ) ) {
27 include_once ABSPATH . 'wp-admin/includes/plugin.php';
28 }
29
30 $plugins = get_plugins();
31 return $plugin_basename == null ? $plugins : isset( $plugins[ $plugin_basename ] );
32 }
33
34 public static function is_plugin_active( $plugin_basename ) {
35 if ( ! function_exists( 'is_plugin_active' ) ) {
36 include_once ABSPATH . 'wp-admin/includes/plugin.php';
37 }
38
39 return is_plugin_active( $plugin_basename );
40 }
41
42 public static function get_tax( $tax = '' ) {
43 global $wp_query;
44
45 if ( is_tax( 'knowledge_base' ) ) {
46 $_taxes = $wp_query->tax_query->queried_terms;
47 if ( array_key_exists( 'doc_category', $_taxes ) ) {
48 $tax = 'doc_category';
49 } else {
50 $tax = 'knowledge_base';
51 }
52 } elseif ( is_tax( 'doc_category' ) ) {
53 $tax = 'doc_category';
54 } elseif ( is_tax( 'doc_tag' ) ) {
55 $tax = 'doc_tag';
56 }
57
58 return $tax;
59 }
60
61 public function is_templates() {
62 global $wp_query;
63 $slug = betterdocs()->settings->get( 'encyclopedia_root_slug', 'encyclopedia' );
64
65 $tax = $this->get_tax();
66 if ( is_post_type_archive( 'docs' ) || $tax === 'knowledge_base' || $tax === 'doc_category' || $tax === 'doc_tag' || is_singular( 'docs' ) || is_tax( 'glossaries' ) ) {
67 return true;
68 }
69
70 if ( isset( $wp_query->query['pagename'] ) && $wp_query->query['pagename'] === $slug ) {
71 return true;
72 }
73
74 return false;
75 }
76
77 public function is_el_templates() {
78 $_return_val = betterdocs()->editor->get( 'elementor' )->is_templates();
79
80 if ( $_return_val !== null ) {
81 return $_return_val;
82 }
83
84 $this->is_templates();
85 }
86
87 /**
88 * Which tab to show.
89 *
90 * 1. Drag and Drop UI
91 * 2. Post List UI
92 *
93 * * 1. dnd
94 * * 2. classic
95 *
96 * look into views/admin/docs-ui directory to know more.
97 *
98 * @return string
99 */
100 public static function admin_tab() {
101 $admin_ui = 'grid';
102 if ( isset( $_GET['mode'], $_GET['page'] ) && $_GET['page'] === 'betterdocs-admin' && ! empty( $_GET['mode'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
103 $admin_ui = $_GET['mode'] === 'grid' ? 'grid' : 'list'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
104 }
105
106 return $admin_ui;
107 }
108
109 public static function is_active( $prev, $current, $class = 'active' ) {
110 if ( $current == $prev ) {
111 return $class;
112 }
113
114 return '';
115 }
116
117 public function get_users( $args ) {
118 $cache_key = 'betterdocs_cache_admin_user_roles';
119 $users = betterdocs()->database->get_cache( $cache_key );
120
121 if ( false === $users ) {
122 $users = get_users( $args );
123 betterdocs()->database->set_cache( $cache_key, $users );
124 }
125
126 return $users;
127 }
128
129 /**
130 * Normalize Menu Array
131 * Menu creator helper
132 *
133 * @since 2.5.0
134 *
135 * @param string $title
136 * @param string $slug
137 * @param string $cap
138 * @param array $callback
139 *
140 * @return array
141 */
142 public static function normalize_menu( $title, $slug, $cap = 'edit_docs', $callback = null, $optional = [] ) {
143 $args = [
144 'page_title' => $title,
145 'menu_title' => $title,
146 'capability' => $cap,
147 'menu_slug' => $slug
148 ];
149
150 if ( $callback != null ) {
151 $args['callback'] = $callback;
152 }
153
154 return wp_parse_args( $optional, $args );
155 }
156
157 /**
158 * Check if the current theme is a block theme.
159 *
160 * @since x.x.x
161 * @return bool
162 */
163 public function current_theme_is_fse_theme() {
164 if ( function_exists( 'wp_is_block_theme' ) ) {
165 return (bool) wp_is_block_theme();
166 }
167 if ( function_exists( 'gutenberg_is_fse_theme' ) ) {
168 return (bool) gutenberg_is_fse_theme();
169 }
170
171 return false;
172 }
173
174 protected static function is_assoc_array( $array ) {
175 return array_keys( $array ) !== range( 0, count( $array ) - 1 );
176 }
177
178 public static function merge( &$array1, &$array2 ) {
179 $merged = $array1;
180
181 foreach ( $array2 as $key => &$value ) {
182 if ( is_array( $value ) && self::is_assoc_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
183 $merged[ $key ] = self::merge( $merged[ $key ], $value );
184 } elseif ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
185 $merged[ $key ] = array_merge( $merged[ $key ], $value );
186 } else {
187 $merged[ $key ] = $value;
188 }
189 }
190
191 return $merged;
192 }
193
194 public static function get_custom_excerpt( $content, $numOfWords ) {
195 $content = strip_shortcodes( $content );
196 $content = wp_strip_all_tags( $content );
197 $words = explode( ' ', $content );
198 $excerptWords = array_slice( $words, 0, $numOfWords );
199 $excerpt = implode( ' ', $excerptWords );
200 if ( count( $words ) > $numOfWords ) {
201 $excerpt .= '...';
202 }
203 return $excerpt;
204 }
205
206 /**
207 * Get current language from various multilingual plugins
208 *
209 * @return string|null Current language code
210 */
211 public static function get_current_language() {
212 $current_language = null;
213
214 // WPML Support
215 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
216 global $sitepress;
217 if ( $sitepress && $sitepress->is_setup_complete() ) {
218 $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
219 }
220 }
221 // Polylang Support
222 elseif ( function_exists( 'pll_current_language' ) ) {
223 $current_language = pll_current_language();
224 }
225 // qTranslate-X Support
226 elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
227 $current_language = qtranxf_getLanguage();
228 }
229 // Weglot Support
230 elseif ( function_exists( 'weglot_get_current_language' ) ) {
231 $current_language = weglot_get_current_language();
232 }
233 // TranslatePress Support
234 elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
235 $current_language = trp_get_current_language();
236 }
237
238 return $current_language;
239 }
240
241 /**
242 * Check if any multilingual plugin is active
243 *
244 * @return bool
245 */
246 public static function is_multilingual_active() {
247 return is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ||
248 function_exists( 'pll_current_language' ) ||
249 function_exists( 'qtranxf_getLanguage' ) ||
250 function_exists( 'weglot_get_current_language' ) ||
251 ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) );
252 }
253
254 /**
255 * Check if we should apply language filtering
256 * Only apply on frontend or when specifically requested
257 *
258 * @return bool
259 */
260 public static function should_apply_language_filtering() {
261 // Don't apply language filtering in admin context unless it's a frontend request
262 if ( is_admin() ) {
263 // Allow language filtering for REST API requests that are frontend-facing
264 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
265 // Check if this is a frontend REST request (not admin)
266 $request_uri = $_SERVER['REQUEST_URI'] ?? '';
267 // Don't filter admin REST requests for glossaries management
268 if ( strpos( $request_uri, '/wp/v2/glossaries' ) !== false ) {
269 return false; // Don't filter admin glossaries management
270 }
271 }
272 return false; // Don't filter other admin requests
273 }
274
275 // Apply filtering on frontend
276 return true;
277 }
278
279 /**
280 * Get current admin language for multilingual sites
281 * This is specifically for admin context where we need to detect
282 * the language being used for editing terms/posts
283 *
284 * @return string|null Current admin language code
285 */
286 public static function get_current_admin_language() {
287 $current_language = null;
288
289 // WPML Support - Admin language detection
290 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
291 global $sitepress;
292 if ( $sitepress && $sitepress->is_setup_complete() ) {
293 // For term editing, check if we have a specific term language
294 if ( isset( $_GET['tag_ID'] ) && function_exists( 'wpml_get_language_information' ) ) {
295 $term_info = wpml_get_language_information( null, (int) $_GET['tag_ID'] );
296 if ( ! is_wp_error( $term_info ) && $term_info && isset( $term_info['language_code'] ) ) {
297 $current_language = $term_info['language_code'];
298 }
299
300 }
301
302 // Check for language parameter in URL
303 if ( ! $current_language && isset( $_GET['lang'] ) ) {
304 $current_language = sanitize_text_field( $_GET['lang'] );
305 }
306
307 // Fallback to admin language or current language
308 if ( ! $current_language ) {
309 $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
310 }
311 }
312 }
313 // Polylang Support - Admin language detection
314 elseif ( function_exists( 'pll_current_language' ) ) {
315 // For term editing, get language from term ID
316 if ( isset( $_GET['tag_ID'] ) && function_exists( 'pll_get_term_language' ) ) {
317 $term_lang = pll_get_term_language( (int) $_GET['tag_ID'] );
318 if ( $term_lang ) {
319 $current_language = $term_lang;
320 }
321 }
322
323 // Check for language parameter in URL
324 if ( ! $current_language && isset( $_GET['lang'] ) ) {
325 $current_language = sanitize_text_field( $_GET['lang'] );
326 }
327
328 // Fallback to current admin language
329 if ( ! $current_language ) {
330 $current_language = pll_current_language( 'slug' );
331 }
332 }
333 // Other multilingual plugins
334 elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
335 $current_language = qtranxf_getLanguage();
336 }
337 elseif ( function_exists( 'weglot_get_current_language' ) ) {
338 $current_language = weglot_get_current_language();
339 }
340 elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
341 $current_language = trp_get_current_language();
342 }
343
344 return $current_language;
345 }
346
347 /**
348 * Generate language-specific meta key for category ordering
349 * Always falls back to base key if language-specific key doesn't exist
350 *
351 * @param string $base_key The base meta key (e.g., 'doc_category_order')
352 * @param string|null $language Language code, if null will auto-detect
353 * @return string Language-specific meta key or base key as fallback
354 */
355 public static function get_language_specific_meta_key( $base_key, $language = null ) {
356 // If no multilingual plugin is active, return the base key
357 if ( ! self::is_multilingual_active() ) {
358 return $base_key;
359 }
360
361 // Get current admin language if not provided
362 if ( $language === null ) {
363 $language = self::get_current_admin_language();
364 }
365
366 // If no language detected, return base key for backward compatibility
367 if ( ! $language ) {
368 return $base_key;
369 }
370
371 // Always return base key for now - we'll handle fallback in the query functions
372 // This ensures compatibility without requiring migration
373 return $base_key;
374 }
375
376 /**
377 * Get the appropriate meta key with fallback logic
378 * This function checks if language-specific meta exists, if not falls back to base key
379 *
380 * @param string $base_key The base meta key
381 * @param int $term_id The term ID to check
382 * @param string|null $language Language code
383 * @return string The meta key to use
384 */
385 public static function get_meta_key_with_fallback( $base_key, $term_id = null, $language = null ) {
386 // If no multilingual plugin is active, return the base key
387 if ( ! self::is_multilingual_active() ) {
388 return $base_key;
389 }
390
391 // Get current admin language if not provided
392 if ( $language === null ) {
393 $language = self::get_current_admin_language();
394 }
395
396 // If no language detected, return base key
397 if ( ! $language ) {
398 return $base_key;
399 }
400
401 $lang_meta_key = $base_key . '_' . $language;
402
403 // If we have a specific term ID, check if language-specific meta exists
404 if ( $term_id ) {
405 $lang_value = get_term_meta( $term_id, $lang_meta_key, true );
406 if ( ! empty( $lang_value ) ) {
407 return $lang_meta_key;
408 }
409 // Fall back to base key if language-specific doesn't exist
410 return $base_key;
411 }
412
413 // For queries without specific term ID, we need to check if ANY terms have language-specific meta
414 global $wpdb;
415 $has_lang_meta = $wpdb->get_var( $wpdb->prepare(
416 "SELECT COUNT(*) FROM {$wpdb->termmeta} tm
417 INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
418 WHERE tm.meta_key = %s AND tt.taxonomy = 'doc_category' AND tm.meta_value != ''",
419 $lang_meta_key
420 ) );
421
422 // If language-specific meta exists for some terms, use it (terms without it will have empty values)
423 // Otherwise, fall back to base key
424 return $has_lang_meta > 0 ? $lang_meta_key : $base_key;
425 }
426
427 /**
428 * Migrate existing category orders to language-specific meta keys
429 * This should be called when a multilingual plugin is activated
430 *
431 * @param string $base_key The base meta key (e.g., 'doc_category_order')
432 * @param string $taxonomy The taxonomy to migrate
433 * @return bool Success status
434 */
435 public static function migrate_category_orders_to_multilingual( $base_key = 'doc_category_order', $taxonomy = 'doc_category' ) {
436 // Only run if multilingual plugin is active
437 if ( ! self::is_multilingual_active() ) {
438 return false;
439 }
440
441 global $wpdb;
442
443 // Get all terms with the base meta key
444 $terms_with_order = $wpdb->get_results( $wpdb->prepare(
445 "SELECT tm.term_id, tm.meta_value, t.slug
446 FROM {$wpdb->termmeta} tm
447 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
448 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
449 WHERE tm.meta_key = %s AND tt.taxonomy = %s",
450 $base_key,
451 $taxonomy
452 ) );
453
454 if ( empty( $terms_with_order ) ) {
455 return true; // Nothing to migrate
456 }
457
458 // Get available languages
459 $languages = self::get_available_languages();
460
461 if ( empty( $languages ) ) {
462 return false; // No languages found
463 }
464
465 // Migrate orders for each language
466 foreach ( $languages as $language ) {
467 $language_meta_key = $base_key . '_' . $language;
468
469 foreach ( $terms_with_order as $term_data ) {
470 // Check if language-specific meta already exists
471 $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
472
473 if ( empty( $existing_value ) ) {
474 // Copy the base order to language-specific key
475 update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
476 }
477 }
478 }
479
480 return true;
481 }
482
483 /**
484 * Migrate existing document orders to language-specific meta keys
485 * This should be called when a multilingual plugin is activated
486 *
487 * @param string $base_key The base meta key (e.g., '_docs_order')
488 * @param string $taxonomy The taxonomy to migrate
489 * @return bool Success status
490 */
491 public static function migrate_docs_orders_to_multilingual( $base_key = '_docs_order', $taxonomy = 'doc_category' ) {
492 // Only run if multilingual plugin is active
493 if ( ! self::is_multilingual_active() ) {
494 return false;
495 }
496
497 global $wpdb;
498
499 // Get all terms with the base meta key for document ordering
500 $terms_with_docs_order = $wpdb->get_results( $wpdb->prepare(
501 "SELECT tm.term_id, tm.meta_value, t.slug
502 FROM {$wpdb->termmeta} tm
503 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
504 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
505 WHERE tm.meta_key = %s AND tt.taxonomy = %s AND tm.meta_value != ''",
506 $base_key,
507 $taxonomy
508 ) );
509
510 if ( empty( $terms_with_docs_order ) ) {
511 return true; // Nothing to migrate
512 }
513
514 // Get available languages
515 $languages = self::get_available_languages();
516
517 if ( empty( $languages ) ) {
518 return false; // No languages found
519 }
520
521 // Migrate document orders for each language
522 foreach ( $languages as $language ) {
523 $language_meta_key = $base_key . '_' . $language;
524
525 foreach ( $terms_with_docs_order as $term_data ) {
526 // Check if language-specific meta already exists
527 $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
528
529 if ( empty( $existing_value ) ) {
530 // Copy the base document order to language-specific key
531 update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
532 }
533 }
534 }
535
536 return true;
537 }
538
539 /**
540 * Migrate both category and document orders to multilingual format
541 * This is a convenience method that runs both migrations
542 *
543 * @return bool Success status
544 */
545 public static function migrate_all_orders_to_multilingual() {
546 $category_result = self::migrate_category_orders_to_multilingual();
547 $docs_result = self::migrate_docs_orders_to_multilingual();
548
549 return $category_result && $docs_result;
550 }
551
552 /**
553 * Get available languages from multilingual plugins
554 *
555 * @return array Array of language codes
556 */
557 public static function get_available_languages() {
558 $languages = [];
559
560 // WPML Support
561 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
562 global $sitepress;
563 if ( $sitepress && $sitepress->is_setup_complete() ) {
564 $active_languages = $sitepress->get_active_languages();
565 if ( is_array( $active_languages ) ) {
566 $languages = array_keys( $active_languages );
567 }
568 }
569 }
570 // Polylang Support
571 elseif ( function_exists( 'pll_languages_list' ) ) {
572 $languages = pll_languages_list();
573 }
574
575 return $languages;
576 }
577
578 public static function get_current_letter_docs( $current_letter, $limit = 0 ) {
579 global $wpdb;
580
581 $limit = absint( $limit );
582 $limit_sql = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
583
584 // Check if the encyclopedia_prefix parameter is set
585
586 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
587 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
588 $encyclopedia_root_slug = betterdocs()->settings->get( 'encyclopedia_root_slug', 'encyclopdia' );
589
590 // if($enable_glossaries && $encyclopeia_suorce === 'glossaries'){
591 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
592 $lang_join = '';
593 $lang_where = '';
594
595 // Add language filtering if multilingual plugin is active and we should apply filtering
596 $current_language = self::get_current_language();
597 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
598 // For WPML, use icl_translations table
599 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
600 $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'";
601 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
602 }
603 // For Polylang, use term_relationships with language taxonomy
604 elseif ( function_exists( 'pll_current_language' ) ) {
605 $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";
606 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
607 }
608 }
609
610 $query = "
611 SELECT
612 t.term_id,
613 t.name AS post_title,
614 t.slug as slug,
615 '' AS post_excerpt,
616 CONCAT('" . get_home_url() . "/$encyclopedia_root_slug/', t.slug) AS permalink,
617 tt.description AS post_content,
618 JSON_OBJECT(
619 'status', COALESCE(MAX(CASE WHEN m.meta_key = 'status' THEN m.meta_value END), ''),
620 'glossary_term_description', COALESCE(MAX(CASE WHEN m.meta_key = 'glossary_term_description' THEN m.meta_value END), '')
621 ) AS meta_data
622 FROM
623 {$wpdb->terms} t
624 INNER JOIN
625 {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
626 LEFT JOIN
627 {$wpdb->termmeta} m ON t.term_id = m.term_id
628 $lang_join
629 WHERE
630 tt.taxonomy = 'glossaries'
631 AND
632 SUBSTRING(t.name, 1, 1) = %s
633 $lang_where
634 GROUP BY
635 t.term_id
636 ORDER BY
637 t.name ASC
638 $limit_sql
639 ";
640 } else {
641 $lang_join = '';
642 $lang_where = '';
643
644 // Add language filtering for docs if multilingual plugin is active and we should apply filtering
645 $current_language = self::get_current_language();
646 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
647 // For WPML, use icl_translations table
648 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
649 $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'";
650 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
651 }
652 // For Polylang, use term_relationships with language taxonomy
653 elseif ( function_exists( 'pll_current_language' ) ) {
654 $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";
655 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
656 }
657 }
658
659 $query = "
660 SELECT ID, post_title, post_excerpt, guid, post_content
661 FROM {$wpdb->posts}
662 $lang_join
663 WHERE post_type = 'docs'
664 AND post_status = 'publish'
665 AND SUBSTRING(post_title, 1, 1) = %s
666 $lang_where
667 ORDER BY post_date DESC
668 $limit_sql
669 ";
670 }
671
672 $current_letter_docs = $wpdb->get_results( $wpdb->prepare( $query, $current_letter ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
673
674 return $current_letter_docs;
675 }
676
677 public static function docs_sort_by_letter( $limit = 10 ) {
678 global $wpdb;
679 $enable_non_latin = betterdocs()->settings->get( 'encyclopedia_enable_non_latin' );
680 $script = betterdocs()->settings->get( 'encyclopedia_non_latin_option' );
681 $letters = Helper::get_character_range( $enable_non_latin, $script );
682
683 $docs_by_letter = [];
684 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
685 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
686
687 foreach ( $letters as $letter ) {
688 $posts = self::get_current_letter_docs( $letter, $limit );
689
690 if ( is_array( $posts ) && ! empty( $posts ) ) {
691 foreach ( $posts as $post ) {
692 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
693 $glossary_term_description = $description['glossary_term_description'] ?? '';
694
695 // Remove any <p> tags or other unwanted HTML tags
696 $glossary_term_description = strip_tags( $glossary_term_description );
697 $post_excerpt = strip_tags( $post['post_excerpt'] ?? '' );
698
699 // Prepare post data
700 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
701 // For glossaries
702 $post_data = [
703 'id' => $post['term_id'] ?? '',
704 'post_title' => $post['post_title'] ?? '',
705 'post_excerpt' => ! empty( $post_excerpt )
706 ? $post_excerpt
707 : ( ! empty( $glossary_term_description )
708 ? self::get_custom_excerpt( $glossary_term_description, 15 )
709 : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ) ),
710 'permalink' => isset( $post['slug'] ) ? get_term_link( $post['slug'], 'glossaries' ) : ''
711 ];
712 } else {
713 // For docs
714 $post_data = [
715 'id' => $post['ID'] ?? '',
716 'post_title' => $post['post_title'] ?? '',
717 'post_excerpt' => ! empty( $post_excerpt )
718 ? $post_excerpt
719 : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ),
720 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
721 ];
722 }
723
724 $docs_by_letter[$letter][] = $post_data;
725 }
726 }
727 }
728
729 return $docs_by_letter;
730 }
731
732 public static function get_glossaries() {
733 global $wpdb;
734
735 $lang_join = '';
736 $lang_where = '';
737
738 // Add language filtering if multilingual plugin is active and we should apply filtering
739 $current_language = self::get_current_language();
740 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
741 // For WPML, use icl_translations table
742 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
743 $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'";
744 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
745 }
746 // For Polylang, use term_relationships with language taxonomy
747 elseif ( function_exists( 'pll_current_language' ) ) {
748 $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";
749 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
750 }
751 }
752
753 $query = "
754 SELECT t.name
755 FROM {$wpdb->terms} t
756 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
757 $lang_join
758 WHERE tt.taxonomy = 'glossaries'
759 $lang_where
760 ORDER BY t.name ASC
761 ";
762
763 $glossaries = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
764
765 return $glossaries;
766 }
767
768 /**
769 * Determine live search template layout, when live search is not selected from customizer(this will work when live search template is not selected from customizer)
770 *
771 * @param string $layout
772 * @return string $layout
773 */
774 public static function determine_search_layout( $layout ) {
775 if ( $layout ) {
776 return $layout;
777 }
778
779 $search_layout = betterdocs()->customizer->defaults->get( 'betterdocs_search_layout_select' );
780 $docs_layout = betterdocs()->customizer->defaults->get( 'betterdocs_docs_layout_select' );
781 $archive_page_layout = betterdocs()->customizer->defaults->get( 'betterdocs_archive_layout_select' );
782 $single_layout = betterdocs()->customizer->defaults->get( 'betterdocs_single_layout_select' );
783
784 if ( is_post_type_archive( 'docs' ) ) {
785 if ( $docs_layout != "layout-7" && ! $search_layout ) {
786 $layout = 'layout-1';
787 } else if ( $docs_layout == 'layout-7' && ! $search_layout ) {
788 $layout = 'layout-2';
789 }
790 } else if ( is_tax( 'doc_tag' ) && ! $search_layout ) {
791 $layout = 'layout-1';
792 } else if ( is_tax( 'doc_category' ) ) {
793 if ( $archive_page_layout != 'layout-7' && $archive_page_layout != 'layout-8' && ! $search_layout ) {
794 $layout = 'layout-1';
795 } else if ( ( $archive_page_layout == 'layout-7' && ! $search_layout ) || ( $archive_page_layout == 'layout-8' && ! $search_layout ) ) {
796 $layout = 'layout-2';
797 }
798 } else if ( is_singular( 'docs' ) ) {
799 if ( $single_layout != 'layout-8' && $single_layout != 'layout-9' && ! $search_layout ) {
800 $layout = 'layout-1';
801 } else if ( ( $single_layout == 'layout-8' && ! $search_layout ) || ( $single_layout == 'layout-9' && ! $search_layout ) ) {
802 $layout = 'layout-2';
803 }
804 }
805
806 return $layout;
807 }
808 public static function mb_ord_fallback( $char ) {
809 $code = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) );
810 return $code[1];
811 }
812
813 public static function mb_chr_fallback( $code ) {
814 return mb_convert_encoding( pack( 'N', $code ), 'UTF-8', 'UCS-4BE' );
815 }
816
817 public static function unicodeRange( $start, $end ) {
818 $range = [];
819 for ( $i = self::mb_ord_fallback( $start ); $i <= self::mb_ord_fallback( $end ); $i++ ) {
820 $range[] = self::mb_chr_fallback( $i );
821 }
822 return $range;
823 }
824
825 public static function get_character_range( $enable_non_latin, $script ) {
826 if ( $enable_non_latin ) {
827 switch ( $script ) {
828 case 'arabic':
829 return self::unicodeRange( 'ء', 'ي' );
830 case 'cyrillic':
831 return self::unicodeRange( 'А', 'Я' );
832 case 'hebrew':
833 return self::unicodeRange( 'א', 'ת' );
834 case 'greek':
835 return self::unicodeRange( 'Α', 'Ω' );
836 default:
837 return range( 'A', 'Z' );
838 }
839 }
840
841 return range( 'A', 'Z' );
842 }
843
844 public static function get_the_top_most_parent( $term_id ) {
845 while ( $term_id != 0 ) {
846 $parent_id = wp_get_term_taxonomy_parent_id( $term_id, 'doc_category' );
847
848 if ( $parent_id == 0 ) {
849 break;
850 }
851
852 $term_id = $parent_id;
853 }
854 return $term_id;
855 }
856
857 public static function get_highest_docs_term() {
858 $terms = get_terms( [
859 'taxonomy' => 'doc_category', // Change to your desired taxonomy
860 'hide_empty' => true, // Only show terms with posts
861 'orderby' => 'count', // Order by post count
862 'order' => 'DESC', // Descending order
863 'number' => 1 // Get only the top term
864 ] );
865 return isset( $terms[0] ) ? $terms[0] : [];
866 }
867
868 public static function delete_specific_faq_posts_by_faq_category( $term_id ) {
869 $args = [
870 'post_type' => 'betterdocs_faq',
871 'posts_per_page' => -1,
872 'tax_query' => [
873 [
874 'taxonomy' => 'betterdocs_faq_category',
875 'field' => 'id',
876 'terms' => $term_id,
877 'operator' => 'IN'
878 ]
879 ],
880 'fields' => 'ids'
881 ];
882
883 $query = new \WP_Query( $args );
884
885 if ( $query->have_posts() ) {
886 foreach ( $query->posts as $doc_id ) {
887 wp_delete_post( $doc_id, true );
888 }
889 }
890 }
891
892 /**
893 * Function To Normalize Repeater Field For Quick Builder
894 *
895 * @param array $fields
896 * @param array $include_field_keys
897 *
898 * @return array
899 */
900 public static function normalize_repeater_field( $fields, $include_field_keys = [] ) {
901 if( empty( $include_field_keys ) ) {
902 return $fields;
903 }
904
905 $normalized_fields = [];
906
907 foreach( $fields as $field ) {
908 foreach( $include_field_keys as $field_key ) {
909 if( ! isset( $normalized_fields[$field_key] ) ) {
910 $normalized_fields[$field_key] = isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [];
911 } else {
912 array_push( $normalized_fields[$field_key], ...( isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [] ) );
913 $normalized_fields[$field_key] = array_unique( $normalized_fields[$field_key] );
914 }
915 }
916 }
917
918 return $normalized_fields;
919 }
920
921 public static function get_local_plugin_data( $basename = '' ) {
922 if ( empty( $basename ) ) {
923 return false;
924 }
925
926 if ( !function_exists( 'get_plugins' ) ) {
927 include_once ABSPATH . 'wp-admin/includes/plugin.php';
928 }
929
930 $plugins = get_plugins();
931
932 if ( !isset( $plugins[ $basename ] ) ) {
933 return false;
934 }
935
936 return $plugins[ $basename ];
937 }
938
939 /**
940 * Get default file icon based on programming language
941 *
942 * @param string $language Programming language identifier
943 * @return string Emoji icon for the language
944 */
945 public static function get_file_icon_by_language( $language ) {
946 $icons = [
947 'javascript' => '📄',
948 'typescript' => '📘',
949 'jsx' => '⚛️',
950 'tsx' => '⚛️',
951 'html' => '🌐',
952 'css' => '🎨',
953 'scss' => '🎨',
954 'sass' => '🎨',
955 'less' => '🎨',
956 'php' => '🐘',
957 'python' => '🐍',
958 'java' => '',
959 'csharp' => '🔷',
960 'cpp' => '⚙️',
961 'c' => '⚙️',
962 'ruby' => '💎',
963 'go' => '🐹',
964 'rust' => '🦀',
965 'swift' => '🦉',
966 'kotlin' => '🎯',
967 'sql' => '🗃️',
968 'json' => '📋',
969 'yaml' => '📋',
970 'xml' => '📄',
971 'markdown' => '📝',
972 'bash' => '💻',
973 'shell' => '💻',
974 'powershell' => '💻',
975 'dockerfile' => '🐳',
976 ];
977
978 return isset( $icons[$language] ) ? $icons[$language] : '📄';
979 }
980
981 /**
982 * Check if AI Chatbot is enabled
983 *
984 * @return bool
985 */
986 public function is_ai_chatbot_enabled() {
987 $chatbot_active = is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
988 $chatbot_license_valid = get_option( 'betterdocs_chatbot_software__license_status' ) === 'valid';
989 $chatbot_enabled = betterdocs()->settings->get( 'enable_ai_chatbot', false );
990
991 // AI Search Suggestions are enabled if all conditions are met
992 return $chatbot_active && $chatbot_license_valid && $chatbot_enabled;
993 }
994
995 /**
996 * Check if tags are enabled and post has tags
997 *
998 * @return bool
999 */
1000 public function is_tag_enabled() {
1001 global $post;
1002 $product_terms = wp_get_object_terms( $post->ID, 'doc_tag' );
1003 $enable_tags = betterdocs()->settings->get( 'enable_tags', false );
1004 return ! empty( $product_terms ) && $enable_tags;
1005 }
1006
1007 /**
1008 * Check if AI Search Suggestions are enabled
1009 *
1010 * @return bool
1011 */
1012 public function is_ai_search_suggestions_enabled() {
1013 $ai_search_suggestions_active = is_plugin_active( 'betterdocs-ai-search-suggestions/betterdocs-ai-search-suggestions.php' );
1014 $ai_search_suggestions_license_valid = get_option( 'betterdocs_ai_search_suggestions_software__license_status' ) === 'valid';
1015 $ai_search_suggestions_enabled = betterdocs()->settings->get( 'enable_ai_powered_search', false );
1016
1017 return $ai_search_suggestions_active && $ai_search_suggestions_license_valid && $ai_search_suggestions_enabled;
1018 }
1019
1020 /**
1021 * Get the maximum order value from the 'doc_category_order' term meta
1022 *
1023 * @return int
1024 */
1025 public static function get_max_doc_category_order_from_term_meta() {
1026 global $wpdb;
1027 $sql = $wpdb->prepare( "SELECT MAX(CAST(meta_value AS UNSIGNED)) AS max FROM {$wpdb->prefix}termmeta WHERE meta_key = %s ", 'doc_category_order');
1028 $result = $wpdb->get_var($sql);
1029 return $result;
1030 }
1031 }
1032