PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.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 2.0.0, at includes/core/class-settings-manager.php

876 lines 28.7 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 * Keys the most recent core-category save could not persist.
57 *
58 * A batch save is all-or-nothing in its reporting but not in its writes, so
59 * a caller that gets false needs to know *which* settings did not make it —
60 * a bare boolean leaves the UI unable to say anything useful (#300).
61 *
62 * @since 1.30.0
63 * @var string[]
64 */
65 private array $last_failed_keys = [];
66
67 /**
68 * Settings categories mapping
69 *
70 * @since 1.0.0
71 * @var array
72 */
73 private array $settings_categories = [
74 'core' => [
75 'name' => 'Core Plugin Settings',
76 'manager' => 'core',
77 'keys' => [
78 'ai_provider',
79 'openai_api_key',
80 'openai_model',
81 'claude_api_key',
82 'claude_model',
83 'gemini_api_key',
84 'gemini_model',
85 'openrouter_api_key',
86 'openrouter_model',
87 'max_tokens',
88 'temperature',
89 'cache_duration',
90 'max_requests_per_minute',
91 'enable_logging',
92 'debug_mode',
93 'api_timeout',
94 'retry_attempts',
95 // 'rate_limit_enabled' used to be listed here, but it has no
96 // entry in Settings::defaults, so Settings::set() rejected it on
97 // every save and no code ever read it. Under the old 70%
98 // threshold that silent rejection was reported as success;
99 // all-or-nothing reporting would now fail every core save that
100 // carried it, so the dead key goes rather than the save (#300).
101 'data_retention_days',
102 'anonymize_logs',
103 'share_usage_data',
104 'keep_data_on_uninstall'
105 ]
106 ],
107 'seo' => [
108 'name' => 'SEO Settings',
109 'manager' => 'seo',
110 'keys' => [
111 'auto_optimize',
112 'seo_score_threshold',
113 'enable_meta_generation',
114 'enable_schema_markup'
115 ]
116 ],
117 'ui' => [
118 'name' => 'User Interface Settings',
119 'manager' => 'core',
120 'keys' => [
121 'show_welcome_message',
122 'dashboard_widgets',
123 'editor_panel_position'
124 ]
125 ],
126 'basic_integrations' => [
127 'name' => 'Basic Integration Settings',
128 'manager' => 'core',
129 'keys' => [
130 'google_analytics_id',
131 'search_console_property'
132 ]
133 ],
134 'social_media' => [
135 'name' => 'Social Media & Open Graph',
136 'manager' => 'seo',
137 'keys' => [
138 'enabled',
139 'enable_open_graph',
140 'og_site_name',
141 'og_description',
142 'og_type',
143 'og_locale',
144 'default_og_image',
145 'og_image_width',
146 'og_image_height',
147 'enable_twitter_cards',
148 'twitter_username',
149 'twitter_card_type',
150 'default_twitter_image',
151 'facebook_app_id',
152 'facebook_admins',
153 'enable_linkedin',
154 'enable_pinterest',
155 'pinterest_site_verification',
156 'enable_instagram',
157 'instagram_verification',
158 'enable_tiktok',
159 'tiktok_verification',
160 'enable_youtube',
161 'youtube_channel_id',
162 'enable_whatsapp',
163 'whatsapp_business_id',
164 'auto_generate_descriptions',
165 'fallback_to_excerpt',
166 'strip_html_tags',
167 'max_description_length'
168 ]
169 ],
170 'sitemap' => [
171 'name' => 'XML Sitemap Management',
172 'manager' => 'seo',
173 'keys' => [
174 'enabled',
175 'include_posts',
176 'include_pages',
177 'include_categories',
178 'include_tags',
179 'auto_generate',
180 'ping_search_engines',
181 'last_generated',
182 'exclude_posts',
183 'exclude_terms',
184 'exclude_password_protected',
185 'exclude_private_posts',
186 'enable_styling',
187 'custom_url_pattern',
188 'links_per_sitemap',
189 'include_images',
190 'include_featured_images',
191 'use_sitemap_index'
192 ]
193 ],
194 'site_identity' => [
195 'name' => 'Site Identity & Global SEO',
196 'manager' => 'seo',
197 'keys' => []
198 ],
199 'content_analysis' => [
200 'name' => 'AI Content Analysis',
201 'manager' => 'seo',
202 'keys' => []
203 ],
204 'content_optimization' => [
205 'name' => 'Content Optimization',
206 'manager' => 'seo',
207 'keys' => []
208 ],
209 'performance_monitoring' => [
210 'name' => 'Performance Monitoring',
211 'manager' => 'seo',
212 'keys' => []
213 ],
214 'schema_management' => [
215 'name' => 'Schema Management',
216 'manager' => 'seo',
217 'keys' => []
218 ],
219 'integrations' => [
220 'name' => 'Integrations',
221 'manager' => 'core',
222 'keys' => [
223 // Google API Keys
224 'google_analytics_api_key',
225 'google_search_console_api_key',
226 'google_pagespeed_api_key',
227 // Google OAuth Tokens
228 'google_access_token',
229 'google_refresh_token',
230 'google_token_expires_in',
231 'google_token_created',
232 'google_account_connected',
233 // API Configuration
234 'api_timeout',
235 'enable_rate_limiting',
236 'cache_duration',
237 // Connection settings
238 'auto_test_connections',
239 'retry_failed_requests'
240 ]
241 ],
242 'seo_analytics' => [
243 'name' => 'SEO Analytics & Intelligence',
244 'manager' => 'core',
245 'keys' => [
246 // Core settings
247 'seo_analytics_enabled',
248 'seo_analytics_setup_completed',
249
250 // Google Analytics configuration. NOTE: the account/property/
251 // data-stream picker that reads AND writes these three keys is
252 // thinkrank-pro's GoogleAnalyticsSettings.js (via this plugin's
253 // settings-management endpoint) — a free-repo grep will find no
254 // consumer. ga_analytics_data_stream_id was once removed as a
255 // "dead key" on that basis, which silently broke the Pro
256 // picker's stream selection persisting across reloads.
257 'seo_analytics_google_analytics_property_id',
258 'ga_analytics_account_id',
259 'ga_analytics_data_stream_id',
260
261 // GA4 Tracking Code Injection (Pro)
262 'ga4_auto_inject',
263 'ga4_measurement_id',
264
265 // Search Console configuration
266 'search_console_property',
267
268 // AI features
269 'seo_analytics_enable_ai_insights',
270 'seo_analytics_enable_automated_alerts',
271 'seo_analytics_enable_predictive_analysis',
272
273 // Monitoring settings
274 'seo_analytics_monitoring_frequency',
275 'seo_analytics_alert_thresholds',
276 'seo_analytics_report_schedule',
277
278 // Data retention
279 'seo_analytics_data_retention_days',
280 'seo_analytics_cache_analytics_data'
281 ]
282 ],
283
284 ];
285
286 /**
287 * Constructor
288 *
289 * @since 1.0.0
290 */
291 public function __construct() {
292 $this->core_settings = Settings::instance();
293 $this->seo_settings = new SEO_Settings_Manager();
294 }
295
296 /**
297 * Get settings for a specific category
298 *
299 * @since 1.0.0
300 *
301 * @param string $category Settings category
302 * @param string $context_type Optional. Context type for SEO settings
303 * @param int|null $context_id Optional. Context ID for SEO settings
304 * @return array Settings array
305 */
306 public function get_settings(string $category, string $context_type = 'site', ?int $context_id = null): array {
307 if (!isset($this->settings_categories[$category])) {
308 return [];
309 }
310
311 $category_config = $this->settings_categories[$category];
312
313 if ($category_config['manager'] === 'core') {
314 return $this->get_core_settings_by_category($category);
315 } else {
316 return $this->get_seo_settings_by_category($category, $context_type, $context_id);
317 }
318 }
319
320 /**
321 * Update settings for a specific category
322 *
323 * @since 1.0.0
324 *
325 * @param array $settings Settings to update
326 * @param string $category Settings category
327 * @param string $context_type Optional. Context type for SEO settings
328 * @param int|null $context_id Optional. Context ID for SEO settings
329 * @return bool|null True on success, false on failure, null when this store
330 * does not own the category (nothing was attempted).
331 */
332 public function update_settings(array $settings, string $category, string $context_type = 'site', ?int $context_id = null): ?bool {
333 // Unknown here means "not this store's category", not "the write
334 // failed" — the caller may still have a dedicated manager that owns it
335 // (#371). Failing closed made those saves report 500 after committing.
336 if (!isset($this->settings_categories[$category])) {
337 return null;
338 }
339
340 $category_config = $this->settings_categories[$category];
341
342 // Reset here, not only in the core path: a SEO-category save must not
343 // leave a previous core save's failed keys readable.
344 $this->last_failed_keys = [];
345
346 if ($category_config['manager'] === 'core') {
347 return $this->update_core_settings_by_category($settings, $category);
348 } else {
349 return $this->update_seo_settings_by_category($settings, $category, $context_type, $context_id);
350 }
351 }
352
353 /**
354 * Keys the most recent update_settings() call could not persist.
355 *
356 * Empty on success, and reset at the start of every update_settings()
357 * call. SEO categories persist through their own manager and do not
358 * report per key, so this stays empty for them.
359 *
360 * @since 1.30.0
361 *
362 * @return string[] Setting keys that failed to save.
363 */
364 public function get_last_failed_keys(): array {
365 return $this->last_failed_keys;
366 }
367
368 /**
369 * Get all settings across categories
370 *
371 * @since 1.0.0
372 *
373 * @param array $categories Optional. Specific categories to retrieve
374 * @return array All settings organized by category
375 */
376 public function get_all_settings(array $categories = []): array {
377 $all_settings = [];
378 $target_categories = empty($categories) ? array_keys($this->settings_categories) : $categories;
379
380 foreach ($target_categories as $category) {
381 if (isset($this->settings_categories[$category])) {
382 $all_settings[$category] = $this->get_settings($category);
383 }
384 }
385
386 return $all_settings;
387 }
388
389 /**
390 * Update multiple categories of settings
391 *
392 * @since 1.0.0
393 *
394 * @param array $settings_by_category Settings organized by category
395 * @return array Update results for each category
396 */
397 public function update_multiple_settings(array $settings_by_category): array {
398 $results = [];
399
400 foreach ($settings_by_category as $category => $settings) {
401 try {
402 $success = $this->update_settings($settings, $category);
403 $results[$category] = [
404 'success' => $success,
405 'settings_count' => count($settings)
406 ];
407 } catch (\Exception $e) {
408 $results[$category] = [
409 'success' => false,
410 'error' => $e->getMessage()
411 ];
412 }
413 }
414
415 return $results;
416 }
417
418 /**
419 * Validate settings across categories
420 *
421 * @since 1.0.0
422 *
423 * @param array $settings_by_category Settings organized by category
424 * @return array Validation results for each category
425 */
426 public function validate_settings(array $settings_by_category): array {
427 $validation_results = [];
428
429 foreach ($settings_by_category as $category => $settings) {
430 if (!isset($this->settings_categories[$category])) {
431 $validation_results[$category] = [
432 'valid' => false,
433 'errors' => ['Invalid category'],
434 'warnings' => []
435 ];
436 continue;
437 }
438
439 $category_config = $this->settings_categories[$category];
440
441 if ($category_config['manager'] === 'core') {
442 $validation_results[$category] = $this->validate_core_settings($settings, $category);
443 } else {
444 $validation_results[$category] = $this->validate_seo_settings($settings, $category);
445 }
446 }
447
448 return $validation_results;
449 }
450
451 /**
452 * Get settings categories information
453 *
454 * @since 1.0.0
455 *
456 * @return array Categories information
457 */
458 public function get_categories(): array {
459 $categories = [];
460
461 foreach ($this->settings_categories as $key => $config) {
462 $categories[$key] = [
463 'name' => $config['name'],
464 'manager' => $config['manager'],
465 'key_count' => count($config['keys'])
466 ];
467 }
468
469 return $categories;
470 }
471
472 /**
473 * Export settings
474 *
475 * @since 1.0.0
476 *
477 * @param array $categories Optional. Categories to export
478 * @return array Export data with metadata
479 */
480 public function export_settings(array $categories = []): array {
481 $export_data = [
482 'metadata' => [
483 'export_timestamp' => current_time('mysql'),
484 'plugin_version' => defined('THINKRANK_VERSION') ? THINKRANK_VERSION : '1.0.0',
485 'wordpress_version' => get_bloginfo('version'),
486 'site_url' => home_url(),
487 'exported_categories' => empty($categories) ? array_keys($this->settings_categories) : $categories
488 ],
489 'settings' => $this->get_all_settings($categories)
490 ];
491
492 return $export_data;
493 }
494
495 /**
496 * Import settings
497 *
498 * @since 1.0.0
499 *
500 * @param array $import_data Import data with settings
501 * @param bool $validate_before_import Whether to validate before importing
502 * @return array Import results
503 */
504 public function import_settings(array $import_data, bool $validate_before_import = true): array {
505 $settings = $import_data['settings'] ?? $import_data;
506 $import_results = [];
507
508 // Validate if requested
509 if ($validate_before_import) {
510 $validation_results = $this->validate_settings($settings);
511
512 foreach ($validation_results as $category => $validation) {
513 if (!$validation['valid']) {
514 $import_results[$category] = [
515 'success' => false,
516 'error' => 'Validation failed',
517 'validation_errors' => $validation['errors']
518 ];
519 continue;
520 }
521 }
522 }
523
524 // Import settings for each category
525 foreach ($settings as $category => $category_settings) {
526 if (isset($import_results[$category])) {
527 continue; // Skip if validation failed
528 }
529
530 try {
531 $success = $this->update_settings($category_settings, $category);
532 $import_results[$category] = [
533 'success' => $success,
534 'settings_count' => count($category_settings)
535 ];
536 } catch (\Exception $e) {
537 $import_results[$category] = [
538 'success' => false,
539 'error' => $e->getMessage()
540 ];
541 }
542 }
543
544 return $import_results;
545 }
546
547 /**
548 * Reset settings to defaults
549 *
550 * @since 1.0.0
551 *
552 * @param array $categories Optional. Categories to reset
553 * @return array Reset results
554 */
555 public function reset_settings(array $categories = []): array {
556 $target_categories = empty($categories) ? array_keys($this->settings_categories) : $categories;
557 $reset_results = [];
558
559 foreach ($target_categories as $category) {
560 if (!isset($this->settings_categories[$category])) {
561 continue;
562 }
563
564 try {
565 $category_config = $this->settings_categories[$category];
566
567 if ($category_config['manager'] === 'core') {
568 $success = $this->reset_core_settings($category);
569 } else {
570 $success = $this->reset_seo_settings($category);
571 }
572
573 $reset_results[$category] = [
574 'success' => $success,
575 'reset_to_defaults' => true
576 ];
577 } catch (\Exception $e) {
578 $reset_results[$category] = [
579 'success' => false,
580 'error' => $e->getMessage()
581 ];
582 }
583 }
584
585 return $reset_results;
586 }
587
588 /**
589 * Get settings statistics
590 *
591 * @since 1.0.0
592 *
593 * @return array Settings statistics
594 */
595 public function get_settings_statistics(): array {
596 $stats = [
597 'total_categories' => count($this->settings_categories),
598 'core_categories' => 0,
599 'seo_categories' => 0,
600 'total_settings' => 0,
601 'last_updated' => get_option('thinkrank_settings_last_updated'),
602 'version' => get_option('thinkrank_settings_version', '1.0.0')
603 ];
604
605 foreach ($this->settings_categories as $category => $config) {
606 if ($config['manager'] === 'core') {
607 $stats['core_categories']++;
608 } else {
609 $stats['seo_categories']++;
610 }
611
612 $category_settings = $this->get_settings($category);
613 $stats['total_settings'] += count($category_settings);
614 }
615
616 return $stats;
617 }
618
619 /**
620 * Helper methods for core settings management
621 */
622
623 /**
624 * Get core settings by category
625 *
626 * @since 1.0.0
627 *
628 * @param string $category Category name
629 * @return array Core settings for category
630 */
631 private function get_core_settings_by_category(string $category): array {
632 $category_config = $this->settings_categories[$category];
633 $settings = [];
634
635 foreach ($category_config['keys'] as $key) {
636 $settings[$key] = $this->core_settings->get($key);
637 }
638
639 return $settings;
640 }
641
642 /**
643 * Update core settings by category
644 *
645 * @since 1.0.0
646 *
647 * @param array $settings Settings to update
648 * @param string $category Category name
649 * @return bool Success status
650 */
651 private function update_core_settings_by_category(array $settings, string $category): bool {
652 $category_config = $this->settings_categories[$category];
653 $total_count = 0;
654
655 $this->last_failed_keys = [];
656
657 // Sanitize per field before persisting. This is unconditional: callers
658 // (including the REST write routes, where the client can ask to skip
659 // validation) must not be able to reach Settings::set with unsanitized
660 // values — Settings::set only key-allowlists, it does not sanitize.
661 $settings = $this->core_settings->sanitize_settings($settings);
662
663 foreach ($settings as $key => $value) {
664 if (in_array($key, $category_config['keys'], true)) {
665 $total_count++;
666
667 if (!$this->core_settings->set($key, $value)) {
668 $this->last_failed_keys[] = $key;
669
670 // Name the key in the log: the UI can only ever show one
671 // message for the batch, so without this a single dropped
672 // setting is indistinguishable from a healthy save.
673 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- deliberate diagnostic, see above.
674 error_log(sprintf('ThinkRank [%s]: settings save failed — key \'%s\' was not stored', $category, $key));
675 }
676 }
677 }
678
679 // Every requested key must persist. A partial save used to pass on a 70%
680 // threshold, so a batch could silently drop up to a third of the user's
681 // settings while the UI reported success and the values were simply gone
682 // (#300). Note the write is not transactional: the keys that did save
683 // stay saved, which is why the failed keys are reported rather than just
684 // a bare false.
685 $success = $total_count > 0 && empty($this->last_failed_keys);
686
687 if ($success) {
688 update_option('thinkrank_settings_last_updated', current_time('mysql'));
689 }
690
691 return $success;
692 }
693
694 /**
695 * Get SEO settings by category
696 *
697 * @since 1.0.0
698 *
699 * @param string $category Category name
700 * @param string $context_type Context type
701 * @param int|null $context_id Context ID
702 * @return array SEO settings for category
703 */
704 private function get_seo_settings_by_category(string $category, string $context_type, ?int $context_id): array {
705 // For SEO categories, delegate to SEO Settings Manager
706 return $this->seo_settings->get_settings_by_category($context_type, $context_id, $category);
707 }
708
709 /**
710 * Update SEO settings by category
711 *
712 * @since 1.0.0
713 *
714 * @param array $settings Settings to update
715 * @param string $category Category name
716 * @param string $context_type Context type
717 * @param int|null $context_id Context ID
718 * @return bool|null True on success, false on failure, null when the SEO
719 * store does not own the category.
720 */
721 private function update_seo_settings_by_category(array $settings, string $category, string $context_type, ?int $context_id): ?bool {
722 return $this->seo_settings->save_settings_by_category($context_type, $context_id, $settings, $category);
723 }
724
725 /**
726 * Validate core settings
727 *
728 * @since 1.0.0
729 *
730 * @param array $settings Settings to validate
731 * @param string $category Category name
732 * @return array Validation results
733 */
734 private function validate_core_settings(array $settings, string $category): array {
735 $validation = [
736 'valid' => true,
737 'errors' => [],
738 'warnings' => []
739 ];
740
741 $category_config = $this->settings_categories[$category];
742
743 foreach ($settings as $key => $value) {
744 if (!in_array($key, $category_config['keys'], true)) {
745 $validation['warnings'][] = "Unknown setting key: {$key}";
746 continue;
747 }
748
749 // Validate specific core settings
750 $field_validation = $this->validate_core_setting($key, $value);
751 if (!$field_validation['valid']) {
752 $validation['valid'] = false;
753 $validation['errors'] = array_merge($validation['errors'], $field_validation['errors']);
754 }
755 }
756
757 return $validation;
758 }
759
760 /**
761 * Validate SEO settings
762 *
763 * @since 1.0.0
764 *
765 * @param array $settings Settings to validate
766 * @param string $category Category name
767 * @return array Validation results
768 */
769 private function validate_seo_settings(array $settings, string $category): array {
770 // Delegate to SEO Settings Manager validation
771 return $this->seo_settings->validate_settings($settings);
772 }
773
774 /**
775 * Validate individual core setting
776 *
777 * @since 1.0.0
778 *
779 * @param string $key Setting key
780 * @param mixed $value Setting value
781 * @return array Validation result
782 */
783 private function validate_core_setting(string $key, $value): array {
784 $validation = ['valid' => true, 'errors' => []];
785
786 switch ($key) {
787 case 'openai_api_key':
788 case 'claude_api_key':
789 case 'openrouter_api_key':
790 if (!empty($value) && !is_string($value)) {
791 $validation['valid'] = false;
792 $validation['errors'][] = "{$key} must be a string";
793 }
794 break;
795
796 case 'max_tokens':
797 case 'cache_duration':
798 case 'max_requests_per_minute':
799 case 'seo_score_threshold':
800 case 'api_timeout':
801 case 'retry_attempts':
802 case 'data_retention_days':
803 if (!is_numeric($value) || $value < 0) {
804 $validation['valid'] = false;
805 $validation['errors'][] = "{$key} must be a positive number";
806 }
807 break;
808
809 case 'temperature':
810 if (!is_numeric($value) || $value < 0 || $value > 2) {
811 $validation['valid'] = false;
812 $validation['errors'][] = "temperature must be between 0 and 2";
813 }
814 break;
815
816 case 'ai_provider':
817 if (!in_array($value, ['openai', 'claude', 'gemini', 'openrouter'], true)) {
818 $validation['valid'] = false;
819 $validation['errors'][] = "ai_provider must be 'openai', 'claude', 'gemini', or 'openrouter'";
820 }
821 break;
822
823 case 'dashboard_widgets':
824 if (!is_array($value)) {
825 $validation['valid'] = false;
826 $validation['errors'][] = "dashboard_widgets must be an array";
827 }
828 break;
829 }
830
831 return $validation;
832 }
833
834 /**
835 * Reset core settings for category
836 *
837 * @since 1.0.0
838 *
839 * @param string $category Category name
840 * @return bool Success status
841 */
842 private function reset_core_settings(string $category): bool {
843 $category_config = $this->settings_categories[$category];
844 $success = true;
845
846 foreach ($category_config['keys'] as $key) {
847 if (!$this->core_settings->delete($key)) {
848 $success = false;
849 }
850 }
851
852 return $success;
853 }
854
855 /**
856 * Reset SEO settings for category
857 *
858 * @since 1.0.0
859 *
860 * @param string $category Category name
861 * @return bool Success status
862 */
863 private function reset_seo_settings(string $category): bool {
864 try {
865 // Map the settings category to the SEO context type
866 return $this->seo_settings->reset_to_defaults('site');
867 } catch (\Exception $e) {
868 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
869 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging only when WP_DEBUG is enabled.
870 error_log('ThinkRank: Failed to reset SEO settings for category "' . $category . '": ' . $e->getMessage());
871 }
872 return false;
873 }
874 }
875 }
876