'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); } }