PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
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 +186 -2 4.6.14.9.2 View file →
@@ -20,10 +20,16 @@
20 20
21 21 class Helper extends Base {
22 22
23 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.
24 + * Mask an API key for safe display.
25 + *
26 + * Prefix-aware: when the key carries a recognizable provider prefix
27 + * (OpenAI sk-/sk-proj-, Anthropic sk-ant-/sk-ant-api03-, Gemini AIza) that
28 + * prefix is kept visible so an admin can tell which provider/key is set,
29 + * then a fixed 8-asterisk block, then the last 4 chars. Keys without a known
30 + * prefix fall back to first 3 + 8 asterisks + last 4. The asterisk count is
31 + * always fixed so the real key length is never leaked.
26 32 */
27 33 public static function mask_api_key( $key ) {
28 34 if ( ! is_string( $key ) || $key === '' ) {
29 35 return '';
@@ -28,8 +34,21 @@
28 34 if ( ! is_string( $key ) || $key === '' ) {
29 35 return '';
30 36 }
31 37 $key = trim( $key );
38 + if ( $key === '' ) {
39 + return '';
40 + }
41 +
42 + // Longest prefixes first so sk-proj-/sk-ant- win over the bare sk-.
43 + $prefixes = array( 'sk-ant-api03-', 'sk-ant-', 'sk-proj-', 'sk-', 'AIza' );
44 + foreach ( $prefixes as $prefix ) {
45 + if ( strncmp( $key, $prefix, strlen( $prefix ) ) === 0
46 + && strlen( $key ) >= strlen( $prefix ) + 4 ) {
47 + return $prefix . str_repeat( '*', 8 ) . substr( $key, -4 );
48 + }
49 + }
50 +
32 51 if ( strlen( $key ) < 8 ) {
33 52 return str_repeat( '*', strlen( $key ) );
34 53 }
35 54 return substr( $key, 0, 3 ) . str_repeat( '*', 8 ) . substr( $key, -4 );
@@ -34,8 +53,35 @@
34 53 }
35 54 return substr( $key, 0, 3 ) . str_repeat( '*', 8 ) . substr( $key, -4 );
36 55 }
37 56
57 + /**
58 + * Resolve the WPML-translated base slug of a taxonomy for the CURRENT language.
59 + *
60 + * WPML registers each translatable taxonomy's rewrite slug as a string named
61 + * "URL <taxonomy> tax slug" in the "WordPress" domain (e.g. "URL doc_tag tax slug").
62 + * BetterDocs stores only the default-language slug in its settings, so routing and
63 + * term links must read the translated value back here. Returns the trimmed default
64 + * slug unchanged when WPML is inactive or the string has no translation.
65 + *
66 + * @param string $taxonomy Taxonomy key, e.g. 'doc_tag'.
67 + * @param string $default_slug Default-language base slug from settings.
68 + * @return string Translated base slug for the active language (falls back to default).
69 + */
70 + public static function wpml_translated_tax_slug( $taxonomy, $default_slug ) {
71 + $default_slug = trim( (string) $default_slug, '/' );
72 +
73 + if ( $default_slug === '' || ! has_filter( 'wpml_translate_single_string' ) ) {
74 + return $default_slug;
75 + }
76 +
77 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML-owned filter; name must be used verbatim.
78 + $translated = apply_filters( 'wpml_translate_single_string', $default_slug, 'WordPress', 'URL ' . $taxonomy . ' tax slug' );
79 + $translated = trim( (string) $translated, '/' );
80 +
81 + return $translated !== '' ? $translated : $default_slug;
82 + }
83 +
38 84 public static function get_plugins( $plugin_basename = null ) {
39 85 if ( ! function_exists( 'get_plugins' ) ) {
40 86 include_once ABSPATH . 'wp-admin/includes/plugin.php';
41 87 }
@@ -301,8 +347,58 @@
301 347 ( class_exists( 'TRP_Translate_Press' ) && function_exists( 'trp_get_current_language' ) );
302 348 }
303 349
304 350 /**
351 + * Configured/active languages from whichever multilingual plugin is present.
352 + *
353 + * Returns a list of { value, label } pairs (language code + display name).
354 + * Used to populate the optional language selector in the Write-with-AI modal;
355 + * returns an empty array when no multilingual plugin is active so the
356 + * selector stays hidden. Mirrors the Pro cross-domain language options.
357 + *
358 + * @return array<int,array{value:string,label:string}>
359 + */
360 + public static function get_active_languages() {
361 + $options = array();
362 +
363 + // WPML
364 + if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
365 + global $sitepress;
366 + if ( $sitepress && method_exists( $sitepress, 'get_active_languages' ) ) {
367 + $active_languages = $sitepress->get_active_languages();
368 + if ( is_array( $active_languages ) ) {
369 + foreach ( $active_languages as $code => $lang ) {
370 + $options[] = array(
371 + 'value' => (string) $code,
372 + 'label' => isset( $lang['native_name'] ) ? $lang['native_name'] : (string) $code,
373 + );
374 + }
375 + }
376 + }
377 + } elseif ( function_exists( 'pll_languages_list' ) ) {
378 + // Polylang
379 + $languages = pll_languages_list( array( 'fields' => array() ) );
380 + if ( is_array( $languages ) ) {
381 + foreach ( $languages as $lang ) {
382 + if ( is_object( $lang ) && isset( $lang->slug ) ) {
383 + $options[] = array(
384 + 'value' => (string) $lang->slug,
385 + 'label' => isset( $lang->name ) ? $lang->name : (string) $lang->slug,
386 + );
387 + }
388 + }
389 + }
390 + }
391 +
392 + /**
393 + * Filter the language options exposed to the Write-with-AI modal.
394 + *
395 + * @param array $options List of { value, label } language pairs.
396 + */
397 + return apply_filters( 'betterdocs_active_languages', $options );
398 + }
399 +
400 + /**
305 401 * Check if we should apply language filtering
306 402 * Only apply on frontend or when specifically requested
307 403 *
308 404 * @return bool
@@ -1295,8 +1391,14 @@
1295 1391 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- targeted bulk delete by FAQ category; tax filter is required.
1296 1392 $args = [
1297 1393 'post_type' => 'betterdocs_faq',
1298 1394 'posts_per_page' => -1,
1395 + // EVERY status, explicitly. WP_Query defaults to 'publish', so "delete this
1396 + // group and its FAQs" was deleting only the published ones — the drafts (and
1397 + // pending/scheduled/private/trashed FAQs) survived, and the wp_delete_term()
1398 + // that follows then stripped their category, leaving them orphaned under
1399 + // "Uncategorized". Note 'any' is NOT enough here: it excludes trash.
1400 + 'post_status' => [ 'publish', 'draft', 'pending', 'future', 'private', 'trash' ],
1299 1401 'tax_query' => [
1300 1402 [
1301 1403 'taxonomy' => $taxonomy,
1302 1404 'field' => 'id',
@@ -1395,8 +1497,9 @@
1395 1497 'json' => '📋',
1396 1498 'yaml' => '📋',
1397 1499 'xml' => '📄',
1398 1500 'markdown' => '📝',
1501 + 'curl' => '💻',
1399 1502 'bash' => '💻',
1400 1503 'shell' => '💻',
1401 1504 'powershell' => '💻',
1402 1505 'dockerfile' => '🐳',
@@ -1402,8 +1505,89 @@
1402 1505 'dockerfile' => '🐳',
1403 1506 ];
1404 1507
1405 1508 return isset( $icons[$language] ) ? $icons[$language] : '📄';
1509 + }
1510 +
1511 + /**
1512 + * Echo the copy-to-clipboard button used by the Code Snippet and Code
1513 + * Snippet Tab templates.
1514 + *
1515 + * Both icons ship in the markup and CSS cross-fades between them on
1516 + * `.is-copied`, so the frontend script never rewrites the SVG. The tooltip
1517 + * carries its own strings as data attributes so the script can swap
1518 + * "Copy" → "Copied!" without hard-coding English.
1519 + *
1520 + * @return void
1521 + */
1522 + public static function code_snippet_copy_button() {
1523 + ?>
1524 + <div class="betterdocs-code-snippet-copy-container">
1525 + <button class="betterdocs-code-snippet-copy-button"
1526 + type="button"
1527 + aria-label="<?php esc_attr_e( 'Copy code to clipboard', 'betterdocs' ); ?>">
1528 + <span class="betterdocs-code-snippet-copy-icon" aria-hidden="true">
1529 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1530 + <rect x="9" y="9" width="12.5" height="12.5" rx="3" stroke="currentColor" stroke-width="1.7"/>
1531 + <path d="M15.5 5.75V5A2.5 2.5 0 0 0 13 2.5H5A2.5 2.5 0 0 0 2.5 5v8A2.5 2.5 0 0 0 5 15.5h.75" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
1532 + </svg>
1533 + </span>
1534 + <span class="betterdocs-code-snippet-copied-icon" aria-hidden="true">
1535 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1536 + <path d="M20 6.5 9.5 17 4 11.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
1537 + </svg>
1538 + </span>
1539 + </button>
1540 + <span class="betterdocs-code-snippet-tooltip"
1541 + role="status"
1542 + data-copy-label="<?php esc_attr_e( 'Copy', 'betterdocs' ); ?>"
1543 + data-copied-label="<?php esc_attr_e( 'Copied!', 'betterdocs' ); ?>"
1544 + data-error-label="<?php esc_attr_e( 'Copy failed', 'betterdocs' ); ?>"><?php esc_html_e( 'Copy', 'betterdocs' ); ?></span>
1545 + </div>
1546 + <?php
1547 + }
1548 +
1549 + /**
1550 + * Human-readable label for a programming-language identifier, used as the
1551 + * language-dropdown label on multi-language code snippets. Mirrors the
1552 + * block's LANGUAGE_OPTIONS; falls back to an upper-cased identifier.
1553 + *
1554 + * @param string $language Programming language identifier
1555 + * @return string
1556 + */
1557 + public static function get_language_label( $language ) {
1558 + $labels = [
1559 + 'javascript' => 'JavaScript',
1560 + 'typescript' => 'TypeScript',
1561 + 'php' => 'PHP',
1562 + 'python' => 'Python',
1563 + 'java' => 'Java',
1564 + 'ruby' => 'Ruby',
1565 + 'curl' => 'cURL',
1566 + 'bash' => 'Bash',
1567 + 'shell' => 'Shell',
1568 + 'json' => 'JSON',
1569 + 'yaml' => 'YAML',
1570 + 'html' => 'HTML',
1571 + 'css' => 'CSS',
1572 + 'scss' => 'SCSS',
1573 + 'sql' => 'SQL',
1574 + 'xml' => 'XML',
1575 + 'cpp' => 'C++',
1576 + 'csharp' => 'C#',
1577 + 'c' => 'C',
1578 + 'go' => 'Go',
1579 + 'rust' => 'Rust',
1580 + 'swift' => 'Swift',
1581 + 'kotlin' => 'Kotlin',
1582 + 'markdown' => 'Markdown'
1583 + ];
1584 +
1585 + if ( isset( $labels[ $language ] ) ) {
1586 + return $labels[ $language ];
1587 + }
1588 +
1589 + return ucwords( str_replace( [ '-', '_' ], ' ', (string) $language ) );
1406 1590 }
1407 1591
1408 1592 /**
1409 1593 * Check if AI Chatbot is enabled