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/Core/Settings.php +80 -69 4.8.04.9.2 View file →
@@ -10,8 +10,9 @@
10 10 use WP_User;
11 11 use WPDeveloper\BetterDocs\Admin\Builder\GlobalFields;
12 12 use WPDeveloper\BetterDocs\Admin\Builder\Rules;
13 13 use WPDeveloper\BetterDocs\AI\ModelRegistry;
14 +use WPDeveloper\BetterDocs\AI\ProviderFactory;
14 15 use WPDeveloper\BetterDocs\REST\AIEdit;
15 16 use WPDeveloper\BetterDocs\Utils\AIHelper;
16 17 use WPDeveloper\BetterDocs\Utils\Base;
17 18 use WPDeveloper\BetterDocs\Utils\Database;
@@ -53,9 +54,8 @@
53 54
54 55 add_action( 'wp_ajax_betterdocs_dark_mode', array( $this, 'dark_mode' ) );
55 56 add_filter( 'betterdocs_settings_tab_advance', array( $this, 'hide_roles_management' ), 11, 1 );
56 57 add_action( 'betterdocs::settings::saved', array( $this, 'fallback_slugs' ), 99, 3 );
57 - add_action( 'admin_init', array( $this, 'maybe_migrate_ai_platform_settings' ) );
58 58 }
59 59
60 60 public function fallback_slugs( $_saved, $_settings, $_old_settings = array() ) {
61 61 $_default = $this->get_default();
@@ -82,72 +82,28 @@
82 82 /**
83 83 * Settings keys holding secret API keys. These are masked before reaching
84 84 * the browser, stripped for non-admins, and never persisted as their mask.
85 85 *
86 - * Includes the legacy single-key fields plus the per-platform content-suite
87 - * keys introduced with multi-platform support.
86 + * Covers the AI Chatbot key plus every content-suite platform key. OpenAI's
87 + * is `ai_autowrite_api_key` (see ProviderFactory::key_field_for), hence the
88 + * dedupe.
88 89 *
89 90 * @return array<int,string>
90 91 */
91 92 public static function sensitive_api_key_fields() {
92 93 $fields = array(
93 - 'ai_autowrite_api_key',
94 + ProviderFactory::OPENAI_KEY_FIELD,
94 95 'ai_chatbot_api_key',
95 96 );
96 97 foreach ( array_keys( ModelRegistry::platforms() ) as $platform ) {
97 - $fields[] = 'ai_api_key_' . $platform;
98 + $fields[] = ProviderFactory::key_field_for( $platform );
98 99 }
99 100 // Add-ons (e.g. the AI Chatbot) register their own per-platform keys here
100 101 // so they are masked in the browser and stripped for non-admins.
101 - return apply_filters( 'betterdocs_sensitive_api_key_fields', $fields );
102 + return apply_filters( 'betterdocs_sensitive_api_key_fields', array_values( array_unique( $fields ) ) );
102 103 }
103 104
104 105 /**
105 - * One-time copy of pre-multi-platform AI settings onto the new keys so the
106 - * settings UI shows the user's existing OpenAI key and model under the new
107 - * platform-aware fields. Runtime already falls back via ProviderFactory, so
108 - * this only affects what the admin sees. Guarded by an option flag.
109 - *
110 - * Runs on `admin_init`, which fires for every logged-in user who loads
111 - * /wp-admin/ — including a Subscriber. Since this writes settings (and copies
112 - * the legacy OpenAI key onto `ai_api_key_openai`), it is gated on the same
113 - * `edit_docs_settings` capability used everywhere else for settings writes.
114 - * The one-shot flag is deliberately NOT set when the capability check fails,
115 - * otherwise the first low-privilege page load would permanently skip the
116 - * migration for administrators too.
117 - *
118 - * @return void
119 - */
120 - public function maybe_migrate_ai_platform_settings() {
121 - if ( get_option( 'betterdocs_ai_platform_migrated' ) ) {
122 - return;
123 - }
124 -
125 - if ( ! current_user_can( 'edit_docs_settings' ) ) {
126 - return;
127 - }
128 -
129 - if ( '' === (string) $this->get_raw_field( 'ai_platform', '' ) ) {
130 - $this->save( 'ai_platform', 'openai' );
131 - }
132 -
133 - $legacy_key = (string) $this->get( 'ai_autowrite_api_key', '' );
134 - if ( '' !== $legacy_key && '' === (string) $this->get_raw_field( 'ai_api_key_openai', '' ) ) {
135 - $this->save( 'ai_api_key_openai', $legacy_key );
136 - }
137 -
138 - if ( '' === (string) $this->get_raw_field( 'ai_model', '' ) ) {
139 - $legacy_model = (string) $this->get( 'write_with_ai_model', '' );
140 - if ( '' === $legacy_model ) {
141 - $legacy_model = (string) $this->get( 'article_summary_model', 'gpt-4o-mini' );
142 - }
143 - $this->save( 'ai_model', $legacy_model !== '' ? $legacy_model : 'gpt-4o-mini' );
144 - }
145 -
146 - update_option( 'betterdocs_ai_platform_migrated', 1 );
147 - }
148 -
149 - /**
150 106 * This method is responsible for enqueueing scripts in settings panel
151 107 *
152 108 * @param string $hook
153 109 *
@@ -327,8 +283,12 @@
327 283 'enable_breadcrumb_category' => true,
328 284 'enable_breadcrumb_title' => true,
329 285 'enable_sidebar_cat_list' => true,
330 286 'enable_print_icon' => true,
287 + 'print_enable_logo' => false,
288 + 'print_logo' => array(),
289 + 'print_enable_footer' => false,
290 + 'print_footer_text' => '',
331 291 'enable_tags' => true,
332 292 'email_feedback' => true,
333 293 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
334 294 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -367,13 +327,13 @@
367 327 'article_summary_model' => 'gpt-4o-mini',
368 328 'article_summary_max_token' => 1500,
369 329 // Multi-platform AI (content suite). `ai_platform` selects the active
370 330 // provider; `ai_model` is the single global model; keys are stored
371 - // per platform so switching never loses a saved key. Legacy keys
372 - // above are kept for back-compat and migrated on upgrade.
331 + // per platform so switching never loses a saved key. OpenAI reuses
332 + // `ai_autowrite_api_key` above — it has always held an OpenAI key,
333 + // so nothing needs migrating.
373 334 'ai_platform' => 'openai',
374 335 'ai_model' => 'gpt-4o-mini',
375 - 'ai_api_key_openai' => '',
376 336 'ai_api_key_gemini' => '',
377 337 'ai_api_key_claude' => '',
378 338 'ai_api_key_deepseek' => '',
379 339 'ai_api_key_openrouter' => '',
@@ -387,9 +347,21 @@
387 347 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
388 348 'betterdocs_access_control_repeater' => array(),
389 349 'internal_knowledge_base_type' => 'basic',
390 350 'betterdocs_access_control_repeater_kb' => array(),
391 - 'enable_git_integration' => false
351 + 'enable_git_integration' => false,
352 + /**
353 + * MCP master switch. Off by default; the toggle lives on the
354 + * BetterDocs → MCP page, not in the settings tree, and writes
355 + * through POST betterdocs/v1/settings.
356 + *
357 + * The key has to be listed here or `get()` cannot see it at all:
358 + * it answers `$default` for anything absent from the defaults
359 + * array, whatever the stored option holds.
360 + *
361 + * @since 4.9.0
362 + */
363 + 'enable_mcp' => ''
392 364 );
393 365
394 366 $_default = apply_filters( 'betterdocs_default_settings', $_default );
395 367 // $_default = apply_filters_deprecated(
@@ -1428,8 +1400,44 @@
1428 1400 'enable_disable_text_active' => true,
1429 1401 'default' => 1,
1430 1402 'priority' => 3
1431 1403 ),
1404 + 'print_enable_logo' => array(
1405 + 'name' => 'print_enable_logo',
1406 + 'type' => 'toggle',
1407 + 'label' => __( 'Logo on Printed Doc', 'betterdocs' ),
1408 + 'label_subtitle' => __( 'Show a logo at the top of the printed / PDF page', 'betterdocs' ),
1409 + 'enable_disable_text_active' => true,
1410 + 'default' => 0,
1411 + 'priority' => 4
1412 + ),
1413 + 'print_logo' => array(
1414 + 'name' => 'print_logo',
1415 + 'type' => 'media',
1416 + 'value' => '',
1417 + 'label' => __( 'Print Logo', 'betterdocs' ),
1418 + 'label_subtitle' => __( 'Leave empty to use your site logo, or the site icon when no site logo is set', 'betterdocs' ),
1419 + 'priority' => 5,
1420 + 'rules' => Rules::is( 'print_enable_logo', true )
1421 + ),
1422 + 'print_enable_footer' => array(
1423 + 'name' => 'print_enable_footer',
1424 + 'type' => 'toggle',
1425 + 'label' => __( 'Footer on Printed Doc', 'betterdocs' ),
1426 + 'label_subtitle' => __( 'Show a footer on every page of the printed / PDF document', 'betterdocs' ),
1427 + 'enable_disable_text_active' => true,
1428 + 'default' => 0,
1429 + 'priority' => 6
1430 + ),
1431 + 'print_footer_text' => array(
1432 + 'name' => 'print_footer_text',
1433 + 'type' => 'textarea',
1434 + 'label' => __( 'Print Footer Text', 'betterdocs' ),
1435 + 'label_subtitle' => __( 'Leave empty to use the site name and current year', 'betterdocs' ),
1436 + 'default' => '',
1437 + 'priority' => 7,
1438 + 'rules' => Rules::is( 'print_enable_footer', true )
1439 + ),
1432 1440 'enable_tags' => array(
1433 1441 'name' => 'enable_tags',
1434 1442 'type' => 'toggle',
1435 1443 'label' => __( 'Tags', 'betterdocs' ),
@@ -1434,9 +1442,9 @@
1434 1442 'type' => 'toggle',
1435 1443 'label' => __( 'Tags', 'betterdocs' ),
1436 1444 'enable_disable_text_active' => true,
1437 1445 'default' => 1,
1438 - 'priority' => 4
1446 + 'priority' => 8
1439 1447 ),
1440 1448 'show_last_update_time' => array(
1441 1449 'name' => 'show_last_update_time',
1442 1450 'type' => 'toggle',
@@ -1442,9 +1450,9 @@
1442 1450 'type' => 'toggle',
1443 1451 'label' => __( 'Last Update Time', 'betterdocs' ),
1444 1452 'enable_disable_text_active' => true,
1445 1453 'default' => 1,
1446 - 'priority' => 5
1454 + 'priority' => 9
1447 1455 ),
1448 1456 'enable_navigation' => array(
1449 1457 'name' => 'enable_navigation',
1450 1458 'type' => 'toggle',
@@ -1450,9 +1458,9 @@
1450 1458 'type' => 'toggle',
1451 1459 'label' => __( 'Navigation', 'betterdocs' ),
1452 1460 'enable_disable_text_active' => true,
1453 1461 'default' => 1,
1454 - 'priority' => 6
1462 + 'priority' => 10
1455 1463 ),
1456 1464 'enable_comment' => array(
1457 1465 'name' => 'enable_comment',
1458 1466 'type' => 'toggle',
@@ -1458,9 +1466,9 @@
1458 1466 'type' => 'toggle',
1459 1467 'label' => __( 'Comment', 'betterdocs' ),
1460 1468 'enable_disable_text_active' => true,
1461 1469 'default' => '',
1462 - 'priority' => 7
1470 + 'priority' => 11
1463 1471 ),
1464 1472 'enable_credit' => array(
1465 1473 'name' => 'enable_credit',
1466 1474 'type' => 'toggle',
@@ -1466,9 +1474,9 @@
1466 1474 'type' => 'toggle',
1467 1475 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1468 1476 'enable_disable_text_active' => true,
1469 1477 'default' => '',
1470 - 'priority' => 8
1478 + 'priority' => 12
1471 1479 ),
1472 1480 'reaction_feedback_text' => array(
1473 1481 'name' => 'reaction_feedback_text',
1474 1482 'type' => 'text',
@@ -1473,9 +1481,9 @@
1473 1481 'name' => 'reaction_feedback_text',
1474 1482 'type' => 'text',
1475 1483 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1476 1484 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1477 - 'priority' => 9
1485 + 'priority' => 13
1478 1486 ),
1479 1487 'enable_estimated_reading_time' => array(
1480 1488 'name' => 'enable_estimated_reading_time',
1481 1489 'type' => 'toggle',
@@ -1481,9 +1489,9 @@
1481 1489 'type' => 'toggle',
1482 1490 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1483 1491 'enable_disable_text_active' => true,
1484 1492 'default' => 0,
1485 - 'priority' => 10
1493 + 'priority' => 14
1486 1494 ),
1487 1495 'estimated_reading_time_title' => array(
1488 1496 'name' => 'estimated_reading_time_title',
1489 1497 'type' => 'text',
@@ -1488,9 +1496,9 @@
1488 1496 'name' => 'estimated_reading_time_title',
1489 1497 'type' => 'text',
1490 1498 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1491 1499 'default' => '',
1492 - 'priority' => 11,
1500 + 'priority' => 15,
1493 1501 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1494 1502 ),
1495 1503 'estimated_reading_time_text' => array(
1496 1504 'name' => 'estimated_reading_time_text',
@@ -1496,9 +1504,9 @@
1496 1504 'name' => 'estimated_reading_time_text',
1497 1505 'type' => 'text',
1498 1506 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1499 1507 'default' => __( 'min read', 'betterdocs' ),
1500 - 'priority' => 12,
1508 + 'priority' => 16,
1501 1509 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1502 1510 ),
1503 1511 'singular_estimated_reading_time_text' => array(
1504 1512 'name' => 'singular_estimated_reading_time_text',
@@ -1504,9 +1512,9 @@
1504 1512 'name' => 'singular_estimated_reading_time_text',
1505 1513 'type' => 'text',
1506 1514 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1507 1515 'default' => __( 'min read', 'betterdocs' ),
1508 - 'priority' => 13,
1516 + 'priority' => 17,
1509 1517 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1510 1518 )
1511 1519 )
1512 1520 ),
@@ -2400,16 +2408,18 @@
2400 2408 'default' => 'openai',
2401 2409 // DeepSeek and OpenRouter are temporarily hidden from the
2402 2410 // dropdown. Filter the options here, NOT ModelRegistry::platforms() —
2403 2411 // sensitive_api_key_fields() loops the registry to mask each
2404 - // ai_api_key_{platform}, so trimming the registry would silently
2412 + // platform's key field, so trimming the registry would silently
2405 2413 // un-mask those keys. Re-enable later by dropping the array_diff_key.
2406 2414 'options' => GlobalFields::normalize_fields(
2407 2415 array_diff_key( ModelRegistry::platforms(), array_flip( array( 'deepseek', 'openrouter' ) ) )
2408 2416 )
2409 2417 ),
2410 - 'ai_api_key_openai' => array(
2411 - 'name' => 'ai_api_key_openai',
2418 + // OpenAI's key field keeps its original name so installs
2419 + // that already saved a Write with AI key keep it.
2420 + 'ai_autowrite_api_key' => array(
2421 + 'name' => 'ai_autowrite_api_key',
2412 2422 'type' => 'text',
2413 2423 'label' => __( 'OpenAI API Key', 'betterdocs' ),
2414 2424 'label_subtitle' => sprintf( /* translators: %s: documentation URL */ __( 'Check out this <a target="_blank" href="%s">documentation</a> to generate your OpenAI API key.', 'betterdocs' ), esc_url( 'https://betterdocs.co/docs/write-with-ai/' ) ),
2415 2425 'default' => '',
@@ -2526,9 +2536,10 @@
2526 2536 'priority' => 6,
2527 2537 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2528 2538 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2529 2539 'enable_disable_text_active' => true,
2530 - 'default' => true
2540 + 'default' => true,
2541 + 'is_pro' => true
2531 2542 ),
2532 2543 'enable_docs_ai_suite' => array(
2533 2544 'name' => 'enable_docs_ai_suite',
2534 2545 'type' => 'toggle',