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

1,081 lines 37.3 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 $post_data = [
752 'id' => $post['term_id'] ?? '',
753 'post_title' => $post['post_title'] ?? '',
754 'post_excerpt' => ! empty( $post_excerpt )
755 ? $post_excerpt
756 : ( ! empty( $glossary_term_description )
757 ? self::get_custom_excerpt( $glossary_term_description, 15 )
758 : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ) ),
759 'permalink' => isset( $post['slug'] ) ? get_term_link( $post['slug'], 'glossaries' ) : ''
760 ];
761 } else {
762 // For docs
763 $post_data = [
764 'id' => $post['ID'] ?? '',
765 'post_title' => $post['post_title'] ?? '',
766 'post_excerpt' => ! empty( $post_excerpt )
767 ? $post_excerpt
768 : self::get_custom_excerpt( strip_tags( $post['post_content'] ?? '' ), 15 ),
769 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
770 ];
771 }
772
773 $docs_by_letter[$letter][] = $post_data;
774 }
775 }
776 }
777
778 return $docs_by_letter;
779 }
780
781 public static function get_glossaries() {
782 global $wpdb;
783
784 $lang_join = '';
785 $lang_where = '';
786
787 // Add language filtering if multilingual plugin is active and we should apply filtering
788 $current_language = self::get_current_language();
789 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
790 // For WPML, use icl_translations table
791 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
792 $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'";
793 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
794 }
795 // For Polylang, use term_relationships with language taxonomy
796 elseif ( function_exists( 'pll_current_language' ) ) {
797 $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";
798 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
799 }
800 }
801
802 $query = "
803 SELECT t.name
804 FROM {$wpdb->terms} t
805 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
806 $lang_join
807 WHERE tt.taxonomy = 'glossaries'
808 $lang_where
809 ORDER BY t.name ASC
810 ";
811
812 $glossaries = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
813
814 return $glossaries;
815 }
816
817 /**
818 * 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)
819 *
820 * @param string $layout
821 * @return string $layout
822 */
823 public static function determine_search_layout( $layout ) {
824 if ( $layout ) {
825 return $layout;
826 }
827
828 $search_layout = betterdocs()->customizer->defaults->get( 'betterdocs_search_layout_select' );
829 $docs_layout = betterdocs()->customizer->defaults->get( 'betterdocs_docs_layout_select' );
830 $archive_page_layout = betterdocs()->customizer->defaults->get( 'betterdocs_archive_layout_select' );
831 $single_layout = betterdocs()->customizer->defaults->get( 'betterdocs_single_layout_select' );
832
833 if ( is_post_type_archive( 'docs' ) ) {
834 if ( $docs_layout != "layout-7" && ! $search_layout ) {
835 $layout = 'layout-1';
836 } else if ( $docs_layout == 'layout-7' && ! $search_layout ) {
837 $layout = 'layout-2';
838 }
839 } else if ( is_tax( 'doc_tag' ) && ! $search_layout ) {
840 $layout = 'layout-1';
841 } else if ( is_tax( 'doc_category' ) ) {
842 if ( $archive_page_layout != 'layout-7' && $archive_page_layout != 'layout-8' && ! $search_layout ) {
843 $layout = 'layout-1';
844 } else if ( ( $archive_page_layout == 'layout-7' && ! $search_layout ) || ( $archive_page_layout == 'layout-8' && ! $search_layout ) ) {
845 $layout = 'layout-2';
846 }
847 } else if ( is_singular( 'docs' ) ) {
848 if ( $single_layout != 'layout-8' && $single_layout != 'layout-9' && ! $search_layout ) {
849 $layout = 'layout-1';
850 } else if ( ( $single_layout == 'layout-8' && ! $search_layout ) || ( $single_layout == 'layout-9' && ! $search_layout ) ) {
851 $layout = 'layout-2';
852 }
853 }
854
855 return $layout;
856 }
857 public static function mb_ord_fallback( $char ) {
858 $code = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) );
859 return $code[1];
860 }
861
862 public static function mb_chr_fallback( $code ) {
863 return mb_convert_encoding( pack( 'N', $code ), 'UTF-8', 'UCS-4BE' );
864 }
865
866 public static function unicodeRange( $start, $end ) {
867 $range = [];
868 for ( $i = self::mb_ord_fallback( $start ); $i <= self::mb_ord_fallback( $end ); $i++ ) {
869 $range[] = self::mb_chr_fallback( $i );
870 }
871 return $range;
872 }
873
874 public static function get_character_range( $enable_non_latin, $script ) {
875 if ( $enable_non_latin ) {
876 switch ( $script ) {
877 case 'arabic':
878 return self::unicodeRange( 'ء', 'ي' );
879 case 'cyrillic':
880 return self::unicodeRange( 'А', 'Я' );
881 case 'hebrew':
882 return self::unicodeRange( 'א', 'ת' );
883 case 'greek':
884 return self::unicodeRange( 'Α', 'Ω' );
885 default:
886 return range( 'A', 'Z' );
887 }
888 }
889
890 return range( 'A', 'Z' );
891 }
892
893 public static function get_the_top_most_parent( $term_id ) {
894 while ( $term_id != 0 ) {
895 $parent_id = wp_get_term_taxonomy_parent_id( $term_id, 'doc_category' );
896
897 if ( $parent_id == 0 ) {
898 break;
899 }
900
901 $term_id = $parent_id;
902 }
903 return $term_id;
904 }
905
906 public static function get_highest_docs_term() {
907 $terms = get_terms( [
908 'taxonomy' => 'doc_category', // Change to your desired taxonomy
909 'hide_empty' => true, // Only show terms with posts
910 'orderby' => 'count', // Order by post count
911 'order' => 'DESC', // Descending order
912 'number' => 1 // Get only the top term
913 ] );
914 return isset( $terms[0] ) ? $terms[0] : [];
915 }
916
917 public static function delete_specific_faq_posts_by_faq_category( $term_id ) {
918 $args = [
919 'post_type' => 'betterdocs_faq',
920 'posts_per_page' => -1,
921 'tax_query' => [
922 [
923 'taxonomy' => 'betterdocs_faq_category',
924 'field' => 'id',
925 'terms' => $term_id,
926 'operator' => 'IN'
927 ]
928 ],
929 'fields' => 'ids'
930 ];
931
932 $query = new \WP_Query( $args );
933
934 if ( $query->have_posts() ) {
935 foreach ( $query->posts as $doc_id ) {
936 wp_delete_post( $doc_id, true );
937 }
938 }
939 }
940
941 /**
942 * Function To Normalize Repeater Field For Quick Builder
943 *
944 * @param array $fields
945 * @param array $include_field_keys
946 *
947 * @return array
948 */
949 public static function normalize_repeater_field( $fields, $include_field_keys = [] ) {
950 if( empty( $include_field_keys ) ) {
951 return $fields;
952 }
953
954 $normalized_fields = [];
955
956 foreach( $fields as $field ) {
957 foreach( $include_field_keys as $field_key ) {
958 if( ! isset( $normalized_fields[$field_key] ) ) {
959 $normalized_fields[$field_key] = isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [];
960 } else {
961 array_push( $normalized_fields[$field_key], ...( isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [] ) );
962 $normalized_fields[$field_key] = array_unique( $normalized_fields[$field_key] );
963 }
964 }
965 }
966
967 return $normalized_fields;
968 }
969
970 public static function get_local_plugin_data( $basename = '' ) {
971 if ( empty( $basename ) ) {
972 return false;
973 }
974
975 if ( !function_exists( 'get_plugins' ) ) {
976 include_once ABSPATH . 'wp-admin/includes/plugin.php';
977 }
978
979 $plugins = get_plugins();
980
981 if ( !isset( $plugins[ $basename ] ) ) {
982 return false;
983 }
984
985 return $plugins[ $basename ];
986 }
987
988 /**
989 * Get default file icon based on programming language
990 *
991 * @param string $language Programming language identifier
992 * @return string Emoji icon for the language
993 */
994 public static function get_file_icon_by_language( $language ) {
995 $icons = [
996 'javascript' => '📄',
997 'typescript' => '📘',
998 'jsx' => '⚛️',
999 'tsx' => '⚛️',
1000 'html' => '🌐',
1001 'css' => '🎨',
1002 'scss' => '🎨',
1003 'sass' => '🎨',
1004 'less' => '🎨',
1005 'php' => '🐘',
1006 'python' => '🐍',
1007 'java' => '',
1008 'csharp' => '🔷',
1009 'cpp' => '⚙️',
1010 'c' => '⚙️',
1011 'ruby' => '💎',
1012 'go' => '🐹',
1013 'rust' => '🦀',
1014 'swift' => '🦉',
1015 'kotlin' => '🎯',
1016 'sql' => '🗃️',
1017 'json' => '📋',
1018 'yaml' => '📋',
1019 'xml' => '📄',
1020 'markdown' => '📝',
1021 'bash' => '💻',
1022 'shell' => '💻',
1023 'powershell' => '💻',
1024 'dockerfile' => '🐳',
1025 ];
1026
1027 return isset( $icons[$language] ) ? $icons[$language] : '📄';
1028 }
1029
1030 /**
1031 * Check if AI Chatbot is enabled
1032 *
1033 * @return bool
1034 */
1035 public function is_ai_chatbot_enabled() {
1036 $chatbot_active = is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
1037 $chatbot_license_valid = get_option( 'betterdocs_chatbot_software__license_status' ) === 'valid';
1038 $chatbot_enabled = betterdocs()->settings->get( 'enable_ai_chatbot', false );
1039
1040 // AI Search Suggestions are enabled if all conditions are met
1041 return $chatbot_active && $chatbot_license_valid && $chatbot_enabled;
1042 }
1043
1044 /**
1045 * Check if tags are enabled and post has tags
1046 *
1047 * @return bool
1048 */
1049 public function is_tag_enabled() {
1050 global $post;
1051 $product_terms = wp_get_object_terms( $post->ID, 'doc_tag' );
1052 $enable_tags = betterdocs()->settings->get( 'enable_tags', false );
1053 return ! empty( $product_terms ) && $enable_tags;
1054 }
1055
1056 /**
1057 * Check if AI Search Suggestions are enabled
1058 *
1059 * @return bool
1060 */
1061 public function is_ai_search_suggestions_enabled() {
1062 $ai_search_suggestions_active = is_plugin_active( 'betterdocs-ai-search-suggestions/betterdocs-ai-search-suggestions.php' );
1063 $ai_search_suggestions_license_valid = get_option( 'betterdocs_ai_search_suggestions_software__license_status' ) === 'valid';
1064 $ai_search_suggestions_enabled = betterdocs()->settings->get( 'enable_ai_powered_search', false );
1065
1066 return $ai_search_suggestions_active && $ai_search_suggestions_license_valid && $ai_search_suggestions_enabled;
1067 }
1068
1069 /**
1070 * Get the maximum order value from the 'doc_category_order' term meta
1071 *
1072 * @return int
1073 */
1074 public static function get_max_doc_category_order_from_term_meta() {
1075 global $wpdb;
1076 $sql = $wpdb->prepare( "SELECT MAX(CAST(meta_value AS UNSIGNED)) AS max FROM {$wpdb->prefix}termmeta WHERE meta_key = %s ", 'doc_category_order');
1077 $result = $wpdb->get_var($sql);
1078 return $result;
1079 }
1080 }
1081