settingModel = new Setting(); $this->logger = new Logger('settings'); } public function index() { $settings = $this->settingModel->getAllAsKeyValue(); $appSettings = $this->settingModel->getAppSettings(); $emailSettings = $this->settingModel->getEmailSettings(); $captchaSettings = $this->settingModel->getCaptchaSettings(); $twoFactorSettings = $this->settingModel->getTwoFactorSettings(); // Predefined notification day options $notificationPresets = [ 'minimal' => [ 'label' => 'Minimal (30, 7, 1 days)', 'value' => '30,7,1' ], 'standard' => [ 'label' => 'Standard (60, 30, 21, 14, 7, 5, 3, 2, 1 days)', 'value' => '60,30,21,14,7,5,3,2,1' ], 'frequent' => [ 'label' => 'Frequent (90, 60, 45, 30, 21, 14, 10, 7, 5, 3, 2, 1 days)', 'value' => '90,60,45,30,21,14,10,7,5,3,2,1' ], 'business' => [ 'label' => 'Business Focused (60, 30, 14, 7, 3, 1 days)', 'value' => '60,30,14,7,3,1' ], 'conservative' => [ 'label' => 'Conservative (30, 15, 7, 3, 1 days)', 'value' => '30,15,7,3,1' ], 'custom' => [ 'label' => 'Custom', 'value' => 'custom' ] ]; // Check interval presets $checkIntervalPresets = [ ['label' => 'Every 6 hours', 'value' => 6], ['label' => 'Every 12 hours', 'value' => 12], ['label' => 'Daily (24 hours)', 'value' => 24], ['label' => 'Every 2 days (48 hours)', 'value' => 48], ['label' => 'Weekly (168 hours)', 'value' => 168] ]; $this->view('settings/index', [ 'settings' => $settings, 'appSettings' => $appSettings, 'emailSettings' => $emailSettings, 'captchaSettings' => $captchaSettings, 'twoFactorSettings' => $twoFactorSettings, 'notificationPresets' => $notificationPresets, 'checkIntervalPresets' => $checkIntervalPresets, 'title' => 'Settings' ]); } public function update() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings#monitoring'); try { // Update notification days $notificationPreset = $_POST['notification_preset'] ?? 'standard'; if ($notificationPreset === 'custom') { // Custom days entered by user $customDays = trim($_POST['custom_notification_days'] ?? ''); if (empty($customDays)) { $_SESSION['error'] = 'Please enter notification days for custom preset'; $this->redirect('/settings#monitoring'); return; } // Validate custom days (comma-separated integers) $daysArray = array_map('trim', explode(',', $customDays)); $daysArray = array_filter($daysArray, function($day) { return is_numeric($day) && $day > 0; }); if (empty($daysArray)) { $_SESSION['error'] = 'Invalid notification days format. Use comma-separated numbers (e.g., 30,15,7,1)'; $this->redirect('/settings#monitoring'); return; } // Sort in descending order rsort($daysArray, SORT_NUMERIC); $notificationDays = implode(',', $daysArray); } else { // Use preset value $notificationDays = $_POST['notification_days_before'] ?? '30,15,7,3,1'; } // Update check interval $checkInterval = (int)($_POST['check_interval_hours'] ?? 24); if ($checkInterval < 1 || $checkInterval > 720) { // Max 30 days $_SESSION['error'] = 'Check interval must be between 1 and 720 hours'; $this->redirect('/settings#monitoring'); return; } // Save settings $this->settingModel->setValue('notification_days_before', $notificationDays); $this->settingModel->setValue('check_interval_hours', $checkInterval); $_SESSION['success'] = 'Settings updated successfully'; $this->redirect('/settings#monitoring'); } catch (\Exception $e) { $_SESSION['error'] = 'Failed to update settings: ' . $e->getMessage(); $this->redirect('/settings#monitoring'); } } public function testCron() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings'); // Update last check run time to show the test worked $this->settingModel->updateLastCheckRun(); $_SESSION['info'] = 'Test notification sent (feature coming soon). Last check time updated.'; $this->redirect('/settings'); } public function clearLogs() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings#maintenance'); try { // Clear notification logs older than 30 days $stmt = $this->settingModel->db->prepare( "DELETE FROM notification_logs WHERE sent_at < DATE_SUB(NOW(), INTERVAL 30 DAY)" ); $stmt->execute(); $deleted = $stmt->rowCount(); $_SESSION['success'] = "Cleared $deleted old notification log(s)"; $this->redirect('/settings#maintenance'); } catch (\Exception $e) { $_SESSION['error'] = 'Failed to clear logs: ' . $e->getMessage(); $this->redirect('/settings#maintenance'); } } public function updateApp() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings#app'); try { $appSettings = [ 'app_name' => trim($_POST['app_name'] ?? 'Domain Monitor'), 'app_url' => trim($_POST['app_url'] ?? 'http://localhost:8000'), 'app_timezone' => trim($_POST['app_timezone'] ?? 'UTC') ]; // Validate app_name if (empty($appSettings['app_name'])) { $_SESSION['error'] = 'Application name is required'; $this->redirect('/settings#app'); return; } // Validate app_url if (empty($appSettings['app_url']) || !filter_var($appSettings['app_url'], FILTER_VALIDATE_URL)) { $_SESSION['error'] = 'Please enter a valid application URL'; $this->redirect('/settings#app'); return; } // Validate timezone $validTimezones = timezone_identifiers_list(); if (!in_array($appSettings['app_timezone'], $validTimezones)) { $_SESSION['error'] = 'Invalid timezone selected'; $this->redirect('/settings#app'); return; } // Update app settings $this->settingModel->updateAppSettings($appSettings); // Update registration settings $registrationEnabled = isset($_POST['registration_enabled']) ? '1' : '0'; $requireEmailVerification = isset($_POST['require_email_verification']) ? '1' : '0'; $this->settingModel->setValue('registration_enabled', $registrationEnabled); $this->settingModel->setValue('require_email_verification', $requireEmailVerification); $_SESSION['success'] = 'Application settings updated successfully'; $this->redirect('/settings#app'); } catch (\Exception $e) { $_SESSION['error'] = 'Failed to update application settings: ' . $e->getMessage(); $this->redirect('/settings#app'); } } public function updateEmail() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings#email'); try { $port = (int)trim($_POST['mail_port'] ?? '2525'); $encryption = trim($_POST['mail_encryption'] ?? 'tls'); // Auto-detect encryption based on port if not explicitly set $originalEncryption = $encryption; if (empty($encryption) || $encryption === 'tls') { if ($port === 465) { $encryption = 'ssl'; // Port 465 should use SSL $this->logger->info('Auto-detected SSL encryption for port 465', [ 'port' => $port, 'original_encryption' => $originalEncryption, 'detected_encryption' => $encryption ]); } elseif ($port === 587) { $encryption = 'tls'; // Port 587 should use TLS $this->logger->info('Auto-detected TLS encryption for port 587', [ 'port' => $port, 'original_encryption' => $originalEncryption, 'detected_encryption' => $encryption ]); } // For other ports, keep the user's selection } $emailSettings = [ 'mail_host' => trim($_POST['mail_host'] ?? ''), 'mail_port' => $port, 'mail_username' => trim($_POST['mail_username'] ?? ''), 'mail_password' => trim($_POST['mail_password'] ?? ''), 'mail_encryption' => $encryption, 'mail_from_address' => trim($_POST['mail_from_address'] ?? ''), 'mail_from_name' => trim($_POST['mail_from_name'] ?? 'Domain Monitor') ]; // Validate required fields if (empty($emailSettings['mail_host'])) { $_SESSION['error'] = 'Mail host is required'; $this->redirect('/settings#email'); return; } if (empty($emailSettings['mail_from_address']) || !filter_var($emailSettings['mail_from_address'], FILTER_VALIDATE_EMAIL)) { $_SESSION['error'] = 'Please enter a valid from email address'; $this->redirect('/settings#email'); return; } // Validate port if (!is_numeric($emailSettings['mail_port']) || $emailSettings['mail_port'] < 1 || $emailSettings['mail_port'] > 65535) { $_SESSION['error'] = 'Please enter a valid port number (1-65535)'; $this->redirect('/settings#email'); return; } $this->settingModel->updateEmailSettings($emailSettings); $_SESSION['success'] = 'Email settings updated successfully'; $this->redirect('/settings#email'); } catch (\Exception $e) { $_SESSION['error'] = 'Failed to update email settings: ' . $e->getMessage(); $this->redirect('/settings#email'); } } public function updateCaptcha() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings#security'); try { $captchaProvider = trim($_POST['captcha_provider'] ?? 'disabled'); $captchaSiteKey = trim($_POST['captcha_site_key'] ?? ''); $captchaSecretKey = trim($_POST['captcha_secret_key'] ?? ''); $recaptchaV3Threshold = trim($_POST['recaptcha_v3_score_threshold'] ?? '0.5'); // Validate provider $validProviders = ['disabled', 'recaptcha_v2', 'recaptcha_v3', 'turnstile']; if (!in_array($captchaProvider, $validProviders)) { $_SESSION['error'] = 'Invalid CAPTCHA provider selected'; $this->redirect('/settings#security'); return; } // If CAPTCHA is enabled, validate keys if ($captchaProvider !== 'disabled') { if (empty($captchaSiteKey)) { $_SESSION['error'] = 'Site key is required when CAPTCHA is enabled'; $this->redirect('/settings#security'); return; } if (empty($captchaSecretKey)) { $_SESSION['error'] = 'Secret key is required when CAPTCHA is enabled'; $this->redirect('/settings#security'); return; } } // Validate v3 score threshold if ($captchaProvider === 'recaptcha_v3') { $threshold = floatval($recaptchaV3Threshold); if ($threshold < 0.0 || $threshold > 1.0) { $_SESSION['error'] = 'reCAPTCHA v3 score threshold must be between 0.0 and 1.0'; $this->redirect('/settings#security'); return; } } // Prepare settings array $captchaSettings = [ 'captcha_provider' => $captchaProvider, 'captcha_site_key' => $captchaSiteKey, 'recaptcha_v3_score_threshold' => $recaptchaV3Threshold ]; // Only update secret key if provided (to allow updating other settings without re-entering secret) if (!empty($captchaSecretKey)) { $captchaSettings['captcha_secret_key'] = $captchaSecretKey; } // Update CAPTCHA settings $this->settingModel->updateCaptchaSettings($captchaSettings); $_SESSION['success'] = 'CAPTCHA settings updated successfully'; $this->redirect('/settings#security'); } catch (\Exception $e) { $_SESSION['error'] = 'Failed to update CAPTCHA settings: ' . $e->getMessage(); $this->redirect('/settings#security'); } } public function testEmail() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings#email'); $testEmail = trim($_POST['test_email'] ?? ''); if (empty($testEmail) || !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) { $_SESSION['error'] = 'Please enter a valid email address'; $this->redirect('/settings#email'); return; } // Use EmailHelper to send test email $result = EmailHelper::sendTestEmail($testEmail); if ($result['success']) { $_SESSION['success'] = $result['message']; $this->logger->info('Test email sent successfully', [ 'email' => $testEmail ]); } else { // Log detailed error information for debugging $this->logger->error('Test email failed', [ 'email' => $testEmail, 'debug_info' => $result['debug_info'] ?? null, 'error' => $result['error'] ?? null ]); $_SESSION['error'] = $result['message']; // In development, show more detailed error if (($_ENV['APP_ENV'] ?? 'production') === 'development') { $_SESSION['error'] .= " (Debug: " . ($result['debug_info'] ?? $result['error']) . ")"; } } $this->redirect('/settings#email'); } public function updateTwoFactor() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('/settings'); return; } // CSRF Protection $this->verifyCsrf('/settings#security'); try { $twoFactorPolicy = trim($_POST['two_factor_policy'] ?? 'optional'); $rateLimitMinutes = (int)($_POST['two_factor_rate_limit_minutes'] ?? 15); $emailCodeExpiryMinutes = (int)($_POST['two_factor_email_code_expiry_minutes'] ?? 10); // Validate policy $validPolicies = ['disabled', 'optional', 'forced']; if (!in_array($twoFactorPolicy, $validPolicies)) { $_SESSION['error'] = 'Invalid 2FA policy selected'; $this->redirect('/settings#security'); return; } // Validate rate limit (1-60 minutes) if ($rateLimitMinutes < 1 || $rateLimitMinutes > 60) { $_SESSION['error'] = 'Rate limit must be between 1 and 60 minutes'; $this->redirect('/settings#security'); return; } // Validate email code expiry (1-30 minutes) if ($emailCodeExpiryMinutes < 1 || $emailCodeExpiryMinutes > 30) { $_SESSION['error'] = 'Email code expiry must be between 1 and 30 minutes'; $this->redirect('/settings#security'); return; } $twoFactorSettings = [ 'two_factor_policy' => $twoFactorPolicy, 'two_factor_rate_limit_minutes' => $rateLimitMinutes, 'two_factor_email_code_expiry_minutes' => $emailCodeExpiryMinutes ]; $this->settingModel->updateTwoFactorSettings($twoFactorSettings); $_SESSION['success'] = 'Two-Factor Authentication settings updated successfully'; $this->redirect('/settings#security'); } catch (\Exception $e) { $_SESSION['error'] = 'Failed to update 2FA settings: ' . $e->getMessage(); $this->redirect('/settings#security'); } } }