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

1,439 lines 51.2 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 // Helper utilities mix per-language URL detection (read-only $_GET reads),
6 // dynamic alphabet-letter / glossary queries composed via $wpdb->prepare,
7 // and meta-key term lookups that are core to BetterDocs functionality.
8 // phpcs:disable WordPress.Security.NonceVerification.Recommended
9 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
10 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
11 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
12 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
13 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
14 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
15 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
16 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
17
18 use function BetterLinksPro\Dependencies\GuzzleHttp\json_decode;
19 use function WPML\PHP\Logger\error;
20
21 class Helper extends Base {
22
23 /**
24 * Mask an API key for safe display: first 3 chars + 8 asterisks + last 4 chars.
25 * Fixed asterisk count avoids leaking the real key length.
26 */
27 public static function mask_api_key( $key ) {
28 if ( ! is_string( $key ) || $key === '' ) {
29 return '';
30 }
31 $key = trim( $key );
32 if ( strlen( $key ) < 8 ) {
33 return str_repeat( '*', strlen( $key ) );
34 }
35 return substr( $key, 0, 3 ) . str_repeat( '*', 8 ) . substr( $key, -4 );
36 }
37
38 public static function get_plugins( $plugin_basename = null ) {
39 if ( ! function_exists( 'get_plugins' ) ) {
40 include_once ABSPATH . 'wp-admin/includes/plugin.php';
41 }
42
43 $plugins = get_plugins();
44 return $plugin_basename == null ? $plugins : isset( $plugins[ $plugin_basename ] );
45 }
46
47 public static function is_plugin_active( $plugin_basename ) {
48 if ( ! function_exists( 'is_plugin_active' ) ) {
49 include_once ABSPATH . 'wp-admin/includes/plugin.php';
50 }
51
52 return is_plugin_active( $plugin_basename );
53 }
54
55 /**
56 * Whether an SEO plugin already emits FAQPage schema on the current page.
57 *
58 * True only when Yoast or Rank Math is active AND its FAQ block is present
59 * in the post's content, so BetterDocs can skip its own FAQPage JSON-LD and
60 * avoid duplicate structured data. Defaults to the queried object when no
61 * post is given.
62 *
63 * @param int|\WP_Post|null $post
64 * @return bool
65 */
66 public static function seo_plugin_outputs_faq_schema( $post = null ) {
67 if ( null === $post ) {
68 $post = get_queried_object();
69 }
70
71 $post = get_post( $post );
72 if ( ! $post instanceof \WP_Post ) {
73 return false;
74 }
75
76 if ( self::is_plugin_active( 'wordpress-seo/wp-seo.php' ) && has_block( 'yoast/faq-block', $post ) ) {
77 return true;
78 }
79
80 if ( self::is_plugin_active( 'seo-by-rank-math/rank-math.php' ) && has_block( 'rank-math/faq-block', $post ) ) {
81 return true;
82 }
83
84 // Extension seam for Pro / other SEO integrations.
85 return (bool) apply_filters( 'betterdocs_seo_plugin_outputs_faq_schema', false, $post );
86 }
87
88 public static function get_tax( $tax = '' ) {
89 global $wp_query;
90
91 if ( is_tax( 'knowledge_base' ) ) {
92 $_taxes = $wp_query->tax_query->queried_terms;
93 if ( array_key_exists( 'doc_category', $_taxes ) ) {
94 $tax = 'doc_category';
95 } else {
96 $tax = 'knowledge_base';
97 }
98 } elseif ( is_tax( 'doc_category' ) ) {
99 $tax = 'doc_category';
100 } elseif ( is_tax( 'doc_tag' ) ) {
101 $tax = 'doc_tag';
102 }
103
104 return $tax;
105 }
106
107 public function is_templates() {
108 global $wp_query;
109 $slug = betterdocs()->settings->get( 'encyclopedia_root_slug', 'encyclopedia' );
110
111 $tax = $this->get_tax();
112 if ( is_post_type_archive( 'docs' ) || $tax === 'knowledge_base' || $tax === 'doc_category' || $tax === 'doc_tag' || is_singular( 'docs' ) || is_tax( 'glossaries' ) ) {
113 return true;
114 }
115
116 if ( isset( $wp_query->query['pagename'] ) && $wp_query->query['pagename'] === $slug ) {
117 return true;
118 }
119
120 return false;
121 }
122
123 public function is_el_templates() {
124 $_return_val = betterdocs()->editor->get( 'elementor' )->is_templates();
125
126 if ( $_return_val !== null ) {
127 return $_return_val;
128 }
129
130 $this->is_templates();
131 }
132
133 /**
134 * Which tab to show.
135 *
136 * 1. Drag and Drop UI
137 * 2. Post List UI
138 *
139 * * 1. dnd
140 * * 2. classic
141 *
142 * look into views/admin/docs-ui directory to know more.
143 *
144 * @return string
145 */
146 public static function admin_tab() {
147 $admin_ui = 'grid';
148 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin UI selection, no state change.
149 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
150 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin UI selection, no state change.
151 $mode = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : '';
152 if ( $page === 'betterdocs-admin' && ! empty( $mode ) ) {
153 $admin_ui = $mode === 'grid' ? 'grid' : 'list';
154 }
155
156 return $admin_ui;
157 }
158
159 public static function is_active( $prev, $current, $class = 'active' ) {
160 if ( $current == $prev ) {
161 return $class;
162 }
163
164 return '';
165 }
166
167 public function get_users( $args ) {
168 $cache_key = 'betterdocs_cache_admin_user_roles';
169 $users = betterdocs()->database->get_cache( $cache_key );
170
171 if ( false === $users ) {
172 $users = get_users( $args );
173 betterdocs()->database->set_cache( $cache_key, $users );
174 }
175
176 return $users;
177 }
178
179 /**
180 * Normalize Menu Array
181 * Menu creator helper
182 *
183 * @since 2.5.0
184 *
185 * @param string $title
186 * @param string $slug
187 * @param string $cap
188 * @param array $callback
189 *
190 * @return array
191 */
192 public static function normalize_menu( $title, $slug, $cap = 'edit_docs', $callback = null, $optional = [] ) {
193 $args = [
194 'page_title' => $title,
195 'menu_title' => $title,
196 'capability' => $cap,
197 'menu_slug' => $slug
198 ];
199
200 if ( $callback != null ) {
201 $args['callback'] = $callback;
202 }
203
204 return wp_parse_args( $optional, $args );
205 }
206
207 /**
208 * Check if the current theme is a block theme.
209 *
210 * @since x.x.x
211 * @return bool
212 */
213 public function current_theme_is_fse_theme() {
214 if ( function_exists( 'wp_is_block_theme' ) ) {
215 return (bool) wp_is_block_theme();
216 }
217 if ( function_exists( 'gutenberg_is_fse_theme' ) ) {
218 return (bool) gutenberg_is_fse_theme();
219 }
220
221 return false;
222 }
223
224 protected static function is_assoc_array( $array ) {
225 return array_keys( $array ) !== range( 0, count( $array ) - 1 );
226 }
227
228 public static function merge( &$array1, &$array2 ) {
229 $merged = $array1;
230
231 foreach ( $array2 as $key => &$value ) {
232 if ( is_array( $value ) && self::is_assoc_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
233 $merged[ $key ] = self::merge( $merged[ $key ], $value );
234 } elseif ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
235 $merged[ $key ] = array_merge( $merged[ $key ], $value );
236 } else {
237 $merged[ $key ] = $value;
238 }
239 }
240
241 return $merged;
242 }
243
244 public static function get_custom_excerpt( $content, $numOfWords ) {
245 $content = strip_shortcodes( $content );
246 $content = wp_strip_all_tags( $content );
247 $words = explode( ' ', $content );
248 $excerptWords = array_slice( $words, 0, $numOfWords );
249 $excerpt = implode( ' ', $excerptWords );
250 if ( count( $words ) > $numOfWords ) {
251 $excerpt .= '...';
252 }
253 return $excerpt;
254 }
255
256 /**
257 * Get current language from various multilingual plugins
258 *
259 * @return string|null Current language code
260 */
261 public static function get_current_language() {
262 $current_language = null;
263
264 // WPML Support
265 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
266 global $sitepress;
267 if ( $sitepress && $sitepress->is_setup_complete() ) {
268 $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
269 }
270 }
271 // Polylang Support
272 elseif ( function_exists( 'pll_current_language' ) ) {
273 $current_language = pll_current_language();
274 }
275 // qTranslate-X Support
276 elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
277 $current_language = qtranxf_getLanguage();
278 }
279 // Weglot Support
280 elseif ( function_exists( 'weglot_get_current_language' ) ) {
281 $current_language = weglot_get_current_language();
282 }
283 // TranslatePress Support
284 elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
285 $current_language = trp_get_current_language();
286 }
287
288 return $current_language;
289 }
290
291 /**
292 * Check if any multilingual plugin is active
293 *
294 * @return bool
295 */
296 public static function is_multilingual_active() {
297 return is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ||
298 function_exists( 'pll_current_language' ) ||
299 function_exists( 'qtranxf_getLanguage' ) ||
300 function_exists( 'weglot_get_current_language' ) ||
301 ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) );
302 }
303
304 /**
305 * Check if we should apply language filtering
306 * Only apply on frontend or when specifically requested
307 *
308 * @return bool
309 */
310 public static function should_apply_language_filtering() {
311 // Don't apply language filtering in admin context unless it's a frontend request
312 if ( is_admin() ) {
313 // Allow language filtering for REST API requests that are frontend-facing
314 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
315 // Check if this is a frontend REST request (not admin)
316 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
317 // Don't filter admin REST requests for glossaries management
318 if ( strpos( $request_uri, '/wp/v2/glossaries' ) !== false ) {
319 return false; // Don't filter admin glossaries management
320 }
321 }
322 return false; // Don't filter other admin requests
323 }
324
325 // Apply filtering on frontend
326 return true;
327 }
328
329 /**
330 * Get current admin language for multilingual sites
331 * This is specifically for admin context where we need to detect
332 * the language being used for editing terms/posts
333 *
334 * @return string|null Current admin language code
335 */
336 public static function get_current_admin_language() {
337 $current_language = null;
338
339 // Explicit language passed by the admin client takes priority.
340 // Covers AJAX (POST) and REST/admin requests (GET) where WPML may
341 // otherwise resolve to the site's default language instead of the
342 // admin UI language.
343 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only UI language hint, sanitized; not a state-changing form submission.
344 if ( isset( $_POST['lang'] ) && ! empty( $_POST['lang'] ) ) {
345 return sanitize_text_field( wp_unslash( $_POST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- see note above.
346 }
347
348 // Limit GET handling to admin/REST contexts so a frontend ?lang= switch
349 // doesn't hijack admin meta-key resolution.
350 if ( isset( $_GET['lang'] ) && ! empty( $_GET['lang'] )
351 && ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) ) {
352 return sanitize_text_field( wp_unslash( $_GET['lang'] ) );
353 }
354
355 // WPML Support - Admin language detection
356 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
357 global $sitepress;
358 if ( $sitepress && $sitepress->is_setup_complete() ) {
359 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
360 $tag_id = isset( $_GET['tag_ID'] ) ? (int) $_GET['tag_ID'] : 0;
361 // For term editing, check if we have a specific term language
362 if ( $tag_id && function_exists( 'wpml_get_language_information' ) ) {
363 $term_info = wpml_get_language_information( null, $tag_id );
364 if ( ! is_wp_error( $term_info ) && $term_info && isset( $term_info['language_code'] ) ) {
365 $current_language = $term_info['language_code'];
366 }
367
368 }
369
370 // Check for language parameter in URL
371 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
372 if ( ! $current_language && isset( $_GET['lang'] ) ) {
373 $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
374 }
375
376 // Check WPML admin language cookie (persists during AJAX)
377 if ( ! $current_language && isset( $_COOKIE['_icl_current_admin_language'] ) ) {
378 $current_language = sanitize_text_field( wp_unslash( $_COOKIE['_icl_current_admin_language'] ) );
379 }
380
381 // Fallback to admin language or current language
382 if ( ! $current_language ) {
383 $current_language = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : $sitepress->get_current_language();
384 }
385 }
386 }
387 // Polylang Support - Admin language detection
388 elseif ( function_exists( 'pll_current_language' ) ) {
389 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
390 $tag_id = isset( $_GET['tag_ID'] ) ? (int) $_GET['tag_ID'] : 0;
391 // For term editing, get language from term ID
392 if ( $tag_id && function_exists( 'pll_get_term_language' ) ) {
393 $term_lang = pll_get_term_language( $tag_id );
394 if ( $term_lang ) {
395 $current_language = $term_lang;
396 }
397 }
398
399 // Check for language parameter in URL
400 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only language detection from URL.
401 if ( ! $current_language && isset( $_GET['lang'] ) ) {
402 $current_language = sanitize_text_field( wp_unslash( $_GET['lang'] ) );
403 }
404
405 // Fallback to current admin language
406 if ( ! $current_language ) {
407 $current_language = pll_current_language( 'slug' );
408 }
409 }
410 // Other multilingual plugins
411 elseif ( function_exists( 'qtranxf_getLanguage' ) ) {
412 $current_language = qtranxf_getLanguage();
413 }
414 elseif ( function_exists( 'weglot_get_current_language' ) ) {
415 $current_language = weglot_get_current_language();
416 }
417 elseif ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) ) {
418 $current_language = trp_get_current_language();
419 }
420
421 return $current_language;
422 }
423
424 /**
425 * Generate language-specific meta key for category ordering
426 * Always falls back to base key if language-specific key doesn't exist
427 *
428 * @param string $base_key The base meta key (e.g., 'doc_category_order')
429 * @param string|null $language Language code, if null will auto-detect
430 * @return string Language-specific meta key or base key as fallback
431 */
432 public static function get_language_specific_meta_key( $base_key, $language = null ) {
433 // If no multilingual plugin is active, return the base key
434 if ( ! self::is_multilingual_active() ) {
435 return $base_key;
436 }
437
438 // Get current admin language if not provided
439 if ( $language === null ) {
440 $language = self::get_current_admin_language();
441 }
442
443 // If no language detected, return base key for backward compatibility
444 if ( ! $language ) {
445 return $base_key;
446 }
447
448 // Always return base key for now - we'll handle fallback in the query functions
449 // This ensures compatibility without requiring migration
450 return $base_key;
451 }
452
453 /**
454 * Get the meta key to write to.
455 *
456 * Unlike `get_meta_key_with_fallback`, this never falls back to the base
457 * key when the language-specific key is empty — that fallback is what
458 * caused secondary-language drag-and-drop saves to clobber the base meta
459 * (and on WPML setups that copy term meta from the original language,
460 * the next read would re-overwrite it from the primary language).
461 *
462 * @param string $base_key The base meta key.
463 * @param string|null $language Language code, auto-detected when null.
464 * @return string Language-specific key when multilingual + language known, else base.
465 */
466 public static function get_meta_key_for_save( $base_key, $language = null ) {
467 if ( ! self::is_multilingual_active() ) {
468 return $base_key;
469 }
470
471 if ( $language === null ) {
472 $language = self::get_current_admin_language();
473 }
474
475 if ( ! $language ) {
476 return $base_key;
477 }
478
479 return $base_key . '_' . $language;
480 }
481
482 /**
483 * Get the appropriate meta key with fallback logic
484 * This function checks if language-specific meta exists, if not falls back to base key
485 *
486 * @param string $base_key The base meta key
487 * @param int $term_id The term ID to check
488 * @param string|null $language Language code
489 * @return string The meta key to use
490 */
491 public static function get_meta_key_with_fallback( $base_key, $term_id = null, $language = null ) {
492 // If no multilingual plugin is active, return the base key
493 if ( ! self::is_multilingual_active() ) {
494 return $base_key;
495 }
496
497 // Get current admin language if not provided
498 if ( $language === null ) {
499 $language = self::get_current_admin_language();
500 }
501
502 // If no language detected, return base key
503 if ( ! $language ) {
504 return $base_key;
505 }
506
507 $lang_meta_key = $base_key . '_' . $language;
508
509 // If we have a specific term ID, check if language-specific meta exists
510 if ( $term_id ) {
511 $lang_value = get_term_meta( $term_id, $lang_meta_key, true );
512 if ( ! empty( $lang_value ) ) {
513 return $lang_meta_key;
514 }
515 // Fall back to base key if language-specific doesn't exist
516 return $base_key;
517 }
518
519 // For queries without specific term ID, we need to check if ANY terms have language-specific meta
520 global $wpdb;
521 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live multilingual meta-key resolution; result varies per active language.
522 $has_lang_meta = $wpdb->get_var( $wpdb->prepare(
523 "SELECT COUNT(*) FROM {$wpdb->termmeta} tm
524 INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
525 WHERE tm.meta_key = %s AND tt.taxonomy = 'doc_category' AND tm.meta_value != ''",
526 $lang_meta_key
527 ) );
528
529 // If language-specific meta exists for some terms, use it (terms without it will have empty values)
530 // Otherwise, fall back to base key
531 return $has_lang_meta > 0 ? $lang_meta_key : $base_key;
532 }
533
534 /**
535 * Migrate existing category orders to language-specific meta keys
536 * This should be called when a multilingual plugin is activated
537 *
538 * @param string $base_key The base meta key (e.g., 'doc_category_order')
539 * @param string $taxonomy The taxonomy to migrate
540 * @return bool Success status
541 */
542 public static function migrate_category_orders_to_multilingual( $base_key = 'doc_category_order', $taxonomy = 'doc_category' ) {
543 // Only run if multilingual plugin is active
544 if ( ! self::is_multilingual_active() ) {
545 return false;
546 }
547
548 global $wpdb;
549
550 // Get all terms with the base meta key
551 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot multilingual migration; cache would be stale immediately after writes.
552 $terms_with_order = $wpdb->get_results( $wpdb->prepare(
553 "SELECT tm.term_id, tm.meta_value, t.slug
554 FROM {$wpdb->termmeta} tm
555 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
556 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
557 WHERE tm.meta_key = %s AND tt.taxonomy = %s",
558 $base_key,
559 $taxonomy
560 ) );
561
562 if ( empty( $terms_with_order ) ) {
563 return true; // Nothing to migrate
564 }
565
566 // Get available languages
567 $languages = self::get_available_languages();
568
569 if ( empty( $languages ) ) {
570 return false; // No languages found
571 }
572
573 // Migrate orders for each language
574 foreach ( $languages as $language ) {
575 $language_meta_key = $base_key . '_' . $language;
576
577 foreach ( $terms_with_order as $term_data ) {
578 // Check if language-specific meta already exists
579 $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
580
581 if ( empty( $existing_value ) ) {
582 // Copy the base order to language-specific key
583 update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
584 }
585 }
586 }
587
588 return true;
589 }
590
591 /**
592 * Migrate existing document orders to language-specific meta keys
593 * This should be called when a multilingual plugin is activated
594 *
595 * @param string $base_key The base meta key (e.g., '_docs_order')
596 * @param string $taxonomy The taxonomy to migrate
597 * @return bool Success status
598 */
599 public static function migrate_docs_orders_to_multilingual( $base_key = '_docs_order', $taxonomy = 'doc_category' ) {
600 // Only run if multilingual plugin is active
601 if ( ! self::is_multilingual_active() ) {
602 return false;
603 }
604
605 global $wpdb;
606
607 // Get all terms with the base meta key for document ordering
608 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot multilingual migration; cache would be stale immediately after writes.
609 $terms_with_docs_order = $wpdb->get_results( $wpdb->prepare(
610 "SELECT tm.term_id, tm.meta_value, t.slug
611 FROM {$wpdb->termmeta} tm
612 INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
613 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
614 WHERE tm.meta_key = %s AND tt.taxonomy = %s AND tm.meta_value != ''",
615 $base_key,
616 $taxonomy
617 ) );
618
619 if ( empty( $terms_with_docs_order ) ) {
620 return true; // Nothing to migrate
621 }
622
623 // Get available languages
624 $languages = self::get_available_languages();
625
626 if ( empty( $languages ) ) {
627 return false; // No languages found
628 }
629
630 // Migrate document orders for each language
631 foreach ( $languages as $language ) {
632 $language_meta_key = $base_key . '_' . $language;
633
634 foreach ( $terms_with_docs_order as $term_data ) {
635 // Check if language-specific meta already exists
636 $existing_value = get_term_meta( $term_data->term_id, $language_meta_key, true );
637
638 if ( empty( $existing_value ) ) {
639 // Copy the base document order to language-specific key
640 update_term_meta( $term_data->term_id, $language_meta_key, $term_data->meta_value );
641 }
642 }
643 }
644
645 return true;
646 }
647
648 /**
649 * Migrate both category and document orders to multilingual format
650 * This is a convenience method that runs both migrations
651 *
652 * @return bool Success status
653 */
654 public static function migrate_all_orders_to_multilingual() {
655 $category_result = self::migrate_category_orders_to_multilingual();
656 $docs_result = self::migrate_docs_orders_to_multilingual();
657
658 return $category_result && $docs_result;
659 }
660
661 /**
662 * Get available languages from multilingual plugins
663 *
664 * @return array Array of language codes
665 */
666 public static function get_available_languages() {
667 $languages = [];
668
669 // WPML Support
670 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
671 global $sitepress;
672 if ( $sitepress && $sitepress->is_setup_complete() ) {
673 $active_languages = $sitepress->get_active_languages();
674 if ( is_array( $active_languages ) ) {
675 $languages = array_keys( $active_languages );
676 }
677 }
678 }
679 // Polylang Support
680 elseif ( function_exists( 'pll_languages_list' ) ) {
681 $languages = pll_languages_list();
682 }
683
684 return $languages;
685 }
686
687 /**
688 * Rich list of active site languages for the React admin language bar.
689 *
690 * @return array<int,array{code:string,label:string,native:string,flag:string}>
691 * Empty when no supported multilingual plugin is active.
692 */
693 public static function get_admin_languages() {
694 $languages = [];
695
696 // WPML
697 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
698 global $sitepress;
699 if ( $sitepress && $sitepress->is_setup_complete() ) {
700 $active = $sitepress->get_active_languages();
701 if ( is_array( $active ) ) {
702 foreach ( $active as $code => $lang ) {
703 $languages[] = [
704 'code' => $code,
705 'label' => isset( $lang['english_name'] ) ? $lang['english_name'] : $code,
706 'native' => isset( $lang['native_name'] ) ? $lang['native_name'] : ( isset( $lang['display_name'] ) ? $lang['display_name'] : $code ),
707 'flag' => isset( $lang['country_flag_url'] ) ? $lang['country_flag_url'] : '',
708 ];
709 }
710 }
711 }
712 }
713 // Polylang
714 elseif ( function_exists( 'pll_languages_list' ) ) {
715 $list = pll_languages_list( [ 'fields' => '' ] ); // full PLL_Language objects
716 if ( is_array( $list ) ) {
717 foreach ( $list as $lang ) {
718 if ( ! is_object( $lang ) ) {
719 continue;
720 }
721 $languages[] = [
722 'code' => isset( $lang->slug ) ? $lang->slug : '',
723 'label' => isset( $lang->name ) ? $lang->name : ( isset( $lang->slug ) ? $lang->slug : '' ),
724 'native' => isset( $lang->name ) ? $lang->name : '',
725 'flag' => isset( $lang->flag_url ) ? $lang->flag_url : '',
726 ];
727 }
728 }
729 }
730
731 return $languages;
732 }
733
734 /**
735 * Read a term's language code via the active multilingual plugin.
736 *
737 * @param \WP_Term $term
738 * @return string Language code, or '' when unavailable.
739 */
740 public static function get_term_language( $term ) {
741 if ( ! is_object( $term ) || empty( $term->term_id ) ) {
742 return '';
743 }
744
745 // Polylang — takes the term_id.
746 if ( function_exists( 'pll_get_term_language' ) ) {
747 $lang = pll_get_term_language( $term->term_id, 'slug' );
748 return $lang ? $lang : '';
749 }
750
751 // WPML — element_id is the term_taxonomy_id (NOT the term_id); WPML
752 // normalizes the element_type to `tax_<taxonomy>` internally.
753 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
754 $lang = apply_filters( 'wpml_element_language_code', null, [
755 'element_id' => $term->term_taxonomy_id,
756 'element_type' => isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category',
757 ] );
758 return $lang ? $lang : '';
759 }
760
761 return '';
762 }
763
764 /**
765 * Stamp a term's language via the active multilingual plugin. Standalone
766 * assignment only — it sets/re-stamps the term's own language and does not
767 * link it into an existing translation group.
768 *
769 * @param \WP_Term $term
770 * @param string $lang_code
771 */
772 public static function set_term_language( $term, $lang_code ) {
773 $lang_code = sanitize_text_field( (string) $lang_code );
774 if ( $lang_code === '' || ! is_object( $term ) || empty( $term->term_id ) ) {
775 return;
776 }
777
778 // Polylang
779 if ( function_exists( 'pll_set_term_language' ) ) {
780 pll_set_term_language( $term->term_id, $lang_code );
781 return;
782 }
783
784 // WPML — element_id is the term_taxonomy_id; element_type is tax_<taxonomy>;
785 // trid=null sets it as a standalone original in the chosen language.
786 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
787 $taxonomy = isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category';
788 do_action( 'wpml_set_element_language_details', [
789 'element_id' => $term->term_taxonomy_id,
790 'element_type' => 'tax_' . $taxonomy,
791 'trid' => null,
792 'language_code' => $lang_code,
793 'source_language_code' => null,
794 ] );
795 }
796 }
797
798 /**
799 * The site's default language code, or '' when no multilingual plugin is active.
800 */
801 public static function get_default_language() {
802 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
803 global $sitepress;
804 if ( $sitepress ) {
805 return (string) $sitepress->get_default_language();
806 }
807 }
808 if ( function_exists( 'pll_default_language' ) ) {
809 return (string) pll_default_language( 'slug' );
810 }
811 return '';
812 }
813
814 /**
815 * All terms in a term's translation group, keyed by language code.
816 *
817 * @param \WP_Term $term
818 * @return array<string,array{term_id:int,name:string}>
819 */
820 public static function get_term_translations( $term ) {
821 if ( ! is_object( $term ) || empty( $term->term_id ) ) {
822 return [];
823 }
824 $taxonomy = isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category';
825 $out = [];
826
827 // Polylang
828 if ( function_exists( 'pll_get_term_translations' ) ) {
829 $group = pll_get_term_translations( $term->term_id ); // [lang => term_id]
830 if ( is_array( $group ) ) {
831 foreach ( $group as $lang => $tid ) {
832 $t = get_term( (int) $tid, $taxonomy );
833 if ( $t && ! is_wp_error( $t ) ) {
834 $out[ $lang ] = [ 'term_id' => (int) $tid, 'name' => $t->name ];
835 }
836 }
837 }
838 return $out;
839 }
840
841 // WPML
842 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
843 $el_type = 'tax_' . $taxonomy;
844 $trid = apply_filters( 'wpml_element_trid', null, $term->term_taxonomy_id, $el_type );
845 if ( ! $trid ) {
846 return $out;
847 }
848 $translations = apply_filters( 'wpml_get_element_translations', null, $trid, $el_type );
849 if ( is_array( $translations ) ) {
850 foreach ( $translations as $lang => $tr ) {
851 $tid = isset( $tr->term_id ) ? (int) $tr->term_id : 0;
852 if ( ! $tid ) {
853 continue;
854 }
855 $t = get_term( $tid, $taxonomy );
856 $out[ $lang ] = [
857 'term_id' => $tid,
858 'name' => ( $t && ! is_wp_error( $t ) ) ? $t->name : ( isset( $tr->name ) ? $tr->name : '' ),
859 ];
860 }
861 }
862 }
863
864 return $out;
865 }
866
867 /**
868 * Candidate source terms for the "This is a translation of" dropdown — terms in
869 * $source_lang (default language) that aren't yet translated into $target_lang.
870 *
871 * @return array<int,array{term_id:int,name:string}>
872 */
873 public static function get_translation_candidates( $taxonomy, $target_lang, $source_lang ) {
874 $candidates = [];
875 $target_lang = sanitize_text_field( (string) $target_lang );
876 $source_lang = sanitize_text_field( (string) $source_lang );
877 if ( $taxonomy === '' || $source_lang === '' ) {
878 return $candidates;
879 }
880
881 // WPML
882 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
883 global $sitepress;
884 if ( $sitepress && method_exists( $sitepress, 'get_elements_without_translations' ) ) {
885 $ttids = $sitepress->get_elements_without_translations( 'tax_' . $taxonomy, $target_lang, $source_lang );
886 foreach ( (array) $ttids as $ttid ) {
887 $t = get_term_by( 'term_taxonomy_id', (int) $ttid, $taxonomy );
888 if ( $t && ! is_wp_error( $t ) ) {
889 $candidates[] = [ 'term_id' => (int) $t->term_id, 'name' => $t->name ];
890 }
891 }
892 }
893 return $candidates;
894 }
895
896 // Polylang — source-lang terms whose group lacks the target language.
897 if ( function_exists( 'pll_get_term_translations' ) && function_exists( 'pll_get_term_language' ) ) {
898 $terms = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => false, 'lang' => $source_lang ] );
899 foreach ( (array) $terms as $t ) {
900 if ( is_wp_error( $t ) ) {
901 continue;
902 }
903 $group = pll_get_term_translations( $t->term_id );
904 if ( ! isset( $group[ $target_lang ] ) ) {
905 $candidates[] = [ 'term_id' => (int) $t->term_id, 'name' => $t->name ];
906 }
907 }
908 }
909
910 return $candidates;
911 }
912
913 /**
914 * Set a term's language and (optionally) link it into the translation group of
915 * $translation_of_term_id. Empty $translation_of_term_id = standalone.
916 *
917 * @param \WP_Term $term
918 * @param string $lang_code
919 * @param int $translation_of_term_id
920 */
921 public static function link_term_translation( $term, $lang_code, $translation_of_term_id = 0 ) {
922 $lang_code = sanitize_text_field( (string) $lang_code );
923 if ( $lang_code === '' || ! is_object( $term ) || empty( $term->term_id ) ) {
924 return;
925 }
926 $taxonomy = isset( $term->taxonomy ) ? $term->taxonomy : 'doc_category';
927 $translation_of_term_id = (int) $translation_of_term_id;
928
929 // Polylang
930 if ( function_exists( 'pll_set_term_language' ) ) {
931 pll_set_term_language( $term->term_id, $lang_code );
932 if ( $translation_of_term_id && function_exists( 'pll_save_term_translations' ) ) {
933 $group = function_exists( 'pll_get_term_translations' )
934 ? (array) pll_get_term_translations( $translation_of_term_id )
935 : [];
936 $group[ $lang_code ] = $term->term_id;
937 pll_save_term_translations( $group );
938 }
939 return;
940 }
941
942 // WPML
943 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && ! empty( $term->term_taxonomy_id ) ) {
944 $el_type = 'tax_' . $taxonomy;
945 $trid = null;
946 $src = null;
947
948 if ( $translation_of_term_id ) {
949 $source = get_term( $translation_of_term_id, $taxonomy );
950 if ( $source && ! is_wp_error( $source ) ) {
951 $trid = apply_filters( 'wpml_element_trid', null, $source->term_taxonomy_id, $el_type );
952 $src = self::get_term_language( $source );
953 }
954 }
955
956 do_action( 'wpml_set_element_language_details', [
957 'element_id' => $term->term_taxonomy_id,
958 'element_type' => $el_type,
959 'trid' => $trid,
960 'language_code' => $lang_code,
961 'source_language_code' => $src,
962 ] );
963 }
964 }
965
966 public static function get_current_letter_docs( $current_letter, $limit = 0 ) {
967 global $wpdb;
968
969 $limit = absint( $limit );
970 $limit_sql = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
971
972 // Check if the encyclopedia_prefix parameter is set
973
974 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
975 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
976 $encyclopedia_root_slug = betterdocs()->settings->get( 'encyclopedia_root_slug', 'encyclopdia' );
977 // Sanitize values that may be interpolated into raw SQL fragments below.
978 $encyclopedia_root_slug = sanitize_title( $encyclopedia_root_slug );
979
980 // if($enable_glossaries && $encyclopeia_suorce === 'glossaries'){
981 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
982 $lang_join = '';
983 $lang_where = '';
984
985 // Add language filtering if multilingual plugin is active and we should apply filtering
986 $current_language = self::get_current_language();
987 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
988 // Restrict language code to a safe character set before SQL interpolation.
989 $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
990 // For WPML, use icl_translations table
991 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
992 $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'";
993 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
994 }
995 // For Polylang, use term_relationships with language taxonomy
996 elseif ( function_exists( 'pll_current_language' ) ) {
997 $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";
998 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
999 }
1000 }
1001
1002 $query = "
1003 SELECT
1004 t.term_id,
1005 t.name AS post_title,
1006 t.slug as slug,
1007 '' AS post_excerpt,
1008 CONCAT('" . get_home_url() . "/$encyclopedia_root_slug/', t.slug) AS permalink,
1009 tt.description AS post_content,
1010 JSON_OBJECT(
1011 'status', COALESCE(MAX(CASE WHEN m.meta_key = 'status' THEN m.meta_value END), ''),
1012 'glossary_term_description', COALESCE(MAX(CASE WHEN m.meta_key = 'glossary_term_description' THEN m.meta_value END), '')
1013 ) AS meta_data
1014 FROM
1015 {$wpdb->terms} t
1016 INNER JOIN
1017 {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
1018 LEFT JOIN
1019 {$wpdb->termmeta} m ON t.term_id = m.term_id
1020 $lang_join
1021 WHERE
1022 tt.taxonomy = 'glossaries'
1023 AND
1024 SUBSTRING(t.name, 1, 1) = %s
1025 $lang_where
1026 GROUP BY
1027 t.term_id
1028 ORDER BY
1029 t.name ASC
1030 $limit_sql
1031 ";
1032 } else {
1033 $lang_join = '';
1034 $lang_where = '';
1035
1036 // Add language filtering for docs if multilingual plugin is active and we should apply filtering
1037 $current_language = self::get_current_language();
1038 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
1039 // Restrict language code to a safe character set before SQL interpolation.
1040 $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
1041 // For WPML, use icl_translations table
1042 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1043 $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'";
1044 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
1045 }
1046 // For Polylang, use term_relationships with language taxonomy
1047 elseif ( function_exists( 'pll_current_language' ) ) {
1048 $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";
1049 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
1050 }
1051 }
1052
1053 $query = "
1054 SELECT ID, post_title, post_excerpt, guid, post_content
1055 FROM {$wpdb->posts}
1056 $lang_join
1057 WHERE post_type = 'docs'
1058 AND post_status = 'publish'
1059 AND SUBSTRING(post_title, 1, 1) = %s
1060 $lang_where
1061 ORDER BY post_date DESC
1062 $limit_sql
1063 ";
1064 }
1065
1066 $current_letter_docs = $wpdb->get_results( $wpdb->prepare( $query, $current_letter ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1067
1068 return $current_letter_docs;
1069 }
1070
1071 public static function docs_sort_by_letter( $limit = 10 ) {
1072 global $wpdb;
1073 $enable_non_latin = betterdocs()->settings->get( 'encyclopedia_enable_non_latin' );
1074 $script = betterdocs()->settings->get( 'encyclopedia_non_latin_option' );
1075 $letters = Helper::get_character_range( $enable_non_latin, $script );
1076
1077 $docs_by_letter = [];
1078 $encyclopeia_suorce = betterdocs()->settings->get( 'encyclopedia_source', 'docs' );
1079 $enable_glossaries = betterdocs()->settings->get( 'enable_glossaries', false );
1080
1081 foreach ( $letters as $letter ) {
1082 $posts = self::get_current_letter_docs( $letter, $limit );
1083
1084 if ( is_array( $posts ) && ! empty( $posts ) ) {
1085 foreach ( $posts as $post ) {
1086 $description = isset($post['meta_data']) ? \json_decode( $post['meta_data'], true ) : '';
1087 $glossary_term_description = $description['glossary_term_description'] ?? '';
1088
1089 // Remove any <p> tags or other unwanted HTML tags
1090 $glossary_term_description = wp_strip_all_tags( $glossary_term_description );
1091 $post_excerpt = wp_strip_all_tags( $post['post_excerpt'] ?? '' );
1092
1093 // Prepare post data
1094 if ( $enable_glossaries && $encyclopeia_suorce === 'glossaries' ) {
1095 // For glossaries
1096 $permalink = '';
1097
1098 if ( isset( $post['slug'] ) ) {
1099 $term_link = get_term_link( $post['slug'], 'glossaries' );
1100
1101 if ( ! is_wp_error( $term_link ) ) {
1102 $permalink = $term_link;
1103 }
1104 }
1105
1106 $post_data = [
1107 'id' => $post['term_id'] ?? '',
1108 'post_title' => $post['post_title'] ?? '',
1109 'post_excerpt' => ! empty( $post_excerpt )
1110 ? $post_excerpt
1111 : ( ! empty( $glossary_term_description )
1112 ? self::get_custom_excerpt( $glossary_term_description, 15 )
1113 : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ) ),
1114 'permalink' => $permalink,
1115 ];
1116 } else {
1117 // For docs
1118 $post_data = [
1119 'id' => $post['ID'] ?? '',
1120 'post_title' => $post['post_title'] ?? '',
1121 'post_excerpt' => ! empty( $post_excerpt )
1122 ? $post_excerpt
1123 : self::get_custom_excerpt( wp_strip_all_tags( $post['post_content'] ?? '' ), 15 ),
1124 'permalink' => isset( $post['ID'] ) ? get_the_permalink( $post['ID'] ) : ''
1125 ];
1126 }
1127
1128 $docs_by_letter[$letter][] = $post_data;
1129 }
1130 }
1131 }
1132
1133 return $docs_by_letter;
1134 }
1135
1136 public static function get_glossaries() {
1137 global $wpdb;
1138
1139 $lang_join = '';
1140 $lang_where = '';
1141
1142 // Add language filtering if multilingual plugin is active and we should apply filtering
1143 $current_language = self::get_current_language();
1144 if ( $current_language && self::is_multilingual_active() && self::should_apply_language_filtering() ) {
1145 // Restrict language code to a safe character set before SQL interpolation.
1146 $current_language = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $current_language );
1147 // For WPML, use icl_translations table
1148 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
1149 $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'";
1150 $lang_where = " AND (icl_t.language_code = '$current_language' OR icl_t.language_code IS NULL)";
1151 }
1152 // For Polylang, use term_relationships with language taxonomy
1153 elseif ( function_exists( 'pll_current_language' ) ) {
1154 $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";
1155 $lang_where = " AND (t_lang.slug = '$current_language' OR t_lang.slug IS NULL)";
1156 }
1157 }
1158
1159 $query = "
1160 SELECT t.name
1161 FROM {$wpdb->terms} t
1162 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
1163 $lang_join
1164 WHERE tt.taxonomy = 'glossaries'
1165 $lang_where
1166 ORDER BY t.name ASC
1167 ";
1168
1169 $glossaries = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1170
1171 return $glossaries;
1172 }
1173
1174 /**
1175 * 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)
1176 *
1177 * @param string $layout
1178 * @return string $layout
1179 */
1180 public static function determine_search_layout( $layout ) {
1181 if ( $layout ) {
1182 return $layout;
1183 }
1184
1185 $search_layout = betterdocs()->customizer->defaults->get( 'betterdocs_search_layout_select' );
1186 $docs_layout = betterdocs()->customizer->defaults->get( 'betterdocs_docs_layout_select' );
1187 $archive_page_layout = betterdocs()->customizer->defaults->get( 'betterdocs_archive_layout_select' );
1188 $single_layout = betterdocs()->customizer->defaults->get( 'betterdocs_single_layout_select' );
1189
1190 if ( is_post_type_archive( 'docs' ) ) {
1191 if ( $docs_layout != "layout-7" && ! $search_layout ) {
1192 $layout = 'layout-1';
1193 } else if ( $docs_layout == 'layout-7' && ! $search_layout ) {
1194 $layout = 'layout-2';
1195 }
1196 } else if ( is_tax( 'doc_tag' ) && ! $search_layout ) {
1197 $layout = 'layout-1';
1198 } else if ( is_tax( 'doc_category' ) ) {
1199 if ( $archive_page_layout != 'layout-7' && $archive_page_layout != 'layout-8' && ! $search_layout ) {
1200 $layout = 'layout-1';
1201 } else if ( ( $archive_page_layout == 'layout-7' && ! $search_layout ) || ( $archive_page_layout == 'layout-8' && ! $search_layout ) ) {
1202 $layout = 'layout-2';
1203 }
1204 } else if ( is_singular( 'docs' ) ) {
1205 if ( $single_layout != 'layout-8' && $single_layout != 'layout-9' && ! $search_layout ) {
1206 $layout = 'layout-1';
1207 } else if ( ( $single_layout == 'layout-8' && ! $search_layout ) || ( $single_layout == 'layout-9' && ! $search_layout ) ) {
1208 $layout = 'layout-2';
1209 }
1210 }
1211
1212 return $layout;
1213 }
1214 public static function mb_ord_fallback( $char ) {
1215 $code = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) );
1216 return $code[1];
1217 }
1218
1219 public static function mb_chr_fallback( $code ) {
1220 return mb_convert_encoding( pack( 'N', $code ), 'UTF-8', 'UCS-4BE' );
1221 }
1222
1223 public static function unicodeRange( $start, $end ) {
1224 $range = [];
1225 for ( $i = self::mb_ord_fallback( $start ); $i <= self::mb_ord_fallback( $end ); $i++ ) {
1226 $range[] = self::mb_chr_fallback( $i );
1227 }
1228 return $range;
1229 }
1230
1231 public static function get_character_range( $enable_non_latin, $script ) {
1232 if ( $enable_non_latin ) {
1233 switch ( $script ) {
1234 case 'arabic':
1235 return self::unicodeRange( 'ء', 'ي' );
1236 case 'cyrillic':
1237 return self::unicodeRange( 'А', 'Я' );
1238 case 'hebrew':
1239 return self::unicodeRange( 'א', 'ת' );
1240 case 'greek':
1241 return self::unicodeRange( 'Α', 'Ω' );
1242 default:
1243 return range( 'A', 'Z' );
1244 }
1245 }
1246
1247 return range( 'A', 'Z' );
1248 }
1249
1250 public static function get_the_top_most_parent( $term_id ) {
1251 while ( $term_id != 0 ) {
1252 $parent_id = wp_get_term_taxonomy_parent_id( $term_id, 'doc_category' );
1253
1254 if ( $parent_id == 0 ) {
1255 break;
1256 }
1257
1258 $term_id = $parent_id;
1259 }
1260 return $term_id;
1261 }
1262
1263 public static function get_highest_docs_term() {
1264 $terms = get_terms( [
1265 'taxonomy' => 'doc_category', // Change to your desired taxonomy
1266 'hide_empty' => true, // Only show terms with posts
1267 'orderby' => 'count', // Order by post count
1268 'order' => 'DESC', // Descending order
1269 'number' => 1 // Get only the top term
1270 ] );
1271 return isset( $terms[0] ) ? $terms[0] : [];
1272 }
1273
1274 public static function delete_specific_faq_posts_by_faq_category( $term_id, $taxonomy = 'betterdocs_faq_category' ) {
1275 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- targeted bulk delete by FAQ category; tax filter is required.
1276 $args = [
1277 'post_type' => 'betterdocs_faq',
1278 'posts_per_page' => -1,
1279 'tax_query' => [
1280 [
1281 'taxonomy' => $taxonomy,
1282 'field' => 'id',
1283 'terms' => $term_id,
1284 'operator' => 'IN'
1285 ]
1286 ],
1287 'fields' => 'ids'
1288 ];
1289
1290 $query = new \WP_Query( $args );
1291
1292 if ( $query->have_posts() ) {
1293 foreach ( $query->posts as $doc_id ) {
1294 wp_delete_post( $doc_id, true );
1295 }
1296 }
1297 }
1298
1299 /**
1300 * Function To Normalize Repeater Field For Quick Builder
1301 *
1302 * @param array $fields
1303 * @param array $include_field_keys
1304 *
1305 * @return array
1306 */
1307 public static function normalize_repeater_field( $fields, $include_field_keys = [] ) {
1308 if( empty( $include_field_keys ) ) {
1309 return $fields;
1310 }
1311
1312 $normalized_fields = [];
1313
1314 foreach( $fields as $field ) {
1315 foreach( $include_field_keys as $field_key ) {
1316 if( ! isset( $normalized_fields[$field_key] ) ) {
1317 $normalized_fields[$field_key] = isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [];
1318 } else {
1319 array_push( $normalized_fields[$field_key], ...( isset( $field[$field_key] ) && ! empty( $field[$field_key] ) ? $field[$field_key] : [] ) );
1320 $normalized_fields[$field_key] = array_unique( $normalized_fields[$field_key] );
1321 }
1322 }
1323 }
1324
1325 return $normalized_fields;
1326 }
1327
1328 public static function get_local_plugin_data( $basename = '' ) {
1329 if ( empty( $basename ) ) {
1330 return false;
1331 }
1332
1333 if ( !function_exists( 'get_plugins' ) ) {
1334 include_once ABSPATH . 'wp-admin/includes/plugin.php';
1335 }
1336
1337 $plugins = get_plugins();
1338
1339 if ( !isset( $plugins[ $basename ] ) ) {
1340 return false;
1341 }
1342
1343 return $plugins[ $basename ];
1344 }
1345
1346 /**
1347 * Get default file icon based on programming language
1348 *
1349 * @param string $language Programming language identifier
1350 * @return string Emoji icon for the language
1351 */
1352 public static function get_file_icon_by_language( $language ) {
1353 $icons = [
1354 'javascript' => '📄',
1355 'typescript' => '📘',
1356 'jsx' => '⚛️',
1357 'tsx' => '⚛️',
1358 'html' => '🌐',
1359 'css' => '🎨',
1360 'scss' => '🎨',
1361 'sass' => '🎨',
1362 'less' => '🎨',
1363 'php' => '🐘',
1364 'python' => '🐍',
1365 'java' => '',
1366 'csharp' => '🔷',
1367 'cpp' => '⚙️',
1368 'c' => '⚙️',
1369 'ruby' => '💎',
1370 'go' => '🐹',
1371 'rust' => '🦀',
1372 'swift' => '🦉',
1373 'kotlin' => '🎯',
1374 'sql' => '🗃️',
1375 'json' => '📋',
1376 'yaml' => '📋',
1377 'xml' => '📄',
1378 'markdown' => '📝',
1379 'bash' => '💻',
1380 'shell' => '💻',
1381 'powershell' => '💻',
1382 'dockerfile' => '🐳',
1383 ];
1384
1385 return isset( $icons[$language] ) ? $icons[$language] : '📄';
1386 }
1387
1388 /**
1389 * Check if AI Chatbot is enabled
1390 *
1391 * @return bool
1392 */
1393 public function is_ai_chatbot_enabled() {
1394 $chatbot_active = is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
1395 $chatbot_license_valid = get_option( 'betterdocs_chatbot_software__license_status' ) === 'valid';
1396 $chatbot_enabled = betterdocs()->settings->get( 'enable_ai_chatbot', false );
1397
1398 // AI Search Suggestions are enabled if all conditions are met
1399 return $chatbot_active && $chatbot_license_valid && $chatbot_enabled;
1400 }
1401
1402 /**
1403 * Check if tags are enabled and post has tags
1404 *
1405 * @return bool
1406 */
1407 public function is_tag_enabled() {
1408 global $post;
1409 $product_terms = wp_get_object_terms( $post->ID, 'doc_tag' );
1410 $enable_tags = betterdocs()->settings->get( 'enable_tags', false );
1411 return ! empty( $product_terms ) && $enable_tags;
1412 }
1413
1414 /**
1415 * Check if AI Search Suggestions are enabled
1416 *
1417 * @return bool
1418 */
1419 public function is_ai_search_suggestions_enabled() {
1420 $ai_search_suggestions_active = is_plugin_active( 'betterdocs-ai-search-suggestions/betterdocs-ai-search-suggestions.php' );
1421 $ai_search_suggestions_license_valid = get_option( 'betterdocs_ai_search_suggestions_software__license_status' ) === 'valid';
1422 $ai_search_suggestions_enabled = betterdocs()->settings->get( 'enable_ai_powered_search', false );
1423
1424 return $ai_search_suggestions_active && $ai_search_suggestions_license_valid && $ai_search_suggestions_enabled;
1425 }
1426
1427 /**
1428 * Get the maximum order value from the 'doc_category_order' term meta
1429 *
1430 * @return int
1431 */
1432 public static function get_max_doc_category_order_from_term_meta() {
1433 global $wpdb;
1434 $sql = $wpdb->prepare( "SELECT MAX(CAST(meta_value AS UNSIGNED)) AS max FROM {$wpdb->termmeta} WHERE meta_key = %s ", 'doc_category_order' );
1435 $result = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is prepared above.
1436 return $result;
1437 }
1438 }
1439