| @@ -83,12 +83,21 @@ | ||
| 83 | 83 | 'gemini_api_key', |
| 84 | 84 | 'gemini_model', |
| 85 | 85 | 'openrouter_api_key', |
| 86 | 86 | 'openrouter_model', |
| 87 | + 'openai_compatible_base_url', | |
| 88 | + 'openai_compatible_api_key', | |
| 89 | + 'openai_compatible_model', | |
| 90 | + 'openai_compatible_timeout', | |
| 91 | + 'openai_compatible_supports_images', | |
| 92 | + 'openai_compatible_json_mode', | |
| 93 | + 'openai_compatible_price_per_million', | |
| 87 | 94 | 'max_tokens', |
| 88 | 95 | 'temperature', |
| 89 | 96 | 'cache_duration', |
| 90 | 97 | 'max_requests_per_minute', |
| 98 | + 'ai_daily_request_limit', | |
| 99 | + 'ai_paused', | |
| 91 | 100 | 'enable_logging', |
| 92 | 101 | 'debug_mode', |
| 93 | 102 | 'api_timeout', |
| 94 | 103 | 'retry_attempts', |
| @@ -257,12 +266,8 @@ | ||
| 257 | 266 | 'seo_analytics_google_analytics_property_id', |
| 258 | 267 | 'ga_analytics_account_id', |
| 259 | 268 | 'ga_analytics_data_stream_id', |
| 260 | 269 | |
| 261 | - // GA4 Tracking Code Injection (Pro) | |
| 262 | - 'ga4_auto_inject', | |
| 263 | - 'ga4_measurement_id', | |
| 264 | - | |
| 265 | 270 | // Search Console configuration |
| 266 | 271 | 'search_console_property', |
| 267 | 272 | |
| 268 | 273 | // AI features |
| @@ -454,8 +459,23 @@ | ||
| 454 | 459 | * @since 1.0.0 |
| 455 | 460 | * |
| 456 | 461 | * @return array Categories information |
| 457 | 462 | */ |
| 463 | + /** | |
| 464 | + * The setting keys a category defines. | |
| 465 | + * | |
| 466 | + * Exposed so callers can reject keys a category does not define instead of | |
| 467 | + * persisting whatever they are handed (#395). | |
| 468 | + * | |
| 469 | + * @since 2.0.1 | |
| 470 | + * | |
| 471 | + * @param string $category Category name. | |
| 472 | + * @return string[] Setting keys, or [] when the category is unknown here. | |
| 473 | + */ | |
| 474 | + public function get_category_keys(string $category): array { | |
| 475 | + return $this->settings_categories[$category]['keys'] ?? []; | |
| 476 | + } | |
| 477 | + | |
| 458 | 478 | public function get_categories(): array { |
| 459 | 479 | $categories = []; |
| 460 | 480 | |
| 461 | 481 | foreach ($this->settings_categories as $key => $config) { |
| @@ -629,8 +649,16 @@ | ||
| 629 | 649 | * @return array Core settings for category |
| 630 | 650 | */ |
| 631 | 651 | private function get_core_settings_by_category(string $category): array { |
| 632 | 652 | $category_config = $this->settings_categories[$category]; |
| 653 | + | |
| 654 | + // Prime the option cache in one query before the loop. Every | |
| 655 | + // thinkrank_* option is autoload=off, so WordPress cannot serve them | |
| 656 | + // from `alloptions` and each Settings->get() below was its own | |
| 657 | + // round-trip — 16 of them on every anonymous front-end request, on | |
| 658 | + // pages that use none of the values (#393). | |
| 659 | + $this->core_settings->prime($category_config['keys']); | |
| 660 | + | |
| 633 | 661 | $settings = []; |
| 634 | 662 | |
| 635 | 663 | foreach ($category_config['keys'] as $key) { |
| 636 | 664 | $settings[$key] = $this->core_settings->get($key); |
| @@ -786,8 +814,10 @@ | ||
| 786 | 814 | switch ($key) { |
| 787 | 815 | case 'openai_api_key': |
| 788 | 816 | case 'claude_api_key': |
| 789 | 817 | case 'openrouter_api_key': |
| 818 | + case 'openai_compatible_api_key': | |
| 819 | + case 'openai_compatible_model': | |
| 790 | 820 | if (!empty($value) && !is_string($value)) { |
| 791 | 821 | $validation['valid'] = false; |
| 792 | 822 | $validation['errors'][] = "{$key} must be a string"; |
| 793 | 823 | } |
| @@ -792,15 +822,29 @@ | ||
| 792 | 822 | $validation['errors'][] = "{$key} must be a string"; |
| 793 | 823 | } |
| 794 | 824 | break; |
| 795 | 825 | |
| 826 | + case 'openai_compatible_base_url': | |
| 827 | + // An unreachable or dangerous URL is refused with a reason | |
| 828 | + // rather than quietly stored (see Endpoint_URL_Validator). | |
| 829 | + if (!empty($value)) { | |
| 830 | + $validated = \ThinkRank\AI\Endpoint_URL_Validator::validate((string) $value); | |
| 831 | + if (is_wp_error($validated)) { | |
| 832 | + $validation['valid'] = false; | |
| 833 | + $validation['errors'][] = $validated->get_error_message(); | |
| 834 | + } | |
| 835 | + } | |
| 836 | + break; | |
| 837 | + | |
| 796 | 838 | case 'max_tokens': |
| 797 | 839 | case 'cache_duration': |
| 798 | 840 | case 'max_requests_per_minute': |
| 841 | + case 'ai_daily_request_limit': | |
| 799 | 842 | case 'seo_score_threshold': |
| 800 | 843 | case 'api_timeout': |
| 801 | 844 | case 'retry_attempts': |
| 802 | 845 | case 'data_retention_days': |
| 846 | + case 'openai_compatible_timeout': | |
| 803 | 847 | if (!is_numeric($value) || $value < 0) { |
| 804 | 848 | $validation['valid'] = false; |
| 805 | 849 | $validation['errors'][] = "{$key} must be a positive number"; |
| 806 | 850 | } |
| @@ -813,11 +857,15 @@ | ||
| 813 | 857 | } |
| 814 | 858 | break; |
| 815 | 859 | |
| 816 | 860 | case 'ai_provider': |
| 817 | - if (!in_array($value, ['openai', 'claude', 'gemini', 'openrouter'], true)) { | |
| 861 | + // '' is legal: it is Settings::AI_PROVIDER_NONE, the state a | |
| 862 | + // fresh install starts in and the one a user returns to by | |
| 863 | + // deselecting their provider (#572). | |
| 864 | + if (!in_array($value, \ThinkRank\Core\Settings::selectable_ai_providers(), true)) { | |
| 818 | 865 | $validation['valid'] = false; |
| 819 | - $validation['errors'][] = "ai_provider must be 'openai', 'claude', 'gemini', or 'openrouter'"; | |
| 866 | + $validation['errors'][] = "ai_provider must be empty (no provider) or one of: " | |
| 867 | + . implode(', ', \ThinkRank\Core\Settings::SUPPORTED_AI_PROVIDERS); | |
| 820 | 868 | } |
| 821 | 869 | break; |
| 822 | 870 | |
| 823 | 871 | case 'dashboard_widgets': |