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