feat: Add comprehensive documentation suite and reorganize project structure

- Created complete documentation in docs/ directory
- Added PROJECT_OVERVIEW.md with feature highlights and getting started guide
- Added ARCHITECTURE.md with system design and technical details
- Added SECURITY.md with comprehensive security implementation guide
- Added DEVELOPMENT.md with development workflows and best practices
- Added DEPLOYMENT.md with production deployment instructions
- Added API.md with complete REST API documentation
- Added CONTRIBUTING.md with contribution guidelines
- Added CHANGELOG.md with version history and migration notes
- Reorganized all documentation files into docs/ directory for better organization
- Updated README.md with proper documentation links and quick navigation
- Enhanced project structure with professional documentation standards
This commit is contained in:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View File

@@ -0,0 +1,864 @@
<?php
/*******************************************************************************************************************
| Software Name : EasyStream
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
| Software Author : (c) Sami Ahmed
|*******************************************************************************************************************
|
|*******************************************************************************************************************
| This source file is subject to the EasyStream Proprietary License Agreement.
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|*******************************************************************************************************************
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|*******************************************************************************************************************/
defined('_ISVALID') or header('Location: /error');
/**
* Settings Management Class
* Provides unified interface for all system settings stored in db_settings table
*/
class Settings
{
private $pdo;
private $redis;
private $cache = [];
private $cacheLoaded = false;
private $redisKey = 'easystream:settings:all';
private $redisTTL = 3600; // 1 hour cache
// Sensitive settings that should be encrypted
private $encryptedKeys = [
'paypal_secret',
'paypal_client_id',
'stripe_secret_key',
'stripe_webhook_secret',
'mail_smtp_password',
'google_client_secret',
'fb_app_secret',
'twitter_api_secret',
];
// Validation rules for settings
private $validationRules = [
// Email settings
'backend_email' => ['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;
}
}
}