PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.28.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.28.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 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / core / class-settings-manager.php

class-settings-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.28.0, at includes/core/class-settings-manager.php

818 lines 25.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Centralized Settings Manager Class
5 *
6 * Coordinates settings management across all ThinkRank components including
7 * core plugin settings, SEO-specific settings, and cross-manager coordination.
8 * Provides unified interface for the Settings Management API Endpoints with
9 * proper validation, import/export, and backup/restore functionality.
10 *
11 * @package ThinkRank
12 * @subpackage Core
13 * @since 1.0.0
14 */
15
16 declare(strict_types=1);
17
18 namespace ThinkRank\Core;
19
20 use ThinkRank\Core\Settings;
21 use ThinkRank\SEO\SEO_Settings_Manager;
22
23 // Prevent direct access
24 if (!defined('ABSPATH')) {
25 exit;
26 }
27
28 /**
29 * Centralized Settings Manager Class
30 *
31 * Provides unified settings management interface that coordinates between
32 * core plugin settings and SEO-specific settings. Handles cross-manager
33 * validation, import/export, backup/restore, and conflict resolution.
34 *
35 * @since 1.0.0
36 */
37 class Settings_Manager {
38
39 /**
40 * Core Settings instance
41 *
42 * @since 1.0.0
43 * @var Settings
44 */
45 private Settings $core_settings;
46
47 /**
48 * SEO Settings Manager instance
49 *
50 * @since 1.0.0
51 * @var SEO_Settings_Manager
52 */
53 private SEO_Settings_Manager $seo_settings;
54
55 /**
56 * Settings categories mapping
57 *
58 * @since 1.0.0
59 * @var array
60 */
61 private array $settings_categories = [
62 'core' => [
63 'name' => 'Core Plugin Settings',
64 'manager' => 'core',
65 'keys' => [
66 'ai_provider',
67 'openai_api_key',
68 'openai_model',
69 'claude_api_key',
70 'claude_model',
71 'gemini_api_key',
72 'gemini_model',
73 'openrouter_api_key',
74 'openrouter_model',
75 'max_tokens',
76 'temperature',
77 'cache_duration',
78 'max_requests_per_minute',
79 'enable_logging',
80 'debug_mode',
81 'api_timeout',
82 'retry_attempts',
83 'rate_limit_enabled',
84 'data_retention_days',
85 'anonymize_logs',
86 'share_usage_data',
87 'keep_data_on_uninstall'
88 ]
89 ],
90 'seo' => [
91 'name' => 'SEO Settings',
92 'manager' => 'seo',
93 'keys' => [
94 'auto_optimize',
95 'seo_score_threshold',
96 'enable_meta_generation',
97 'enable_schema_markup'
98 ]
99 ],
100 'ui' => [
101 'name' => 'User Interface Settings',
102 'manager' => 'core',
103 'keys' => [
104 'show_welcome_message',
105 'dashboard_widgets',
106 'editor_panel_position'
107 ]
108 ],
109 'basic_integrations' => [
110 'name' => 'Basic Integration Settings',
111 'manager' => 'core',
112 'keys' => [
113 'google_analytics_id',
114 'search_console_property'
115 ]
116 ],
117 'social_media' => [
118 'name' => 'Social Media & Open Graph',
119 'manager' => 'seo',
120 'keys' => [
121 'enabled',
122 'enable_open_graph',
123 'og_site_name',
124 'og_description',
125 'og_type',
126 'og_locale',
127 'default_og_image',
128 'og_image_width',
129 'og_image_height',
130 'enable_twitter_cards',
131 'twitter_username',
132 'twitter_card_type',
133 'default_twitter_image',
134 'facebook_app_id',
135 'facebook_admins',
136 'enable_linkedin',
137 'enable_pinterest',
138 'pinterest_site_verification',
139 'enable_instagram',
140 'instagram_verification',
141 'enable_tiktok',
142 'tiktok_verification',
143 'enable_youtube',
144 'youtube_channel_id',
145 'enable_whatsapp',
146 'whatsapp_business_id',
147 'auto_generate_descriptions',
148 'fallback_to_excerpt',
149 'strip_html_tags',
150 'max_description_length'
151 ]
152 ],
153 'sitemap' => [
154 'name' => 'XML Sitemap Management',
155 'manager' => 'seo',
156 'keys' => [
157 'enabled',
158 'include_posts',
159 'include_pages',
160 'include_categories',
161 'include_tags',
162 'auto_generate',
163 'ping_search_engines',
164 'last_generated',
165 'exclude_posts',
166 'exclude_terms',
167 'exclude_password_protected',
168 'exclude_private_posts',
169 'enable_styling',
170 'custom_url_pattern',
171 'links_per_sitemap',
172 'include_images',
173 'include_featured_images',
174 'use_sitemap_index'
175 ]
176 ],
177 'site_identity' => [
178 'name' => 'Site Identity & Global SEO',
179 'manager' => 'seo',
180 'keys' => []
181 ],
182 'content_analysis' => [
183 'name' => 'AI Content Analysis',
184 'manager' => 'seo',
185 'keys' => []
186 ],
187 'content_optimization' => [
188 'name' => 'Content Optimization',
189 'manager' => 'seo',
190 'keys' => []
191 ],
192 'performance_monitoring' => [
193 'name' => 'Performance Monitoring',
194 'manager' => 'seo',
195 'keys' => []
196 ],
197 'schema_management' => [
198 'name' => 'Schema Management',
199 'manager' => 'seo',
200 'keys' => []
201 ],
202 'integrations' => [
203 'name' => 'Integrations',
204 'manager' => 'core',
205 'keys' => [
206 // Google API Keys
207 'google_analytics_api_key',
208 'google_search_console_api_key',
209 'google_pagespeed_api_key',
210 // Google OAuth Tokens
211 'google_access_token',
212 'google_refresh_token',
213 'google_token_expires_in',
214 'google_token_created',
215 'google_account_connected',
216 // API Configuration
217 'api_timeout',
218 'enable_rate_limiting',
219 'cache_duration',
220 // Connection settings
221 'auto_test_connections',
222 'retry_failed_requests'
223 ]
224 ],
225 'seo_analytics' => [
226 'name' => 'SEO Analytics & Intelligence',
227 'manager' => 'core',
228 'keys' => [
229 // Core settings
230 'seo_analytics_enabled',
231 'seo_analytics_setup_completed',
232
233 // Google Analytics configuration. NOTE: the account/property/
234 // data-stream picker that reads AND writes these three keys is
235 // thinkrank-pro's GoogleAnalyticsSettings.js (via this plugin's
236 // settings-management endpoint) — a free-repo grep will find no
237 // consumer. ga_analytics_data_stream_id was once removed as a
238 // "dead key" on that basis, which silently broke the Pro
239 // picker's stream selection persisting across reloads.
240 'seo_analytics_google_analytics_property_id',
241 'ga_analytics_account_id',
242 'ga_analytics_data_stream_id',
243
244 // GA4 Tracking Code Injection (Pro)
245 'ga4_auto_inject',
246 'ga4_measurement_id',
247
248 // Search Console configuration
249 'search_console_property',
250
251 // AI features
252 'seo_analytics_enable_ai_insights',
253 'seo_analytics_enable_automated_alerts',
254 'seo_analytics_enable_predictive_analysis',
255
256 // Monitoring settings
257 'seo_analytics_monitoring_frequency',
258 'seo_analytics_alert_thresholds',
259 'seo_analytics_report_schedule',
260
261 // Data retention
262 'seo_analytics_data_retention_days',
263 'seo_analytics_cache_analytics_data'
264 ]
265 ],
266
267 ];
268
269 /**
270 * Constructor
271 *
272 * @since 1.0.0
273 */
274 public function __construct() {
275 $this->core_settings = Settings::instance();
276 $this->seo_settings = new SEO_Settings_Manager();
277 }
278
279 /**
280 * Get settings for a specific category
281 *
282 * @since 1.0.0
283 *
284 * @param string $category Settings category
285 * @param string $context_type Optional. Context type for SEO settings
286 * @param int|null $context_id Optional. Context ID for SEO settings
287 * @return array Settings array
288 */
289 public function get_settings(string $category, string $context_type = 'site', ?int $context_id = null): array {
290 if (!isset($this->settings_categories[$category])) {
291 return [];
292 }
293
294 $category_config = $this->settings_categories[$category];
295
296 if ($category_config['manager'] === 'core') {
297 return $this->get_core_settings_by_category($category);
298 } else {
299 return $this->get_seo_settings_by_category($category, $context_type, $context_id);
300 }
301 }
302
303 /**
304 * Update settings for a specific category
305 *
306 * @since 1.0.0
307 *
308 * @param array $settings Settings to update
309 * @param string $category Settings category
310 * @param string $context_type Optional. Context type for SEO settings
311 * @param int|null $context_id Optional. Context ID for SEO settings
312 * @return bool Success status
313 */
314 public function update_settings(array $settings, string $category, string $context_type = 'site', ?int $context_id = null): bool {
315 if (!isset($this->settings_categories[$category])) {
316 return false;
317 }
318
319 $category_config = $this->settings_categories[$category];
320
321 if ($category_config['manager'] === 'core') {
322 return $this->update_core_settings_by_category($settings, $category);
323 } else {
324 return $this->update_seo_settings_by_category($settings, $category, $context_type, $context_id);
325 }
326 }
327
328 /**
329 * Get all settings across categories
330 *
331 * @since 1.0.0
332 *
333 * @param array $categories Optional. Specific categories to retrieve
334 * @return array All settings organized by category
335 */
336 public function get_all_settings(array $categories = []): array {
337 $all_settings = [];
338 $target_categories = empty($categories) ? array_keys($this->settings_categories) : $categories;
339
340 foreach ($target_categories as $category) {
341 if (isset($this->settings_categories[$category])) {
342 $all_settings[$category] = $this->get_settings($category);
343 }
344 }
345
346 return $all_settings;
347 }
348
349 /**
350 * Update multiple categories of settings
351 *
352 * @since 1.0.0
353 *
354 * @param array $settings_by_category Settings organized by category
355 * @return array Update results for each category
356 */
357 public function update_multiple_settings(array $settings_by_category): array {
358 $results = [];
359
360 foreach ($settings_by_category as $category => $settings) {
361 try {
362 $success = $this->update_settings($settings, $category);
363 $results[$category] = [
364 'success' => $success,
365 'settings_count' => count($settings)
366 ];
367 } catch (\Exception $e) {
368 $results[$category] = [
369 'success' => false,
370 'error' => $e->getMessage()
371 ];
372 }
373 }
374
375 return $results;
376 }
377
378 /**
379 * Validate settings across categories
380 *
381 * @since 1.0.0
382 *
383 * @param array $settings_by_category Settings organized by category
384 * @return array Validation results for each category
385 */
386 public function validate_settings(array $settings_by_category): array {
387 $validation_results = [];
388
389 foreach ($settings_by_category as $category => $settings) {
390 if (!isset($this->settings_categories[$category])) {
391 $validation_results[$category] = [
392 'valid' => false,
393 'errors' => ['Invalid category'],
394 'warnings' => []
395 ];
396 continue;
397 }
398
399 $category_config = $this->settings_categories[$category];
400
401 if ($category_config['manager'] === 'core') {
402 $validation_results[$category] = $this->validate_core_settings($settings, $category);
403 } else {
404 $validation_results[$category] = $this->validate_seo_settings($settings, $category);
405 }
406 }
407
408 return $validation_results;
409 }
410
411 /**
412 * Get settings categories information
413 *
414 * @since 1.0.0
415 *
416 * @return array Categories information
417 */
418 public function get_categories(): array {
419 $categories = [];
420
421 foreach ($this->settings_categories as $key => $config) {
422 $categories[$key] = [
423 'name' => $config['name'],
424 'manager' => $config['manager'],
425 'key_count' => count($config['keys'])
426 ];
427 }
428
429 return $categories;
430 }
431
432 /**
433 * Export settings
434 *
435 * @since 1.0.0
436 *
437 * @param array $categories Optional. Categories to export
438 * @return array Export data with metadata
439 */
440 public function export_settings(array $categories = []): array {
441 $export_data = [
442 'metadata' => [
443 'export_timestamp' => current_time('mysql'),
444 'plugin_version' => defined('THINKRANK_VERSION') ? THINKRANK_VERSION : '1.0.0',
445 'wordpress_version' => get_bloginfo('version'),
446 'site_url' => home_url(),
447 'exported_categories' => empty($categories) ? array_keys($this->settings_categories) : $categories
448 ],
449 'settings' => $this->get_all_settings($categories)
450 ];
451
452 return $export_data;
453 }
454
455 /**
456 * Import settings
457 *
458 * @since 1.0.0
459 *
460 * @param array $import_data Import data with settings
461 * @param bool $validate_before_import Whether to validate before importing
462 * @return array Import results
463 */
464 public function import_settings(array $import_data, bool $validate_before_import = true): array {
465 $settings = $import_data['settings'] ?? $import_data;
466 $import_results = [];
467
468 // Validate if requested
469 if ($validate_before_import) {
470 $validation_results = $this->validate_settings($settings);
471
472 foreach ($validation_results as $category => $validation) {
473 if (!$validation['valid']) {
474 $import_results[$category] = [
475 'success' => false,
476 'error' => 'Validation failed',
477 'validation_errors' => $validation['errors']
478 ];
479 continue;
480 }
481 }
482 }
483
484 // Import settings for each category
485 foreach ($settings as $category => $category_settings) {
486 if (isset($import_results[$category])) {
487 continue; // Skip if validation failed
488 }
489
490 try {
491 $success = $this->update_settings($category_settings, $category);
492 $import_results[$category] = [
493 'success' => $success,
494 'settings_count' => count($category_settings)
495 ];
496 } catch (\Exception $e) {
497 $import_results[$category] = [
498 'success' => false,
499 'error' => $e->getMessage()
500 ];
501 }
502 }
503
504 return $import_results;
505 }
506
507 /**
508 * Reset settings to defaults
509 *
510 * @since 1.0.0
511 *
512 * @param array $categories Optional. Categories to reset
513 * @return array Reset results
514 */
515 public function reset_settings(array $categories = []): array {
516 $target_categories = empty($categories) ? array_keys($this->settings_categories) : $categories;
517 $reset_results = [];
518
519 foreach ($target_categories as $category) {
520 if (!isset($this->settings_categories[$category])) {
521 continue;
522 }
523
524 try {
525 $category_config = $this->settings_categories[$category];
526
527 if ($category_config['manager'] === 'core') {
528 $success = $this->reset_core_settings($category);
529 } else {
530 $success = $this->reset_seo_settings($category);
531 }
532
533 $reset_results[$category] = [
534 'success' => $success,
535 'reset_to_defaults' => true
536 ];
537 } catch (\Exception $e) {
538 $reset_results[$category] = [
539 'success' => false,
540 'error' => $e->getMessage()
541 ];
542 }
543 }
544
545 return $reset_results;
546 }
547
548 /**
549 * Get settings statistics
550 *
551 * @since 1.0.0
552 *
553 * @return array Settings statistics
554 */
555 public function get_settings_statistics(): array {
556 $stats = [
557 'total_categories' => count($this->settings_categories),
558 'core_categories' => 0,
559 'seo_categories' => 0,
560 'total_settings' => 0,
561 'last_updated' => get_option('thinkrank_settings_last_updated'),
562 'version' => get_option('thinkrank_settings_version', '1.0.0')
563 ];
564
565 foreach ($this->settings_categories as $category => $config) {
566 if ($config['manager'] === 'core') {
567 $stats['core_categories']++;
568 } else {
569 $stats['seo_categories']++;
570 }
571
572 $category_settings = $this->get_settings($category);
573 $stats['total_settings'] += count($category_settings);
574 }
575
576 return $stats;
577 }
578
579 /**
580 * Helper methods for core settings management
581 */
582
583 /**
584 * Get core settings by category
585 *
586 * @since 1.0.0
587 *
588 * @param string $category Category name
589 * @return array Core settings for category
590 */
591 private function get_core_settings_by_category(string $category): array {
592 $category_config = $this->settings_categories[$category];
593 $settings = [];
594
595 foreach ($category_config['keys'] as $key) {
596 $settings[$key] = $this->core_settings->get($key);
597 }
598
599 return $settings;
600 }
601
602 /**
603 * Update core settings by category
604 *
605 * @since 1.0.0
606 *
607 * @param array $settings Settings to update
608 * @param string $category Category name
609 * @return bool Success status
610 */
611 private function update_core_settings_by_category(array $settings, string $category): bool {
612 $category_config = $this->settings_categories[$category];
613 $success_count = 0;
614 $total_count = 0;
615
616 foreach ($settings as $key => $value) {
617 if (in_array($key, $category_config['keys'], true)) {
618 $total_count++;
619
620 if ($this->core_settings->set($key, $value)) {
621 $success_count++;
622 }
623 }
624 }
625
626 // Consider successful if at least 70% of settings were saved
627 $success_rate = $total_count > 0 ? ($success_count / $total_count) : 0;
628 $success = $success_rate >= 0.7;
629
630 if ($success) {
631 update_option('thinkrank_settings_last_updated', current_time('mysql'));
632 }
633
634 return $success;
635 }
636
637 /**
638 * Get SEO settings by category
639 *
640 * @since 1.0.0
641 *
642 * @param string $category Category name
643 * @param string $context_type Context type
644 * @param int|null $context_id Context ID
645 * @return array SEO settings for category
646 */
647 private function get_seo_settings_by_category(string $category, string $context_type, ?int $context_id): array {
648 // For SEO categories, delegate to SEO Settings Manager
649 return $this->seo_settings->get_settings_by_category($context_type, $context_id, $category);
650 }
651
652 /**
653 * Update SEO settings by category
654 *
655 * @since 1.0.0
656 *
657 * @param array $settings Settings to update
658 * @param string $category Category name
659 * @param string $context_type Context type
660 * @param int|null $context_id Context ID
661 * @return bool Success status
662 */
663 private function update_seo_settings_by_category(array $settings, string $category, string $context_type, ?int $context_id): bool {
664 return $this->seo_settings->save_settings_by_category($context_type, $context_id, $settings, $category);
665 }
666
667 /**
668 * Validate core settings
669 *
670 * @since 1.0.0
671 *
672 * @param array $settings Settings to validate
673 * @param string $category Category name
674 * @return array Validation results
675 */
676 private function validate_core_settings(array $settings, string $category): array {
677 $validation = [
678 'valid' => true,
679 'errors' => [],
680 'warnings' => []
681 ];
682
683 $category_config = $this->settings_categories[$category];
684
685 foreach ($settings as $key => $value) {
686 if (!in_array($key, $category_config['keys'], true)) {
687 $validation['warnings'][] = "Unknown setting key: {$key}";
688 continue;
689 }
690
691 // Validate specific core settings
692 $field_validation = $this->validate_core_setting($key, $value);
693 if (!$field_validation['valid']) {
694 $validation['valid'] = false;
695 $validation['errors'] = array_merge($validation['errors'], $field_validation['errors']);
696 }
697 }
698
699 return $validation;
700 }
701
702 /**
703 * Validate SEO settings
704 *
705 * @since 1.0.0
706 *
707 * @param array $settings Settings to validate
708 * @param string $category Category name
709 * @return array Validation results
710 */
711 private function validate_seo_settings(array $settings, string $category): array {
712 // Delegate to SEO Settings Manager validation
713 return $this->seo_settings->validate_settings($settings);
714 }
715
716 /**
717 * Validate individual core setting
718 *
719 * @since 1.0.0
720 *
721 * @param string $key Setting key
722 * @param mixed $value Setting value
723 * @return array Validation result
724 */
725 private function validate_core_setting(string $key, $value): array {
726 $validation = ['valid' => true, 'errors' => []];
727
728 switch ($key) {
729 case 'openai_api_key':
730 case 'claude_api_key':
731 case 'openrouter_api_key':
732 if (!empty($value) && !is_string($value)) {
733 $validation['valid'] = false;
734 $validation['errors'][] = "{$key} must be a string";
735 }
736 break;
737
738 case 'max_tokens':
739 case 'cache_duration':
740 case 'max_requests_per_minute':
741 case 'seo_score_threshold':
742 case 'api_timeout':
743 case 'retry_attempts':
744 case 'data_retention_days':
745 if (!is_numeric($value) || $value < 0) {
746 $validation['valid'] = false;
747 $validation['errors'][] = "{$key} must be a positive number";
748 }
749 break;
750
751 case 'temperature':
752 if (!is_numeric($value) || $value < 0 || $value > 2) {
753 $validation['valid'] = false;
754 $validation['errors'][] = "temperature must be between 0 and 2";
755 }
756 break;
757
758 case 'ai_provider':
759 if (!in_array($value, ['openai', 'claude', 'gemini', 'openrouter'], true)) {
760 $validation['valid'] = false;
761 $validation['errors'][] = "ai_provider must be 'openai', 'claude', 'gemini', or 'openrouter'";
762 }
763 break;
764
765 case 'dashboard_widgets':
766 if (!is_array($value)) {
767 $validation['valid'] = false;
768 $validation['errors'][] = "dashboard_widgets must be an array";
769 }
770 break;
771 }
772
773 return $validation;
774 }
775
776 /**
777 * Reset core settings for category
778 *
779 * @since 1.0.0
780 *
781 * @param string $category Category name
782 * @return bool Success status
783 */
784 private function reset_core_settings(string $category): bool {
785 $category_config = $this->settings_categories[$category];
786 $success = true;
787
788 foreach ($category_config['keys'] as $key) {
789 if (!$this->core_settings->delete($key)) {
790 $success = false;
791 }
792 }
793
794 return $success;
795 }
796
797 /**
798 * Reset SEO settings for category
799 *
800 * @since 1.0.0
801 *
802 * @param string $category Category name
803 * @return bool Success status
804 */
805 private function reset_seo_settings(string $category): bool {
806 try {
807 // Map the settings category to the SEO context type
808 return $this->seo_settings->reset_to_defaults('site');
809 } catch (\Exception $e) {
810 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
811 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging only when WP_DEBUG is enabled.
812 error_log('ThinkRank: Failed to reset SEO settings for category "' . $category . '": ' . $e->getMessage());
813 }
814 return false;
815 }
816 }
817 }
818