PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.4
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.5.4, at includes/Utils/Helper.php

1,091 lines 37.6 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 // Explicit language passed by the admin client takes priority.
290 // Covers AJAX (POST) and REST/admin requests (GET) where WPML may
291 // otherwise resolve to the site's default language instead of the
292 // admin UI language.
293 if ( isset( $_POST['lang'] ) && ! empty( $_POST['lang'] ) ) {
294 return sanitize_text_field( wp_unslash( $_POST['lang'] ) );
295 }
296
297 // Limit GET handling to admin/REST contexts so a frontend ?lang= switch
298 // doesn't hijack admin meta-key resolution.
299 if ( isset( $_GET['lang'] ) && ! empty( $_GET['lang'] )
300 && ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) ) {
301 return sanitize_text_field( wp_unslash( $_GET['lang'] ) );
302 }
303
304 // WPML Support - Admin language detection
305 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
306 global $sitepress;
307 if ( $sitepress && $sitepress->is_setup_complete() ) {
308 // For term editing, check if we have a specific term language
309 if ( isset( $_GET['tag_ID'] ) && function_exists( 'wpml_get_language_information' ) ) {
310 $term_info = wpml_get_language_information( null, (int) $_GET['tag_ID'] );
311 if ( ! is_wp_error( $term_info ) && $term_info && isset( $term_info['language_code'] ) ) {
312 $current_language = $term_info['language_code'];
313 }
314
315 }
316
317 // Check for language parameter in URL
318 if ( ! $current_language && isset( $_GET['lang'] ) ) {
319 $current_language = sanitize_text_field( $_GET['lang'] );
320 }
321
322 // Check WPML admin language cookie (persists during AJAX)
323 if ( ! $current_language && isset( $_COOKIE['_icl_current_admin_language'] ) ) {
324 $current_language = sanitize_text_field( wp_unslash( $_COOKIE['_icl_current_admin_language'] ) );
325 }
326
327 // Fallback to admin language or current language
328 if ( ! $current_language ) {
329 $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
330 }
331 }
332 }
333 // Polylang Support - Admin language detection
334 elseif ( function_exists( 'pll_current_language' ) ) {
335 // For term editing, get language from term ID
336 if ( isset( $_GET['tag_ID'] ) && function_exists( 'pll_get_term_language' ) ) {
337 $term_lang = pll_get_term_language( (int) $_GET['tag_ID'] );
338 if ( $term_lang ) {
339 $current_language = $term_lang;
340 }
341 }
342
343 // Check for language parameter in URL
344 if ( ! $current_language && isset( $_GET['lang'] ) ) {
345 $current_language = sanitize_text_field( $_GET['lang'] );
346 }
347
348 // Fallback to current admin language
349 if ( ! $current_language ) {
350 $current_language = pll_current_language( 'slug' );
351 }
352 }
353 // Other multilingual plugins
354 elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
355 $current_language = qtranxf_getLanguage();
356 }
357 elseif ( function_exists( 'weglot_get_current_language' ) ) {
358 $current_language = weglot_get_current_language();
359 }
360 elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
361 $current_language = trp_get_current_language();
362 }
363
364 return $current_language;
365 }
366
367 /**
368 * Generate language-specific meta key for category ordering
369 * Always falls back to base key if language-specific key doesn't exist
370 *
371 * @param string $base_key The base meta key (e.g., 'doc_category_order')
372 * @param string|null $language Language code, if null will auto-detect
373 * @return string Language-specific meta key or base key as fallback
374 */
375 public static function get_language_specific_meta_key( $base_key, $language = null ) {
376 // If no multilingual plugin is active, return the base key
377 if ( ! self::is_multilingual_active() ) {
378 return $base_key;
379 }
380
381 // Get current admin language if not provided
382 if ( $language === null ) {
383 $language = self::get_current_admin_language();
384 }
385
386 // If no language detected, return base key for backward compatibility
387 if ( ! $language ) {
388 return $base_key;
389 }
390
391 // Always return base key for now - we'll handle fallback in the query functions
392 // This ensures compatibility without requiring migration
393 return $base_key;
394 }
395
396 /**
397 * Get the meta key to write to.
398 *
399 * Unlike `get_meta_key_with_fallback`, this never falls back to the base
400 * key when the language-specific key is empty — that fallback is what
401 * caused secondary-language drag-and-drop saves to clobber the base meta
402 * (and on WPML setups that copy term meta from the original language,
403 * the next read would re-overwrite it from the primary language).
404 *
405 * @param string $base_key The base meta key.
406 * @param string|null $language Language code, auto-detected when null.
407 * @return string Language-specific key when multilingual + language known, else base.
408 */
409 public static function get_meta_key_for_save( $base_key, $language = null ) {
410 if ( ! self::is_multilingual_active() ) {
411 return $base_key;
412 }
413
414 if ( $language === null ) {
415 $language = self::get_current_admin_language();
416 }
417
418 if ( ! $language ) {
419 return $base_key;
420 }
421
422 return $base_key . '_' . $language;
423 }
424
425 /**
426 * Get the appropriate meta key with fallback logic
427 * This function checks if language-specific meta exists, if not falls back to base key
428 *
429 * @param string $base_key The base meta key
430 * @param int $term_id The term ID to check
431 * @param string|null $language Language code
432 * @return string The meta key to use
433 */
434 public static function get_meta_key_with_fallback( $base_key, $term_id = null, $language = null ) {
435 // If no multilingual plugin is active, return the base key
436 if ( ! self::is_multilingual_active() ) {
437 return $base_key;
438 }
439
440 // Get current admin language if not provided
441 if ( $language === null ) {
442 $language = self::get_current_admin_language();
443 }
444
445 // If no language detected, return base key
446 if ( ! $language ) {
447 return $base_key;
448 }
449
450 $lang_meta_key = $base_key . '_' . $language;
451
452 // If we have a specific term ID, check if language-specific meta exists
453 if ( $term_id ) {
454 $lang_value = get_term_meta( $term_id, $lang_meta_key, true );
455 if ( ! empty( $lang_value ) ) {
456 return $lang_meta_key;
457 }
458 // Fall back to base key if language-specific doesn't exist
459 return $base_key;
460 }
461
462 // For queries without specific term ID, we need to check if ANY terms have language-specific meta
463 global $wpdb;
464 $has_lang_meta = $wpdb->get_var( $wpdb->prepare(
465 "SELECT COUNT(*) FROM {$wpdb->termmeta} tm
466 INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
467 WHERE tm.meta_key = %s AND tt.taxonomy = 'doc_category' AND tm.meta_value != ''",
468 $lang_meta_key
469 ) );
470
471 // If language-specific meta exists for some terms, use it (terms without it will have empty values)
472 // Otherwise, fall back to base key
473 return $has_lang_meta > 0 ? $lang_meta_key : $base_key;
474 }
475
476 /**
477 * Migrate existing category orders to language-specific meta keys
478 * This should be called when a multilingual plugin is activated
479 *
480 * @param string $base_key The base meta key (e.g., 'doc_category_order')
481 * @param string $taxonomy The taxonomy to migrate
482 * @return bool Success status
483 */
484 public static function migrate_category_orders_to_multilingual( $base_key = 'doc_category_order', $taxonomy = 'doc_category' ) {
485 // Only run if multilingual plugin is active
486 if ( ! self::is_multilingual_active() ) {
487 return false;
488 }
489
490 global $wpdb;
491
492 // Get all terms with the base meta key
493 $terms_with_order = $wpdb->get_results( $wpdb->prepare(
494 "SELECT tm.term_id, tm.meta_value, t.slug
495 FROM {$wpdb->termmeta} tm
496 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
497 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
498 WHERE tm.meta_key = %s AND tt.taxonomy = %s",
499 $base_key,
500 $taxonomy
501 ) );
502
503 if ( empty( $terms_with_order ) ) {
504 return true; // Nothing to migrate
505 }
506
507 // Get available languages
508 $languages = self::get_available_languages();
509
510 if ( empty( $languages ) ) {
511 return false; // No languages found
512 }
513
514 // Migrate orders for each language
515 foreach ( $languages as $language ) {
516 $language_meta_key = $base_key . '_' . $language;
517
518 foreach ( $terms_with_order as $term_data ) {
519 // Check if language-specific meta already exists
520 $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
521
522 if ( empty( $existing_value ) ) {
523 // Copy the base order to language-specific key
524 update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
525 }
526 }
527 }
528
529 return true;
530 }
531
532 /**
533 * Migrate existing document orders to language-specific meta keys
534 * This should be called when a multilingual plugin is activated
535 *
536 * @param string $base_key The base meta key (e.g., '_docs_order')
537 * @param string $taxonomy The taxonomy to migrate
538 * @return bool Success status
539 */
540 public static function migrate_docs_orders_to_multilingual( $base_key = '_docs_order', $taxonomy = 'doc_category' ) {
541 // Only run if multilingual plugin is active
542 if ( ! self::is_multilingual_active() ) {
543 return false;
544 }
545
546 global $wpdb;
547
548 // Get all terms with the base meta key for document ordering
549 $terms_with_docs_order = $wpdb->get_results( $wpdb->prepare(
550 "SELECT tm.term_id, tm.meta_value, t.slug
551 FROM {$wpdb->termmeta} tm
552 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
553 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
554 WHERE tm.meta_key = %s AND tt.taxonomy = %s AND tm.meta_value != ''",
555 $base_key,
556 $taxonomy
557 ) );
558
559 if ( empty( $terms_with_docs_order ) ) {
560 return true; // Nothing to migrate
561 }
562
563 // Get available languages
564 $languages = self::get_available_languages();
565
566 if ( empty( $languages ) ) {
567 return false; // No languages found
568 }
569
570 // Migrate document orders for each language
571 foreach ( $languages as $language ) {
572 $language_meta_key = $base_key . '_' . $language;
573
574 foreach ( $terms_with_docs_order as $term_data ) {
575 // Check if language-specific meta already exists
576 $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
577
578 if ( empty( $existing_value ) ) {
579 // Copy the base document order to language-specific key
580 update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
581 }
582 }
583 }
584
585 return true;
586 }
587
588 /**
589 * Migrate both category and document orders to multilingual format
590 * This is a convenience method that runs both migrations
591 *
592 * @return bool Success status
593 */
594 public static function migrate_all_orders_to_multilingual() {
595 $category_result = self::migrate_category_orders_to_multilingual();
596 $docs_result = self::migrate_docs_orders_to_multilingual();
597
598 return $category_result && $docs_result;
599 }
600
601 /**
602 * Get available languages from multilingual plugins
603 *
604 * @return array Array of language codes
605 */
606 public static function get_available_languages() {
607 $languages = [];
608
609 // WPML Support
610 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
611 global $sitepress;
612 if ( $sitepress && $sitepress->is_setup_complete() ) {
613 $active_languages = $sitepress->get_active_languages();
614 if ( is_array( $active_languages ) ) {
615 $languages = array_keys( $active_languages );
616 }
617 }
618 }
619 // Polylang Support
620 elseif ( function_exists( 'pll_languages_list' ) ) {
621 $languages = pll_languages_list();
622 }
623
624 return $languages;
625 }
626
627 public static function get_current_letter_docs( $current_letter, $limit = 0 ) {
628 global $wpdb;
629
630 $limit = absint( $limit );
631 $limit_sql = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
632
633 // Check if the encyclopedia_prefix parameter is set
634
635 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
636 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
637 $encyclopedia_root_slug = betterdocs()->settings->get( 'encyclopedia_root_slug', 'encyclopdia' );
638
639 // if($enable_glossaries && $encyclopeia_suorce === 'glossaries'){
640 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
641 $lang_join = '';
642 $lang_where = '';
643
644 // Add language filtering 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 = t.term_id AND icl_t.element_type = 'tax_glossaries'";
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 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";
655 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
656 }
657 }
658
659 $query = "
660 SELECT
661 t.term_id,
662 t.name AS post_title,
663 t.slug as slug,
664 '' AS post_excerpt,
665 CONCAT('" . get_home_url() . "/$encyclopedia_root_slug/', t.slug) AS permalink,
666 tt.description AS post_content,
667 JSON_OBJECT(
668 'status', COALESCE(MAX(CASE WHEN m.meta_key = 'status' THEN m.meta_value END), ''),
669 'glossary_term_description', COALESCE(MAX(CASE WHEN m.meta_key = 'glossary_term_description' THEN m.meta_value END), '')
670 ) AS meta_data
671 FROM
672 {$wpdb->terms} t
673 INNER JOIN
674 {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
675 LEFT JOIN
676 {$wpdb->termmeta} m ON t.term_id = m.term_id
677 $lang_join
678 WHERE
679 tt.taxonomy = 'glossaries'
680 AND
681 SUBSTRING(t.name, 1, 1) = %s
682 $lang_where
683 GROUP BY
684 t.term_id
685 ORDER BY
686 t.name ASC
687 $limit_sql
688 ";
689 } else {
690 $lang_join = '';
691 $lang_where = '';
692
693 // Add language filtering for docs if multilingual plugin is active and we should apply filtering
694 $current_language = self::get_current_language();
695 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
696 // For WPML, use icl_translations table
697 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
698 $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'";
699 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
700 }
701 // For Polylang, use term_relationships with language taxonomy
702 elseif ( function_exists( 'pll_current_language' ) ) {
703 $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";
704 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
705 }
706 }
707
708 $query = "
709 SELECT ID, post_title, post_excerpt, guid, post_content
710 FROM {$wpdb->posts}
711 $lang_join
712 WHERE post_type = 'docs'
713 AND post_status = 'publish'
714 AND SUBSTRING(post_title, 1, 1) = %s
715 $lang_where
716 ORDER BY post_date DESC
717 $limit_sql
718 ";
719 }
720
721 $current_letter_docs = $wpdb->get_results( $wpdb->prepare( $query, $current_letter ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
722
723 return $current_letter_docs;
724 }
725
726 public static function docs_sort_by_letter( $limit = 10 ) {
727 global $wpdb;
728 $enable_non_latin = betterdocs()->settings->get( 'encyclopedia_enable_non_latin' );
729 $script = betterdocs()->settings->get( 'encyclopedia_non_latin_option' );
730 $letters = Helper::get_character_range( $enable_non_latin, $script );
731
732 $docs_by_letter = [];
733 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
734 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
735
736 foreach ( $letters as $letter ) {
737 $posts = self::get_current_letter_docs( $letter, $limit );
738
739 if ( is_array( $posts ) && ! empty( $posts ) ) {
740 foreach ( $posts as $post ) {
741 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
742 $glossary_term_description = $description['glossary_term_description'] ?? '';
743
744 // Remove any <p> tags or other unwanted HTML tags
745 $glossary_term_description = strip_tags( $glossary_term_description );
746 $post_excerpt = strip_tags( $post['post_excerpt'] ?? '' );
747
748 // Prepare post data
749 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
750 // For glossaries
751 $permalink = '';
752
753 if ( isset( $post['slug'] ) ) {
754 $term_link = get_term_link( $post['slug'], 'glossaries' );
755
756 if ( ! is_wp_error( $term_link ) ) {
757 $permalink = $term_link;
758 }
759 }
760
761 $post_data = [
762 'id' => $post['term_id'] ?? '',
763 'post_title' => $post['post_title'] ?? '',
764 'post_excerpt' => ! empty( $post_excerpt )
765 ? $post_excerpt
766 : ( ! empty( $glossary_term_description )
767 ? self::get_custom_excerpt( $glossary_term_description, 15 )
768 : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ) ),
769 'permalink' => $permalink,
770 ];
771 } else {
772 // For docs
773 $post_data = [
774 'id' => $post['ID'] ?? '',
775 'post_title' => $post['post_title'] ?? '',
776 'post_excerpt' => ! empty( $post_excerpt )
777 ? $post_excerpt
778 : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ),
779 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
780 ];
781 }
782
783 $docs_by_letter[$letter][] = $post_data;
784 }
785 }
786 }
787
788 return $docs_by_letter;
789 }
790
791 public static function get_glossaries() {
792 global $wpdb;
793
794 $lang_join = '';
795 $lang_where = '';
796
797 // Add language filtering if multilingual plugin is active and we should apply filtering
798 $current_language = self::get_current_language();
799 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
800 // For WPML, use icl_translations table
801 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
802 $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'";
803 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
804 }
805 // For Polylang, use term_relationships with language taxonomy
806 elseif ( function_exists( 'pll_current_language' ) ) {
807 $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";
808 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
809 }
810 }
811
812 $query = "
813 SELECT t.name
814 FROM {$wpdb->terms} t
815 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
816 $lang_join
817 WHERE tt.taxonomy = 'glossaries'
818 $lang_where
819 ORDER BY t.name ASC
820 ";
821
822 $glossaries = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
823
824 return $glossaries;
825 }
826
827 /**
828 * 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)
829 *
830 * @param string $layout
831 * @return string $layout
832 */
833 public static function determine_search_layout( $layout ) {
834 if ( $layout ) {
835 return $layout;
836 }
837
838 $search_layout = betterdocs()->customizer->defaults->get( 'betterdocs_search_layout_select' );
839 $docs_layout = betterdocs()->customizer->defaults->get( 'betterdocs_docs_layout_select' );
840 $archive_page_layout = betterdocs()->customizer->defaults->get( 'betterdocs_archive_layout_select' );
841 $single_layout = betterdocs()->customizer->defaults->get( 'betterdocs_single_layout_select' );
842
843 if ( is_post_type_archive( 'docs' ) ) {
844 if ( $docs_layout != "layout-7" && ! $search_layout ) {
845 $layout = 'layout-1';
846 } else if ( $docs_layout == 'layout-7' && ! $search_layout ) {
847 $layout = 'layout-2';
848 }
849 } else if ( is_tax( 'doc_tag' ) && ! $search_layout ) {
850 $layout = 'layout-1';
851 } else if ( is_tax( 'doc_category' ) ) {
852 if ( $archive_page_layout != 'layout-7' && $archive_page_layout != 'layout-8' && ! $search_layout ) {
853 $layout = 'layout-1';
854 } else if ( ( $archive_page_layout == 'layout-7' && ! $search_layout ) || ( $archive_page_layout == 'layout-8' && ! $search_layout ) ) {
855 $layout = 'layout-2';
856 }
857 } else if ( is_singular( 'docs' ) ) {
858 if ( $single_layout != 'layout-8' && $single_layout != 'layout-9' && ! $search_layout ) {
859 $layout = 'layout-1';
860 } else if ( ( $single_layout == 'layout-8' && ! $search_layout ) || ( $single_layout == 'layout-9' && ! $search_layout ) ) {
861 $layout = 'layout-2';
862 }
863 }
864
865 return $layout;
866 }
867 public static function mb_ord_fallback( $char ) {
868 $code = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) );
869 return $code[1];
870 }
871
872 public static function mb_chr_fallback( $code ) {
873 return mb_convert_encoding( pack( 'N', $code ), 'UTF-8', 'UCS-4BE' );
874 }
875
876 public static function unicodeRange( $start, $end ) {
877 $range = [];
878 for ( $i = self::mb_ord_fallback( $start ); $i <= self::mb_ord_fallback( $end ); $i++ ) {
879 $range[] = self::mb_chr_fallback( $i );
880 }
881 return $range;
882 }
883
884 public static function get_character_range( $enable_non_latin, $script ) {
885 if ( $enable_non_latin ) {
886 switch ( $script ) {
887 case 'arabic':
888 return self::unicodeRange( 'ء', 'ي' );
889 case 'cyrillic':
890 return self::unicodeRange( 'А', 'Я' );
891 case 'hebrew':
892 return self::unicodeRange( 'א', 'ת' );
893 case 'greek':
894 return self::unicodeRange( 'Α', 'Ω' );
895 default:
896 return range( 'A', 'Z' );
897 }
898 }
899
900 return range( 'A', 'Z' );
901 }
902
903 public static function get_the_top_most_parent( $term_id ) {
904 while ( $term_id != 0 ) {
905 $parent_id = wp_get_term_taxonomy_parent_id( $term_id, 'doc_category' );
906
907 if ( $parent_id == 0 ) {
908 break;
909 }
910
911 $term_id = $parent_id;
912 }
913 return $term_id;
914 }
915
916 public static function get_highest_docs_term() {
917 $terms = get_terms( [
918 'taxonomy' => 'doc_category', // Change to your desired taxonomy
919 'hide_empty' => true, // Only show terms with posts
920 'orderby' => 'count', // Order by post count
921 'order' => 'DESC', // Descending order
922 'number' => 1 // Get only the top term
923 ] );
924 return isset( $terms[0] ) ? $terms[0] : [];
925 }
926
927 public static function delete_specific_faq_posts_by_faq_category( $term_id ) {
928 $args = [
929 'post_type' => 'betterdocs_faq',
930 'posts_per_page' => -1,
931 'tax_query' => [
932 [
933 'taxonomy' => 'betterdocs_faq_category',
934 'field' => 'id',
935 'terms' => $term_id,
936 'operator' => 'IN'
937 ]
938 ],
939 'fields' => 'ids'
940 ];
941
942 $query = new \WP_Query( $args );
943
944 if ( $query->have_posts() ) {
945 foreach ( $query->posts as $doc_id ) {
946 wp_delete_post( $doc_id, true );
947 }
948 }
949 }
950
951 /**
952 * Function To Normalize Repeater Field For Quick Builder
953 *
954 * @param array $fields
955 * @param array $include_field_keys
956 *
957 * @return array
958 */
959 public static function normalize_repeater_field( $fields, $include_field_keys = [] ) {
960 if( empty( $include_field_keys ) ) {
961 return $fields;
962 }
963
964 $normalized_fields = [];
965
966 foreach( $fields as $field ) {
967 foreach( $include_field_keys as $field_key ) {
968 if( ! isset( $normalized_fields[$field_key] ) ) {
969 $normalized_fields[$field_key] = isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [];
970 } else {
971 array_push( $normalized_fields[$field_key], ...( isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [] ) );
972 $normalized_fields[$field_key] = array_unique( $normalized_fields[$field_key] );
973 }
974 }
975 }
976
977 return $normalized_fields;
978 }
979
980 public static function get_local_plugin_data( $basename = '' ) {
981 if ( empty( $basename ) ) {
982 return false;
983 }
984
985 if ( !function_exists( 'get_plugins' ) ) {
986 include_once ABSPATH . 'wp-admin/includes/plugin.php';
987 }
988
989 $plugins = get_plugins();
990
991 if ( !isset( $plugins[ $basename ] ) ) {
992 return false;
993 }
994
995 return $plugins[ $basename ];
996 }
997
998 /**
999 * Get default file icon based on programming language
1000 *
1001 * @param string $language Programming language identifier
1002 * @return string Emoji icon for the language
1003 */
1004 public static function get_file_icon_by_language( $language ) {
1005 $icons = [
1006 'javascript' => '📄',
1007 'typescript' => '📘',
1008 'jsx' => '⚛️',
1009 'tsx' => '⚛️',
1010 'html' => '🌐',
1011 'css' => '🎨',
1012 'scss' => '🎨',
1013 'sass' => '🎨',
1014 'less' => '🎨',
1015 'php' => '🐘',
1016 'python' => '🐍',
1017 'java' => '',
1018 'csharp' => '🔷',
1019 'cpp' => '⚙️',
1020 'c' => '⚙️',
1021 'ruby' => '💎',
1022 'go' => '🐹',
1023 'rust' => '🦀',
1024 'swift' => '🦉',
1025 'kotlin' => '🎯',
1026 'sql' => '🗃️',
1027 'json' => '📋',
1028 'yaml' => '📋',
1029 'xml' => '📄',
1030 'markdown' => '📝',
1031 'bash' => '💻',
1032 'shell' => '💻',
1033 'powershell' => '💻',
1034 'dockerfile' => '🐳',
1035 ];
1036
1037 return isset( $icons[$language] ) ? $icons[$language] : '📄';
1038 }
1039
1040 /**
1041 * Check if AI Chatbot is enabled
1042 *
1043 * @return bool
1044 */
1045 public function is_ai_chatbot_enabled() {
1046 $chatbot_active = is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
1047 $chatbot_license_valid = get_option( 'betterdocs_chatbot_software__license_status' ) === 'valid';
1048 $chatbot_enabled = betterdocs()->settings->get( 'enable_ai_chatbot', false );
1049
1050 // AI Search Suggestions are enabled if all conditions are met
1051 return $chatbot_active && $chatbot_license_valid && $chatbot_enabled;
1052 }
1053
1054 /**
1055 * Check if tags are enabled and post has tags
1056 *
1057 * @return bool
1058 */
1059 public function is_tag_enabled() {
1060 global $post;
1061 $product_terms = wp_get_object_terms( $post->ID, 'doc_tag' );
1062 $enable_tags = betterdocs()->settings->get( 'enable_tags', false );
1063 return ! empty( $product_terms ) && $enable_tags;
1064 }
1065
1066 /**
1067 * Check if AI Search Suggestions are enabled
1068 *
1069 * @return bool
1070 */
1071 public function is_ai_search_suggestions_enabled() {
1072 $ai_search_suggestions_active = is_plugin_active( 'betterdocs-ai-search-suggestions/betterdocs-ai-search-suggestions.php' );
1073 $ai_search_suggestions_license_valid = get_option( 'betterdocs_ai_search_suggestions_software__license_status' ) === 'valid';
1074 $ai_search_suggestions_enabled = betterdocs()->settings->get( 'enable_ai_powered_search', false );
1075
1076 return $ai_search_suggestions_active && $ai_search_suggestions_license_valid && $ai_search_suggestions_enabled;
1077 }
1078
1079 /**
1080 * Get the maximum order value from the 'doc_category_order' term meta
1081 *
1082 * @return int
1083 */
1084 public static function get_max_doc_category_order_from_term_meta() {
1085 global $wpdb;
1086 $sql = $wpdb->prepare( "SELECT MAX(CAST(meta_value AS UNSIGNED)) AS max FROM {$wpdb->prefix}termmeta WHERE meta_key = %s ", 'doc_category_order');
1087 $result = $wpdb->get_var($sql);
1088 return $result;
1089 }
1090 }
1091