['type' => 'email', 'required' => true, 'label' => 'Admin Email'], 'mail_smtp_username' => ['type' => 'email', 'required' => false, 'label' => 'SMTP Username'], 'paypal_email' => ['type' => 'email', 'required' => false, 'label' => 'PayPal Email'], // URL settings 'branding_logo_url' => ['type' => 'url', 'required' => false, 'label' => 'Logo URL'], 'branding_favicon_url' => ['type' => 'url', 'required' => false, 'label' => 'Favicon URL'], 'main_url' => ['type' => 'url', 'required' => true, 'label' => 'Main URL'], // Color settings 'branding_primary_color' => ['type' => 'color', 'pattern' => '/^#[0-9A-F]{6}$/i', 'required' => true, 'label' => 'Primary Color'], 'branding_secondary_color' => ['type' => 'color', 'pattern' => '/^#[0-9A-F]{6}$/i', 'required' => true, 'label' => 'Secondary Color'], 'token_color' => ['type' => 'color', 'pattern' => '/^#[0-9A-F]{6}$/i', 'required' => false, 'label' => 'Token Color'], // Integer settings 'mail_smtp_port' => ['type' => 'int', 'min' => 1, 'max' => 65535, 'required' => true, 'label' => 'SMTP Port'], 'creator_payout_percentage' => ['type' => 'int', 'min' => 0, 'max' => 100, 'required' => true, 'label' => 'Payout Percentage'], 'signup_min_age' => ['type' => 'int', 'min' => 13, 'max' => 100, 'required' => true, 'label' => 'Minimum Age'], 'signup_max_age' => ['type' => 'int', 'min' => 13, 'max' => 150, 'required' => true, 'label' => 'Maximum Age'], 'signup_min_password' => ['type' => 'int', 'min' => 4, 'max' => 50, 'required' => true, 'label' => 'Min Password Length'], 'signup_max_password' => ['type' => 'int', 'min' => 8, 'max' => 100, 'required' => true, 'label' => 'Max Password Length'], 'signup_min_username' => ['type' => 'int', 'min' => 3, 'max' => 20, 'required' => true, 'label' => 'Min Username Length'], 'signup_max_username' => ['type' => 'int', 'min' => 5, 'max' => 50, 'required' => true, 'label' => 'Max Username Length'], // Float settings 'minimum_payout_amount' => ['type' => 'float', 'min' => 0, 'required' => true, 'label' => 'Minimum Payout'], // Boolean settings 'video_module' => ['type' => 'bool', 'label' => 'Video Module'], 'live_module' => ['type' => 'bool', 'label' => 'Live Module'], 'short_module' => ['type' => 'bool', 'label' => 'Short Module'], 'image_module' => ['type' => 'bool', 'label' => 'Image Module'], 'audio_module' => ['type' => 'bool', 'label' => 'Audio Module'], 'document_module' => ['type' => 'bool', 'label' => 'Document Module'], 'blog_module' => ['type' => 'bool', 'label' => 'Blog Module'], 'stripe_enabled' => ['type' => 'bool', 'label' => 'Stripe Enabled'], 'paypal_test' => ['type' => 'bool', 'label' => 'PayPal Test Mode'], 'creator_payout_enabled' => ['type' => 'bool', 'label' => 'Creator Payouts Enabled'], 'login_remember' => ['type' => 'bool', 'label' => 'Remember Me Feature'], 'mail_smtp_auth' => ['type' => 'bool_string', 'label' => 'SMTP Authentication'], // String with pattern 'stripe_publishable_key' => ['type' => 'string', 'pattern' => '/^pk_(test|live)_/', 'required' => false, 'label' => 'Stripe Publishable Key'], 'stripe_secret_key' => ['type' => 'string', 'pattern' => '/^sk_(test|live)_/', 'required' => false, 'label' => 'Stripe Secret Key'], // String with length 'website_shortname' => ['type' => 'string', 'min_length' => 1, 'max_length' => 50, 'required' => true, 'label' => 'Website Name'], 'head_title' => ['type' => 'string', 'min_length' => 1, 'max_length' => 100, 'required' => true, 'label' => 'Site Title'], 'backend_email_fromname' => ['type' => 'string', 'min_length' => 1, 'max_length' => 50, 'required' => true, 'label' => 'Email From Name'], ]; /** * Initialize settings manager * @param PDO $pdo Database connection */ public function __construct($pdo = null) { if ($pdo) { $this->pdo = $pdo; } else { // Use global PDO if available global $db_pdo; $this->pdo = $db_pdo ?? null; } // Initialize Redis if available try { if (class_exists('VRedis')) { $this->redis = VRedis::getInstance(); } } catch (Exception $e) { $this->redis = null; error_log("Settings: Redis not available - " . $e->getMessage()); } } /** * Load all settings into cache * @return bool Success status */ private function loadCache() { if ($this->cacheLoaded) { return true; } // Try Redis first if ($this->redis) { try { $cached = $this->redis->get($this->redisKey); if ($cached) { $this->cache = json_decode($cached, true); $this->cacheLoaded = true; return true; } } catch (Exception $e) { error_log("Settings: Redis cache read error - " . $e->getMessage()); } } // Fallback to database if (!$this->pdo) { return false; } try { $stmt = $this->pdo->query("SELECT cfg_name, cfg_data, cfg_info FROM db_settings"); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $this->cache[$row['cfg_name']] = [ 'value' => $row['cfg_data'], 'info' => $row['cfg_info'] ]; } $this->cacheLoaded = true; // Store in Redis for next time if ($this->redis) { try { $this->redis->set( $this->redisKey, json_encode($this->cache), $this->redisTTL ); } catch (Exception $e) { error_log("Settings: Redis cache write error - " . $e->getMessage()); } } return true; } catch (PDOException $e) { error_log("Settings cache load error: " . $e->getMessage()); return false; } } /** * Get a setting value * @param string $key Setting name * @param mixed $default Default value if not found * @return mixed Setting value or default */ public function get($key, $default = null) { $this->loadCache(); if (isset($this->cache[$key])) { $value = $this->cache[$key]['value']; // Decrypt if needed if ($this->shouldEncrypt($key)) { return $this->decrypt($value); } return $value; } return $default; } /** * Get multiple settings at once * @param array $keys Array of setting names * @return array Associative array of settings */ public function getMultiple(array $keys) { $this->loadCache(); $results = []; foreach ($keys as $key) { $results[$key] = $this->get($key); } return $results; } /** * Get all settings in a category * @param string $prefix Category prefix (e.g., 'backend_', 'mail_', 'token_') * @return array Associative array of settings */ public function getByPrefix($prefix) { $this->loadCache(); $results = []; foreach ($this->cache as $key => $data) { if (strpos($key, $prefix) === 0) { $results[$key] = $data['value']; } } return $results; } /** * Check if a setting should be encrypted * @param string $key Setting name * @return bool */ private function shouldEncrypt($key) { return in_array($key, $this->encryptedKeys); } /** * Get encryption key from config * @return string */ private function getEncryptionKey() { return defined('ENC_FIRSTKEY') ? base64_decode(ENC_FIRSTKEY) : ''; } /** * Get IV for encryption * @return string */ private function getIV() { return defined('ENC_SECONDKEY') ? substr(base64_decode(ENC_SECONDKEY), 0, 16) : ''; } /** * Encrypt a value * @param string $value Value to encrypt * @return string Encrypted value */ private function encrypt($value) { if (empty($value)) { return $value; } try { $encrypted = openssl_encrypt( $value, 'AES-256-CBC', $this->getEncryptionKey(), 0, $this->getIV() ); return base64_encode($encrypted); } catch (Exception $e) { error_log("Settings encryption error: " . $e->getMessage()); return $value; } } /** * Decrypt a value * @param string $value Value to decrypt * @return string Decrypted value */ private function decrypt($value) { if (empty($value)) { return $value; } try { $decrypted = openssl_decrypt( base64_decode($value), 'AES-256-CBC', $this->getEncryptionKey(), 0, $this->getIV() ); return $decrypted ?: $value; } catch (Exception $e) { error_log("Settings decryption error: " . $e->getMessage()); return $value; } } /** * Validate a setting value * @param string $key Setting name * @param mixed $value Value to validate * @return array ['valid' => bool, 'error' => string] */ public function validate($key, $value) { // No rules = allow if (!isset($this->validationRules[$key])) { return ['valid' => true]; } $rule = $this->validationRules[$key]; // Check required if (isset($rule['required']) && $rule['required'] && ($value === '' || $value === null)) { return [ 'valid' => false, 'error' => "{$rule['label']} is required" ]; } // Skip validation if empty and not required if (($value === '' || $value === null) && (!isset($rule['required']) || !$rule['required'])) { return ['valid' => true]; } // Type-specific validation switch ($rule['type']) { case 'email': if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { return ['valid' => false, 'error' => "{$rule['label']} must be a valid email address"]; } break; case 'url': if (!filter_var($value, FILTER_VALIDATE_URL)) { return ['valid' => false, 'error' => "{$rule['label']} must be a valid URL"]; } break; case 'color': if (isset($rule['pattern']) && !preg_match($rule['pattern'], $value)) { return ['valid' => false, 'error' => "{$rule['label']} must be a valid hex color (e.g., #FF0000)"]; } break; case 'int': if (!is_numeric($value) || (int)$value != $value) { return ['valid' => false, 'error' => "{$rule['label']} must be a whole number"]; } $intVal = (int)$value; if (isset($rule['min']) && $intVal < $rule['min']) { return ['valid' => false, 'error' => "{$rule['label']} must be at least {$rule['min']}"]; } if (isset($rule['max']) && $intVal > $rule['max']) { return ['valid' => false, 'error' => "{$rule['label']} must be at most {$rule['max']}"]; } break; case 'float': if (!is_numeric($value)) { return ['valid' => false, 'error' => "{$rule['label']} must be a number"]; } $floatVal = (float)$value; if (isset($rule['min']) && $floatVal < $rule['min']) { return ['valid' => false, 'error' => "{$rule['label']} must be at least {$rule['min']}"]; } if (isset($rule['max']) && $floatVal > $rule['max']) { return ['valid' => false, 'error' => "{$rule['label']} must be at most {$rule['max']}"]; } break; case 'bool': if (!in_array($value, ['0', '1', 0, 1, true, false], true)) { return ['valid' => false, 'error' => "{$rule['label']} must be 0 or 1"]; } break; case 'bool_string': if (!in_array($value, ['true', 'false'], true)) { return ['valid' => false, 'error' => "{$rule['label']} must be 'true' or 'false'"]; } break; case 'string': if (isset($rule['min_length']) && strlen($value) < $rule['min_length']) { return ['valid' => false, 'error' => "{$rule['label']} must be at least {$rule['min_length']} characters"]; } if (isset($rule['max_length']) && strlen($value) > $rule['max_length']) { return ['valid' => false, 'error' => "{$rule['label']} must be at most {$rule['max_length']} characters"]; } if (isset($rule['pattern']) && !preg_match($rule['pattern'], $value)) { return ['valid' => false, 'error' => "{$rule['label']} has an invalid format"]; } break; } return ['valid' => true]; } /** * Set a setting value * @param string $key Setting name * @param mixed $value Setting value * @param string $info Optional description * @return bool Success status */ public function set($key, $value, $info = '') { if (!$this->pdo) { return false; } // Validate before saving $validation = $this->validate($key, $value); if (!$validation['valid']) { throw new InvalidArgumentException($validation['error']); } try { // Get old value for audit trail $oldValue = $this->get($key); // Encrypt sensitive values before saving $saveValue = $this->shouldEncrypt($key) ? $this->encrypt($value) : $value; // Check if setting exists $stmt = $this->pdo->prepare("SELECT id FROM db_settings WHERE cfg_name = ?"); $stmt->execute([$key]); $exists = $stmt->fetch(); if ($exists) { // Update existing setting $stmt = $this->pdo->prepare("UPDATE db_settings SET cfg_data = ?, cfg_info = ? WHERE cfg_name = ?"); $result = $stmt->execute([$saveValue, $info ?: $this->cache[$key]['info'] ?? '', $key]); } else { // Insert new setting $stmt = $this->pdo->prepare("INSERT INTO db_settings (cfg_name, cfg_data, cfg_info) VALUES (?, ?, ?)"); $result = $stmt->execute([$key, $saveValue, $info]); } // Update cache $this->cache[$key] = [ 'value' => $value, 'info' => $info ?: ($this->cache[$key]['info'] ?? '') ]; // Log the change to audit trail $this->logChange($key, $oldValue, $value); // Invalidate Redis cache if ($this->redis) { try { $this->redis->delete($this->redisKey); } catch (Exception $e) { error_log("Settings: Redis cache invalidation error - " . $e->getMessage()); } } return $result; } catch (PDOException $e) { error_log("Settings set error: " . $e->getMessage()); return false; } } /** * Set multiple settings at once * @param array $settings Associative array of key => value pairs * @return bool Success status */ public function setMultiple(array $settings) { if (!$this->pdo) { return false; } try { $this->pdo->beginTransaction(); foreach ($settings as $key => $value) { $this->set($key, $value); } $this->pdo->commit(); return true; } catch (Exception $e) { $this->pdo->rollBack(); error_log("Settings setMultiple error: " . $e->getMessage()); return false; } } /** * Delete a setting * @param string $key Setting name * @return bool Success status */ public function delete($key) { if (!$this->pdo) { return false; } try { $stmt = $this->pdo->prepare("DELETE FROM db_settings WHERE cfg_name = ?"); $result = $stmt->execute([$key]); unset($this->cache[$key]); return $result; } catch (PDOException $e) { error_log("Settings delete error: " . $e->getMessage()); return false; } } /** * Get all settings grouped by category * @return array Multi-dimensional array of settings by category */ public function getAllGrouped() { $this->loadCache(); $grouped = [ 'general' => [], 'backend' => [], 'frontend' => [], 'mail' => [], 'payment' => [], 'token' => [], 'branding' => [], 'security' => [], 'seo' => [], 'modules' => [], 'conversion' => [], 'social' => [], 'other' => [] ]; foreach ($this->cache as $key => $data) { $category = 'other'; if (strpos($key, 'backend_') === 0) { $category = 'backend'; } elseif (strpos($key, 'mail_') === 0) { $category = 'mail'; } elseif (strpos($key, 'paypal_') === 0 || strpos($key, 'stripe_') === 0 || strpos($key, 'payment_') === 0) { $category = 'payment'; } elseif (strpos($key, 'token_') === 0) { $category = 'token'; } elseif (strpos($key, 'branding_') === 0 || strpos($key, 'color_') === 0 || strpos($key, 'logo_') === 0) { $category = 'branding'; } elseif (strpos($key, 'signup_') === 0 || strpos($key, 'login_') === 0 || strpos($key, 'username_') === 0) { $category = 'security'; } elseif (strpos($key, 'head_') === 0 || strpos($key, 'metaname_') === 0 || strpos($key, 'website_') === 0) { $category = 'seo'; } elseif (strpos($key, '_module') !== false || $key === 'paid_memberships') { $category = 'modules'; } elseif (strpos($key, 'conversion_') === 0) { $category = 'conversion'; } elseif (strpos($key, 'fb_') === 0 || strpos($key, 'google_') === 0 || strpos($key, 'twitter_') === 0) { $category = 'social'; } else { $category = 'general'; } $grouped[$category][$key] = [ 'value' => $data['value'], 'info' => $data['info'] ]; } return $grouped; } /** * Export all settings as JSON * @return string JSON string */ public function exportJSON() { $this->loadCache(); return json_encode($this->cache, JSON_PRETTY_PRINT); } /** * Import settings from JSON * @param string $json JSON string * @return bool Success status */ public function importJSON($json) { $data = json_decode($json, true); if (!$data) { return false; } try { $this->pdo->beginTransaction(); foreach ($data as $key => $item) { $this->set($key, $item['value'], $item['info'] ?? ''); } $this->pdo->commit(); return true; } catch (Exception $e) { $this->pdo->rollBack(); error_log("Settings import error: " . $e->getMessage()); return false; } } /** * Check if a module is enabled * @param string $module Module name (video, live, blog, etc.) * @return bool Enabled status */ public function isModuleEnabled($module) { return $this->get($module . '_module', '0') === '1'; } /** * Enable or disable a module * @param string $module Module name * @param bool $enabled Enable/disable * @return bool Success status */ public function setModuleEnabled($module, $enabled) { return $this->set($module . '_module', $enabled ? '1' : '0'); } /** * Get site branding information * @return array Branding settings */ public function getBranding() { return [ 'site_name' => $this->get('website_shortname', 'EasyStream'), 'site_title' => $this->get('head_title', 'EasyStream'), 'primary_color' => $this->get('branding_primary_color', '#1a73e8'), 'secondary_color' => $this->get('branding_secondary_color', '#34a853'), 'logo_url' => $this->get('branding_logo_url', ''), 'favicon_url' => $this->get('branding_favicon_url', ''), 'meta_description' => $this->get('metaname_description', ''), 'meta_keywords' => $this->get('metaname_keywords', '') ]; } /** * Get payment gateway configuration * @return array Payment settings */ public function getPaymentConfig() { return [ 'enabled_gateways' => explode(',', $this->get('payment_methods', 'Paypal')), 'paypal' => [ 'email' => $this->get('paypal_email', ''), 'test_mode' => $this->get('paypal_test', '1') === '1', 'client_id' => $this->get('paypal_client_id', ''), 'secret' => $this->get('paypal_secret', '') ], 'stripe' => [ 'enabled' => $this->get('stripe_enabled', '0') === '1', 'publishable_key' => $this->get('stripe_publishable_key', ''), 'secret_key' => $this->get('stripe_secret_key', ''), 'webhook_secret' => $this->get('stripe_webhook_secret', '') ] ]; } /** * Get email/SMTP configuration * @return array Email settings */ public function getEmailConfig() { return [ 'type' => $this->get('mail_type', 'smtp'), 'from_email' => $this->get('backend_email', ''), 'from_name' => $this->get('backend_email_fromname', 'Webmaster'), 'smtp' => [ 'host' => $this->get('mail_smtp_host', 'smtp.gmail.com'), 'port' => (int)$this->get('mail_smtp_port', '587'), 'username' => $this->get('mail_smtp_username', ''), 'password' => $this->get('mail_smtp_password', ''), 'auth' => $this->get('mail_smtp_auth', 'true') === 'true', 'prefix' => $this->get('mail_smtp_prefix', 'tls') ] ]; } /** * Get token economy configuration * @return array Token settings */ public function getTokenConfig() { return [ 'enabled' => $this->get('token_system_enabled', '1') === '1', 'name' => $this->get('token_name', 'Tokens'), 'symbol' => $this->get('token_symbol', 'TKN'), 'color' => $this->get('token_color', '#FFD700'), 'icon_url' => $this->get('token_icon_url', ''), 'creator_payout_enabled' => $this->get('creator_payout_enabled', '0') === '1', 'creator_payout_percentage' => (int)$this->get('creator_payout_percentage', '70'), 'minimum_payout' => (float)$this->get('minimum_payout_amount', '50.00') ]; } /** * Log a settings change to audit trail * @param string $key Setting name * @param mixed $oldValue Old value * @param mixed $newValue New value * @param string $reason Change reason * @return bool Success status */ private function logChange($key, $oldValue, $newValue, $reason = '') { if (!$this->pdo) { return false; } global $usr; try { $stmt = $this->pdo->prepare(" INSERT INTO db_settings_history (cfg_name, old_value, new_value, changed_by, ip_address, user_agent, change_reason) VALUES (?, ?, ?, ?, ?, ?, ?) "); return $stmt->execute([ $key, $oldValue, $newValue, $usr['id'] ?? $_SESSION['admin_user_id'] ?? 0, $_SERVER['REMOTE_ADDR'] ?? '', substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255), $reason ]); } catch (PDOException $e) { error_log("Settings audit log error: " . $e->getMessage()); return false; } } /** * Get change history for a specific setting * @param string $key Setting name * @param int $limit Number of records to return * @return array History records */ public function getHistory($key, $limit = 20) { if (!$this->pdo) { return []; } try { $stmt = $this->pdo->prepare(" SELECT h.*, u.usr_user as username FROM db_settings_history h LEFT JOIN db_accountuser u ON h.changed_by = u.usr_id WHERE h.cfg_name = ? ORDER BY h.changed_at DESC LIMIT ? "); $stmt->execute([$key, $limit]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { error_log("Settings history error: " . $e->getMessage()); return []; } } /** * Get all recent settings changes * @param int $limit Number of records to return * @return array History records */ public function getAllRecentChanges($limit = 50) { if (!$this->pdo) { return []; } try { $stmt = $this->pdo->prepare(" SELECT h.*, u.usr_user as username, s.cfg_info FROM db_settings_history h LEFT JOIN db_accountuser u ON h.changed_by = u.usr_id LEFT JOIN db_settings s ON h.cfg_name = s.cfg_name ORDER BY h.changed_at DESC LIMIT ? "); $stmt->execute([$limit]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { error_log("Settings recent changes error: " . $e->getMessage()); return []; } } /** * Rollback a setting to a previous value from history * @param string $key Setting name * @param int $historyId History record ID to rollback to * @return bool Success status */ public function rollback($key, $historyId) { if (!$this->pdo) { return false; } try { // Get the historical value $stmt = $this->pdo->prepare("SELECT old_value FROM db_settings_history WHERE id = ? AND cfg_name = ?"); $stmt->execute([$historyId, $key]); $history = $stmt->fetch(PDO::FETCH_ASSOC); if (!$history) { return false; } // Set the old value back return $this->set($key, $history['old_value'], '', 'Rollback from history #' . $historyId); } catch (PDOException $e) { error_log("Settings rollback error: " . $e->getMessage()); return false; } } }