PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.0
2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
← All changes | includes/core/class-settings-manager.php +64 -11 1.31.0 → 2.10.0 View file →
@@ -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
@@ -325,13 +330,17 @@
325 330 * @param array $settings Settings to update
326 331 * @param string $category Settings category
327 332 * @param string $context_type Optional. Context type for SEO settings
328 333 * @param int|null $context_id Optional. Context ID for SEO settings
329 - * @return bool Success status
334 + * @return bool|null True on success, false on failure, null when this store
335 + * does not own the category (nothing was attempted).
330 336 */
331 - public function update_settings(array $settings, string $category, string $context_type = 'site', ?int $context_id = null): bool {
337 + public function update_settings(array $settings, string $category, string $context_type = 'site', ?int $context_id = null): ?bool {
338 + // Unknown here means "not this store's category", not "the write
339 + // failed" — the caller may still have a dedicated manager that owns it
340 + // (#371). Failing closed made those saves report 500 after committing.
332 341 if (!isset($this->settings_categories[$category])) {
333 - return false;
342 + return null;
334 343 }
335 344
336 345 $category_config = $this->settings_categories[$category];
337 346
@@ -450,8 +459,23 @@
450 459 * @since 1.0.0
451 460 *
452 461 * @return array Categories information
453 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 +
454 478 public function get_categories(): array {
455 479 $categories = [];
456 480
457 481 foreach ($this->settings_categories as $key => $config) {
@@ -625,8 +649,16 @@
625 649 * @return array Core settings for category
626 650 */
627 651 private function get_core_settings_by_category(string $category): array {
628 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 +
629 661 $settings = [];
630 662
631 663 foreach ($category_config['keys'] as $key) {
632 664 $settings[$key] = $this->core_settings->get($key);
@@ -710,11 +742,12 @@
710 742 * @param array $settings Settings to update
711 743 * @param string $category Category name
712 744 * @param string $context_type Context type
713 745 * @param int|null $context_id Context ID
714 - * @return bool Success status
746 + * @return bool|null True on success, false on failure, null when the SEO
747 + * store does not own the category.
715 748 */
716 - private function update_seo_settings_by_category(array $settings, string $category, string $context_type, ?int $context_id): bool {
749 + private function update_seo_settings_by_category(array $settings, string $category, string $context_type, ?int $context_id): ?bool {
717 750 return $this->seo_settings->save_settings_by_category($context_type, $context_id, $settings, $category);
718 751 }
719 752
720 753 /**
@@ -781,8 +814,10 @@
781 814 switch ($key) {
782 815 case 'openai_api_key':
783 816 case 'claude_api_key':
784 817 case 'openrouter_api_key':
818 + case 'openai_compatible_api_key':
819 + case 'openai_compatible_model':
785 820 if (!empty($value) && !is_string($value)) {
786 821 $validation['valid'] = false;
787 822 $validation['errors'][] = "{$key} must be a string";
788 823 }
@@ -787,15 +822,29 @@
787 822 $validation['errors'][] = "{$key} must be a string";
788 823 }
789 824 break;
790 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 +
791 838 case 'max_tokens':
792 839 case 'cache_duration':
793 840 case 'max_requests_per_minute':
841 + case 'ai_daily_request_limit':
794 842 case 'seo_score_threshold':
795 843 case 'api_timeout':
796 844 case 'retry_attempts':
797 845 case 'data_retention_days':
846 + case 'openai_compatible_timeout':
798 847 if (!is_numeric($value) || $value < 0) {
799 848 $validation['valid'] = false;
800 849 $validation['errors'][] = "{$key} must be a positive number";
801 850 }
@@ -808,11 +857,15 @@
808 857 }
809 858 break;
810 859
811 860 case 'ai_provider':
812 - 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)) {
813 865 $validation['valid'] = false;
814 - $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);
815 868 }
816 869 break;
817 870
818 871 case 'dashboard_widgets':