# thinkrank/1.1.0/includes/core/class-settings.php

ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console &amp; Local SEO, version 1.1.0. 579 lines.

- Page: https://pluginprobe.com/plugins/thinkrank/1.1.0/code/includes/core/class-settings.php
- Raw: https://pluginprobe.com/plugins/thinkrank/1.1.0/raw/includes/core/class-settings.php
- Modified: 2025-08-10T10:57:26+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/thinkrank/1.1.0/code/includes/core/class-settings.php#L10-L20`.

```php
<?php
/**
 * Settings Class
 * 
 * Handles plugin settings and configuration using WordPress options
 * 
 * @package ThinkRank\Core
 * @since 1.0.0
 */

declare(strict_types=1);

namespace ThinkRank\Core;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Settings Class
 * 
 * Single Responsibility: Manage plugin settings and configuration
 * Uses WordPress options for storage
 * 
 * @since 1.0.0
 */
class Settings {
    
    /**
     * Settings cache
     * 
     * @var array
     */
    private array $cache = [];
    
    /**
     * Default settings
     * 
     * @var array
     */
    private array $defaults = [
        // AI Settings
        'ai_provider' => 'openai',
        'openai_api_key' => '',
        'openai_model' => 'gpt-5-nano', // Default to GPT‑5‑nano
        'claude_api_key' => '',
        'claude_model' => 'claude-3-7-sonnet-latest', // Use latest stable Claude
        'gemini_api_key' => '',
        'gemini_model' => 'gemini-2.5-flash',
        'max_tokens' => 1000,
        'temperature' => 0.7,

        // Google API Keys (encrypted)
        'google_analytics_api_key' => '',
        'google_search_console_api_key' => '',
        'google_pagespeed_api_key' => '',

        // Performance Settings
        'cache_duration' => 3600,
        'max_requests_per_minute' => 10,
        'enable_logging' => true,

        // Integration Settings
        'api_timeout' => 30,
        'enable_rate_limiting' => true,
        'auto_test_connections' => true,
        'retry_failed_requests' => true,

        // Social Platform Settings
        // Public IDs (not encrypted)
        'facebook_app_id' => '',
        'facebook_admins' => '',
        'youtube_channel_id' => '',
        'whatsapp_business_id' => '',
        // Verification codes (encrypted)
        'pinterest_site_verification' => '',
        'instagram_verification' => '',
        'tiktok_verification' => '',

        // SEO Settings
        'auto_optimize' => false,
        'seo_score_threshold' => 70,
        'enable_meta_generation' => true,
        'enable_schema_markup' => true,
        
        // UI Settings
        'show_welcome_message' => true,
        'dashboard_widgets' => ['seo_score', 'ai_usage', 'recent_briefs'],
        'editor_panel_position' => 'side',
        
        // Advanced Settings
        'debug_mode' => false,
        'retry_attempts' => 3,
        
        // Privacy Settings
        'data_retention_days' => 90,
        'anonymize_logs' => true,
        'share_usage_data' => false,
        
        // Integration Settings
        'google_analytics_id' => '',
        'search_console_property' => '',

        // GA4 Tracking Settings
        'ga4_measurement_id' => '',
        'ga4_auto_inject' => false,
        'ga4_anonymize_ip' => false,
        'ga4_exclude_admin' => false,
        'ga4_tracking_verified' => false,
        'ga4_last_verification' => '',

        // SEO Analytics Settings
        'seo_analytics_enabled' => false,
        'seo_analytics_setup_completed' => false,
        'seo_analytics_google_analytics_property_id' => '',
        'search_console_property' => '',
        'seo_analytics_enable_ai_insights' => true,
        'seo_analytics_enable_automated_alerts' => false,
        'seo_analytics_enable_predictive_analysis' => false,
        'seo_analytics_monitoring_frequency' => 3600,
        'seo_analytics_alert_thresholds' => [],
        'seo_analytics_report_schedule' => 'weekly',
        'seo_analytics_data_retention_days' => 90,
        'seo_analytics_cache_analytics_data' => true,
        
        // Uninstall Settings
        'keep_data_on_uninstall' => true,
    ];
    
    /**
     * Encrypted settings keys
     *
     * @var array
     */
    private array $encrypted_keys = [
        // AI API Keys
        'openai_api_key',
        'claude_api_key',
        'gemini_api_key',
        // Google API Keys
        'google_analytics_api_key',
        'google_search_console_api_key',
        'google_pagespeed_api_key',
        // Social Platform Verification Codes (sensitive)
        'pinterest_site_verification',
        'instagram_verification',
        'tiktok_verification',
    ];
    
    /**
     * Initialize settings
     * 
     * @return void
     */
    public function init(): void {
        add_action('admin_init', [$this, 'register_settings']);
    }
    
    /**
     * Register WordPress settings
     * 
     * @return void
     */
    public function register_settings(): void {
        register_setting('thinkrank_settings', 'thinkrank_settings', [
            'sanitize_callback' => [$this, 'sanitize_settings'],
            'default' => $this->defaults,
        ]);
    }
    
    /**
     * Get setting value
     * 
     * @param string $key Setting key
     * @param mixed $default Default value
     * @param int $user_id User ID (0 for global, >0 for user-specific)
     * @return mixed Setting value
     */
    public function get(string $key, $default = null, int $user_id = 0) {
        // Check cache first
        $cache_key = $user_id > 0 ? "user_{$user_id}_{$key}" : $key;
        
        if (isset($this->cache[$cache_key])) {
            return $this->maybe_decrypt($key, $this->cache[$cache_key]);
        }
        
        // Load from WordPress options or user meta
        if ($user_id > 0) {
            $value = get_user_meta($user_id, "thinkrank_{$key}", true);
        } else {
            $value = get_option("thinkrank_{$key}", null);
        }


        
        // Handle boolean settings with special logic for WordPress storage quirks
        if (isset($this->defaults[$key]) && is_bool($this->defaults[$key])) {
            // Convert string representations back to booleans
            if ('1' === $value || 1 === $value || true === $value || 'true' === $value) {
                $value = true;
            } elseif ('0' === $value || 0 === $value || false === $value || 'false' === $value) {
                $value = false;
            } elseif (null === $value || '' === $value) {
                // For boolean settings, empty string could mean false was stored
                // Check if this setting was explicitly set by looking for the option
                $option_exists = get_option("thinkrank_{$key}", 'OPTION_NOT_FOUND') !== 'OPTION_NOT_FOUND';
                if ($option_exists && $value === '') {
                    // Empty string exists in DB, this likely means false was stored
                    $value = false;
                } else {
                    // Option doesn't exist, use default
                    $value = $default ?? $this->defaults[$key];
                }
            }
        } else {
            // Non-boolean settings: use default if not found
            if (null === $value || '' === $value) {
                $value = $default ?? ($this->defaults[$key] ?? null);
            }
        }
        
        // Cache the value
        $this->cache[$cache_key] = $value;
        
        return $this->maybe_decrypt($key, $value);
    }
    
    /**
     * Set setting value
     * 
     * @param string $key Setting key
     * @param mixed $value Setting value
     * @param int $user_id User ID (0 for global, >0 for user-specific)
     * @return bool Success status
     */
    public function set(string $key, $value, int $user_id = 0): bool {
        // Check if key exists in defaults
        if (!array_key_exists($key, $this->defaults)) {
            return false;
        }



        // Encrypt if needed
        $encrypted_value = $this->maybe_encrypt($key, $value);

        // Save to WordPress options or user meta
        if ($user_id > 0) {
            $result = update_user_meta($user_id, "thinkrank_{$key}", $encrypted_value);
        } else {
            $option_name = "thinkrank_{$key}";
            $is_sensitive = in_array($key, $this->encrypted_keys, true);

            // Determine if option exists
            $existing = get_option($option_name, '__tr_not_set__');
            if ($existing === '__tr_not_set__') {
                // First-time save: set autoload=no for sensitive options
                $autoload = $is_sensitive ? 'no' : 'yes';
                $result = add_option($option_name, $encrypted_value, '', $autoload);
            } else {
                // Update existing option; enforce autoload=no for sensitive options
                if ($is_sensitive) {
                    $result = update_option($option_name, $encrypted_value, 'no');
                } else {
                    $result = update_option($option_name, $encrypted_value);
                }
            }

            // Verify value actually saved (update_option returns false when unchanged)
            $saved_value = get_option($option_name, 'NOT_FOUND');
            $values_match = ($saved_value === $encrypted_value) ||
                           ($saved_value == $encrypted_value && $encrypted_value !== 'NOT_FOUND');

            // Consider it successful if the value was saved correctly
            $result = $result || $values_match;
        }

        // Update cache
        if ($result) {
            $cache_key = $user_id > 0 ? "user_{$user_id}_{$key}" : $key;
            $this->cache[$cache_key] = $encrypted_value;

            // Invalidate bulk cache when individual setting changes
            $bulk_cache_key = $user_id > 0 ? "bulk_user_{$user_id}" : 'bulk_global';
            wp_cache_delete($bulk_cache_key, 'thinkrank_settings');
        }

        return $result !== false;
    }
    
    /**
     * Delete setting
     * 
     * @param string $key Setting key
     * @param int $user_id User ID (0 for global, >0 for user-specific)
     * @return bool Success status
     */
    public function delete(string $key, int $user_id = 0): bool {
        // Remove from WordPress options or user meta
        if ($user_id > 0) {
            $result = delete_user_meta($user_id, "thinkrank_{$key}");
        } else {
            $result = delete_option("thinkrank_{$key}");
        }
        
        // Remove from cache
        if ($result) {
            $cache_key = $user_id > 0 ? "user_{$user_id}_{$key}" : $key;
            unset($this->cache[$cache_key]);
        }
        
        return $result;
    }
    
    /**
     * Get all settings (optimized with bulk caching)
     *
     * @param int $user_id User ID (0 for global)
     * @return array All settings
     */
    public function get_all(int $user_id = 0): array {
        // Check bulk cache first
        $bulk_cache_key = $user_id > 0 ? "bulk_user_{$user_id}" : 'bulk_global';

        $cached_settings = wp_cache_get($bulk_cache_key, 'thinkrank_settings');
        if ($cached_settings !== false) {
            return $cached_settings;
        }

        $settings = [];

        foreach (array_keys($this->defaults) as $key) {
            $settings[$key] = $this->get($key, null, $user_id);
        }

        // Cache all settings for 5 minutes
        wp_cache_set($bulk_cache_key, $settings, 'thinkrank_settings', 300);

        return $settings;
    }
    
    /**
     * Reset settings to defaults
     * 
     * @param int $user_id User ID (0 for global)
     * @return bool Success status
     */
    public function reset(int $user_id = 0): bool {
        $success = true;
        
        foreach (array_keys($this->defaults) as $key) {
            if (!$this->delete($key, $user_id)) {
                $success = false;
            }
        }
        
        // Clear cache
        $this->cache = [];
        
        return $success;
    }
    
    /**
     * Sanitize settings
     * 
     * @param array $settings Settings array
     * @return array Sanitized settings
     */
    public function sanitize_settings(array $settings): array {
        $sanitized = [];
        
        foreach ($settings as $key => $value) {
            $sanitized[$key] = $this->sanitize_setting($key, $value);
        }
        
        return $sanitized;
    }
    
    /**
     * Sanitize individual setting
     * 
     * @param string $key Setting key
     * @param mixed $value Setting value
     * @return mixed Sanitized value
     */
    private function sanitize_setting(string $key, $value) {
        switch ($key) {
            case 'openai_api_key':
            case 'claude_api_key':
            case 'gemini_api_key':
                return sanitize_text_field($value);

            case 'ai_provider':
            case 'openai_model':
            case 'claude_model':
            case 'gemini_model':
                return sanitize_key($value);
                
            case 'max_tokens':
            case 'cache_duration':
            case 'max_requests_per_minute':
            case 'seo_score_threshold':
            case 'api_timeout':
            case 'retry_attempts':
            case 'data_retention_days':
            case 'monitoring_frequency':
                return absint($value);
                
            case 'temperature':
                return (float) $value;
                
            case 'dashboard_widgets':
            case 'seo_analytics_alert_thresholds':
                return is_array($value) ? array_map('sanitize_key', $value) : [];

            case 'google_analytics_property_id':
            case 'seo_analytics_google_analytics_property_id':
            case 'seo_analytics_report_schedule':
                return sanitize_text_field($value);
                
            default:
                if (is_bool($value)) {
                    return (bool) $value;
                } elseif (is_string($value)) {
                    return sanitize_text_field($value);
                } elseif (is_array($value)) {
                    return array_map('sanitize_text_field', $value);
                }
                return $value;
        }
    }

    /**
     * Encryption version for tracking
     */
    private const ENCRYPTION_VERSION = 2;

    /**
     * Maybe encrypt value using WordPress native encryption
     *
     * @param string $key Setting key
     * @param mixed $value Setting value
     * @return mixed Encrypted or original value
     */
    private function maybe_encrypt(string $key, $value) {
        // Critical fix: disable custom encryption; store raw value without transformation
        return $value;
    }

    /**
     * Maybe decrypt value using WordPress native encryption
     *
     * @param string $key Setting key
     * @param mixed $value Setting value
     * @return mixed Decrypted or original value
     */
    private function maybe_decrypt(string $key, $value) {
        if (!in_array($key, $this->encrypted_keys, true) || empty($value)) {
            return $value;
        }

        // Backward compatibility: if value looks like our old encrypted payload, try to decrypt
        $data = base64_decode((string) $value, true);
        if ($data !== false && strlen($data) >= 33) {
            $version = unpack('C', substr($data, 0, 1))[1] ?? null;
            if ($version === self::ENCRYPTION_VERSION) {
                $decrypted = $this->decrypt_value((string) $value);
                // If decryption worked, return it; otherwise fall through and return original
                if ($decrypted !== '') {
                    return $decrypted;
                }
            }
        }

        // Treat as already plain
        return $value;
    }

    /**
     * Encrypt value using WordPress native methods
     *
     * @param string $value Value to encrypt
     * @return string Encrypted value (base64 encoded) or empty string on failure
     * @throws \Exception When encryption process fails
     */
    private function encrypt_value(string $value): string {
        try {
            // Generate unique salt for this encryption
            $salt = wp_generate_password(32, false);

            // Create encryption key using WordPress salts
            $auth_salt = wp_salt('auth');
            $secure_salt = wp_salt('secure_auth');
            $key = wp_hash($salt . $auth_salt . $secure_salt);

            // XOR encryption
            $encrypted = $this->xor_encrypt($value, $key);

            // Version prefix + salt + encrypted data
            $version = pack('C', self::ENCRYPTION_VERSION);
            return base64_encode($version . $salt . $encrypted);

        } catch (\Exception $e) {
            // Silent failure on encryption error
            return ''; // Return empty on encryption failure
        }
    }

    /**
     * Decrypt value using WordPress native methods
     *
     * @param string $encrypted_value Encrypted value to decrypt
     * @return string Decrypted value or empty string on failure
     * @throws \Exception When decryption process fails
     */
    private function decrypt_value(string $encrypted_value): string {
        try {
            $data = base64_decode($encrypted_value, true);
            if (false === $data || strlen($data) < 33) {
                return '';
            }

            // Extract version, salt, and encrypted data
            $version = unpack('C', substr($data, 0, 1))[1];
            $salt = substr($data, 1, 32);
            $encrypted = substr($data, 33);

            // Verify version
            if ($version !== self::ENCRYPTION_VERSION) {
                return '';
            }

            // Recreate encryption key
            $auth_salt = wp_salt('auth');
            $secure_salt = wp_salt('secure_auth');
            $key = wp_hash($salt . $auth_salt . $secure_salt);

            // Decrypt using XOR
            return $this->xor_decrypt($encrypted, $key);

        } catch (\Exception $e) {
            // Silent failure on decryption error
            return '';
        }
    }

    /**
     * XOR encryption helper
     *
     * @param string $data Data to encrypt
     * @param string $key Encryption key
     * @return string Encrypted data
     */
    private function xor_encrypt(string $data, string $key): string {
        $result = '';
        $key_length = strlen($key);
        $data_length = strlen($data);

        for ($i = 0; $i < $data_length; $i++) {
            $result .= chr(ord($data[$i]) ^ ord($key[$i % $key_length]));
        }

        return $result;
    }

    /**
     * XOR decryption helper (XOR is symmetric)
     *
     * @param string $encrypted Encrypted data
     * @param string $key Encryption key
     * @return string Decrypted data
     */
    private function xor_decrypt(string $encrypted, string $key): string {
        // XOR is symmetric, so decryption is the same as encryption
        return $this->xor_encrypt($encrypted, $key);
    }
}

```
