| @@ -257,12 +257,8 @@ | ||
| 257 | 257 | 'seo_analytics_google_analytics_property_id', |
| 258 | 258 | 'ga_analytics_account_id', |
| 259 | 259 | 'ga_analytics_data_stream_id', |
| 260 | 260 | |
| 261 | - // GA4 Tracking Code Injection (Pro) | |
| 262 | - 'ga4_auto_inject', | |
| 263 | - 'ga4_measurement_id', | |
| 264 | - | |
| 265 | 261 | // Search Console configuration |
| 266 | 262 | 'search_console_property', |
| 267 | 263 | |
| 268 | 264 | // AI features |
| @@ -325,13 +321,17 @@ | ||
| 325 | 321 | * @param array $settings Settings to update |
| 326 | 322 | * @param string $category Settings category |
| 327 | 323 | * @param string $context_type Optional. Context type for SEO settings |
| 328 | 324 | * @param int|null $context_id Optional. Context ID for SEO settings |
| 329 | - * @return bool Success status | |
| 325 | + * @return bool|null True on success, false on failure, null when this store | |
| 326 | + * does not own the category (nothing was attempted). | |
| 330 | 327 | */ |
| 331 | - public function update_settings(array $settings, string $category, string $context_type = 'site', ?int $context_id = null): bool { | |
| 328 | + public function update_settings(array $settings, string $category, string $context_type = 'site', ?int $context_id = null): ?bool { | |
| 329 | + // Unknown here means "not this store's category", not "the write | |
| 330 | + // failed" — the caller may still have a dedicated manager that owns it | |
| 331 | + // (#371). Failing closed made those saves report 500 after committing. | |
| 332 | 332 | if (!isset($this->settings_categories[$category])) { |
| 333 | - return false; | |
| 333 | + return null; | |
| 334 | 334 | } |
| 335 | 335 | |
| 336 | 336 | $category_config = $this->settings_categories[$category]; |
| 337 | 337 | |
| @@ -450,8 +450,23 @@ | ||
| 450 | 450 | * @since 1.0.0 |
| 451 | 451 | * |
| 452 | 452 | * @return array Categories information |
| 453 | 453 | */ |
| 454 | + /** | |
| 455 | + * The setting keys a category defines. | |
| 456 | + * | |
| 457 | + * Exposed so callers can reject keys a category does not define instead of | |
| 458 | + * persisting whatever they are handed (#395). | |
| 459 | + * | |
| 460 | + * @since 2.0.1 | |
| 461 | + * | |
| 462 | + * @param string $category Category name. | |
| 463 | + * @return string[] Setting keys, or [] when the category is unknown here. | |
| 464 | + */ | |
| 465 | + public function get_category_keys(string $category): array { | |
| 466 | + return $this->settings_categories[$category]['keys'] ?? []; | |
| 467 | + } | |
| 468 | + | |
| 454 | 469 | public function get_categories(): array { |
| 455 | 470 | $categories = []; |
| 456 | 471 | |
| 457 | 472 | foreach ($this->settings_categories as $key => $config) { |
| @@ -625,8 +640,16 @@ | ||
| 625 | 640 | * @return array Core settings for category |
| 626 | 641 | */ |
| 627 | 642 | private function get_core_settings_by_category(string $category): array { |
| 628 | 643 | $category_config = $this->settings_categories[$category]; |
| 644 | + | |
| 645 | + // Prime the option cache in one query before the loop. Every | |
| 646 | + // thinkrank_* option is autoload=off, so WordPress cannot serve them | |
| 647 | + // from `alloptions` and each Settings->get() below was its own | |
| 648 | + // round-trip — 16 of them on every anonymous front-end request, on | |
| 649 | + // pages that use none of the values (#393). | |
| 650 | + $this->core_settings->prime($category_config['keys']); | |
| 651 | + | |
| 629 | 652 | $settings = []; |
| 630 | 653 | |
| 631 | 654 | foreach ($category_config['keys'] as $key) { |
| 632 | 655 | $settings[$key] = $this->core_settings->get($key); |
| @@ -710,11 +733,12 @@ | ||
| 710 | 733 | * @param array $settings Settings to update |
| 711 | 734 | * @param string $category Category name |
| 712 | 735 | * @param string $context_type Context type |
| 713 | 736 | * @param int|null $context_id Context ID |
| 714 | - * @return bool Success status | |
| 737 | + * @return bool|null True on success, false on failure, null when the SEO | |
| 738 | + * store does not own the category. | |
| 715 | 739 | */ |
| 716 | - private function update_seo_settings_by_category(array $settings, string $category, string $context_type, ?int $context_id): bool { | |
| 740 | + private function update_seo_settings_by_category(array $settings, string $category, string $context_type, ?int $context_id): ?bool { | |
| 717 | 741 | return $this->seo_settings->save_settings_by_category($context_type, $context_id, $settings, $category); |
| 718 | 742 | } |
| 719 | 743 | |
| 720 | 744 | /** |
| @@ -808,11 +832,15 @@ | ||
| 808 | 832 | } |
| 809 | 833 | break; |
| 810 | 834 | |
| 811 | 835 | case 'ai_provider': |
| 812 | - if (!in_array($value, ['openai', 'claude', 'gemini', 'openrouter'], true)) { | |
| 836 | + // '' is legal: it is Settings::AI_PROVIDER_NONE, the state a | |
| 837 | + // fresh install starts in and the one a user returns to by | |
| 838 | + // deselecting their provider (#572). | |
| 839 | + if (!in_array($value, \ThinkRank\Core\Settings::selectable_ai_providers(), true)) { | |
| 813 | 840 | $validation['valid'] = false; |
| 814 | - $validation['errors'][] = "ai_provider must be 'openai', 'claude', 'gemini', or 'openrouter'"; | |
| 841 | + $validation['errors'][] = "ai_provider must be empty (no provider) or one of: " | |
| 842 | + . implode(', ', \ThinkRank\Core\Settings::SUPPORTED_AI_PROVIDERS); | |
| 815 | 843 | } |
| 816 | 844 | break; |
| 817 | 845 | |
| 818 | 846 | case 'dashboard_widgets': |