Add CSRF, CAPTCHA, and input validation improvements

Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
This commit is contained in:
Hosteroid
2025-10-10 00:04:12 +03:00
parent 98f37c2482
commit a29becc944
41 changed files with 1689 additions and 77 deletions

View File

@@ -33,10 +33,14 @@ class AuthController extends Controller
// Check if registration is enabled
$registrationEnabled = $this->settingModel->getValue('registration_enabled');
// Get CAPTCHA settings
$captchaSettings = $this->settingModel->getCaptchaSettings();
$this->view('auth/login', [
'title' => 'Login',
'registrationEnabled' => $registrationEnabled
'registrationEnabled' => $registrationEnabled,
'captchaSettings' => $captchaSettings
]);
}
@@ -50,10 +54,25 @@ class AuthController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/login');
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? ''; // Don't trim - passwords may have intentional spaces
$remember = isset($_POST['remember']);
// Verify CAPTCHA
$captchaService = new \App\Services\CaptchaService();
$captchaResponse = $_POST['captcha_response'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? null;
$captchaResult = $captchaService->verifyCaptcha($captchaResponse, $remoteIp);
if (!$captchaResult['success']) {
$_SESSION['error'] = $captchaResult['error'] ?? 'CAPTCHA verification failed';
$this->redirect('/login');
return;
}
// Validate input
if (empty($username) || empty($password)) {
$_SESSION['error'] = 'Username and password are required';
@@ -140,8 +159,12 @@ class AuthController extends Controller
return;
}
// Get CAPTCHA settings
$captchaSettings = $this->settingModel->getCaptchaSettings();
$this->view('auth/register', [
'title' => 'Register'
'title' => 'Register',
'captchaSettings' => $captchaSettings
]);
}
@@ -155,6 +178,9 @@ class AuthController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/register');
// Check if registration is enabled
$registrationEnabled = $this->settingModel->getValue('registration_enabled');
if (!$registrationEnabled) {
@@ -163,6 +189,18 @@ class AuthController extends Controller
return;
}
// Verify CAPTCHA
$captchaService = new \App\Services\CaptchaService();
$captchaResponse = $_POST['captcha_response'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? null;
$captchaResult = $captchaService->verifyCaptcha($captchaResponse, $remoteIp);
if (!$captchaResult['success']) {
$_SESSION['error'] = $captchaResult['error'] ?? 'CAPTCHA verification failed';
$this->redirect('/register');
return;
}
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$fullName = trim($_POST['full_name'] ?? '');
@@ -176,6 +214,22 @@ class AuthController extends Controller
return;
}
// Validate username format and length
$usernameError = \App\Helpers\InputValidator::validateUsername($username, 3, 50);
if ($usernameError) {
$_SESSION['error'] = $usernameError;
$this->redirect('/register');
return;
}
// Validate full name length
$nameError = \App\Helpers\InputValidator::validateLength($fullName, 255, 'Full name');
if ($nameError) {
$_SESSION['error'] = $nameError;
$this->redirect('/register');
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error'] = 'Please enter a valid email address';
$this->redirect('/register');
@@ -398,8 +452,12 @@ class AuthController extends Controller
return;
}
// Get CAPTCHA settings
$captchaSettings = $this->settingModel->getCaptchaSettings();
$this->view('auth/forgot-password', [
'title' => 'Forgot Password'
'title' => 'Forgot Password',
'captchaSettings' => $captchaSettings
]);
}
@@ -413,6 +471,21 @@ class AuthController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/forgot-password');
// Verify CAPTCHA
$captchaService = new \App\Services\CaptchaService();
$captchaResponse = $_POST['captcha_response'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? null;
$captchaResult = $captchaService->verifyCaptcha($captchaResponse, $remoteIp);
if (!$captchaResult['success']) {
$_SESSION['error'] = $captchaResult['error'] ?? 'CAPTCHA verification failed';
$this->redirect('/forgot-password');
return;
}
$email = trim($_POST['email'] ?? '');
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
@@ -478,9 +551,13 @@ class AuthController extends Controller
return;
}
// Get CAPTCHA settings
$captchaSettings = $this->settingModel->getCaptchaSettings();
$this->view('auth/reset-password', [
'title' => 'Reset Password',
'token' => $token
'token' => $token,
'captchaSettings' => $captchaSettings
]);
}
@@ -494,6 +571,23 @@ class AuthController extends Controller
return;
}
// CSRF Protection
$token = $_POST['token'] ?? '';
$this->verifyCsrf('/reset-password?token=' . urlencode($token));
// Verify CAPTCHA
$captchaService = new \App\Services\CaptchaService();
$captchaResponse = $_POST['captcha_response'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? null;
$captchaResult = $captchaService->verifyCaptcha($captchaResponse, $remoteIp);
if (!$captchaResult['success']) {
$token = $_POST['token'] ?? '';
$_SESSION['error'] = $captchaResult['error'] ?? 'CAPTCHA verification failed';
$this->redirect('/reset-password?token=' . urlencode($token));
return;
}
$token = $_POST['token'] ?? '';
$password = $_POST['password'] ?? '';
$passwordConfirm = $_POST['password_confirm'] ?? '';

View File

@@ -23,7 +23,7 @@ class DomainController extends Controller
public function index()
{
// Get filter parameters
$search = $_GET['search'] ?? '';
$search = \App\Helpers\InputValidator::sanitizeSearch($_GET['search'] ?? '', 100);
$status = $_GET['status'] ?? '';
$groupId = $_GET['group'] ?? '';
$sortBy = $_GET['sort'] ?? 'domain_name';
@@ -131,6 +131,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains/create');
$domainName = trim($_POST['domain_name'] ?? '');
$groupId = !empty($_POST['notification_group_id']) ? (int)$_POST['notification_group_id'] : null;
@@ -141,6 +144,13 @@ class DomainController extends Controller
return;
}
// Validate domain format
if (!\App\Helpers\InputValidator::validateDomain($domainName)) {
$_SESSION['error'] = 'Invalid domain name format (e.g., example.com)';
$this->redirect('/domains/create');
return;
}
// Check if domain already exists
if ($this->domainModel->existsByDomain($domainName)) {
$_SESSION['error'] = 'Domain already exists';
@@ -212,6 +222,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains');
$id = (int)($params['id'] ?? 0);
$domain = $this->domainModel->find($id);
@@ -391,6 +404,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains/bulk-add');
// POST - Process bulk add
$domainsText = trim($_POST['domains'] ?? '');
$groupId = !empty($_POST['notification_group_id']) ? (int)$_POST['notification_group_id'] : null;
@@ -467,6 +483,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains');
$domainIds = $_POST['domain_ids'] ?? [];
if (empty($domainIds)) {
@@ -475,6 +494,14 @@ class DomainController extends Controller
return;
}
// Validate bulk operation size
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
if ($sizeError) {
$_SESSION['error'] = $sizeError;
$this->redirect('/domains');
return;
}
$refreshed = 0;
$failed = 0;
@@ -516,6 +543,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains');
$domainIds = $_POST['domain_ids'] ?? [];
if (empty($domainIds)) {
@@ -524,6 +554,14 @@ class DomainController extends Controller
return;
}
// Validate bulk operation size
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
if ($sizeError) {
$_SESSION['error'] = $sizeError;
$this->redirect('/domains');
return;
}
$deleted = 0;
foreach ($domainIds as $id) {
if ($this->domainModel->delete($id)) {
@@ -542,6 +580,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains');
$domainIds = $_POST['domain_ids'] ?? [];
$groupId = !empty($_POST['group_id']) ? (int)$_POST['group_id'] : null;
@@ -551,6 +592,14 @@ class DomainController extends Controller
return;
}
// Validate bulk operation size
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
if ($sizeError) {
$_SESSION['error'] = $sizeError;
$this->redirect('/domains');
return;
}
$updated = 0;
foreach ($domainIds as $id) {
if ($this->domainModel->update($id, ['notification_group_id' => $groupId])) {
@@ -569,6 +618,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains');
$domainIds = $_POST['domain_ids'] ?? [];
$isActive = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
@@ -578,6 +630,14 @@ class DomainController extends Controller
return;
}
// Validate bulk operation size
$sizeError = \App\Helpers\InputValidator::validateArraySize($domainIds, 1000, 'Domain selection');
if ($sizeError) {
$_SESSION['error'] = $sizeError;
$this->redirect('/domains');
return;
}
$updated = 0;
foreach ($domainIds as $id) {
if ($this->domainModel->update($id, ['is_active' => $isActive])) {
@@ -597,6 +657,9 @@ class DomainController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/domains');
$id = (int)($params['id'] ?? 0);
$domain = $this->domainModel->find($id);
@@ -608,6 +671,14 @@ class DomainController extends Controller
$notes = $_POST['notes'] ?? '';
// Validate notes length
$lengthError = \App\Helpers\InputValidator::validateLength($notes, 5000, 'Notes');
if ($lengthError) {
$_SESSION['error'] = $lengthError;
$this->redirect('/domains/' . $id);
return;
}
$this->domainModel->update($id, [
'notes' => $notes
]);

View File

@@ -45,6 +45,7 @@ class InstallerController extends Controller
'011_create_sessions_table.sql',
'012_link_remember_tokens_to_sessions.sql',
'013_create_user_notifications_table.sql',
'014_add_captcha_settings.sql',
];
try {
@@ -103,7 +104,8 @@ class InstallerController extends Controller
'010_add_app_version_setting.sql',
'011_create_sessions_table.sql',
'012_link_remember_tokens_to_sessions.sql',
'013_create_user_notifications_table.sql'
'013_create_user_notifications_table.sql',
'014_add_captcha_settings.sql'
];
}
@@ -179,9 +181,10 @@ class InstallerController extends Controller
$adminPassword = trim($_POST['admin_password'] ?? '');
$adminEmail = trim($_POST['admin_email'] ?? '');
// Validate
if (empty($adminUsername) || !preg_match('/^[a-zA-Z0-9_]+$/', $adminUsername)) {
$_SESSION['error'] = 'Username can only contain letters, numbers, and underscores';
// Validate username format and length
$usernameError = \App\Helpers\InputValidator::validateUsername($adminUsername, 3, 50);
if ($usernameError) {
$_SESSION['error'] = $usernameError;
$this->redirect('/install');
return;
}
@@ -257,7 +260,8 @@ class InstallerController extends Controller
'010_add_app_version_setting.sql',
'011_create_sessions_table.sql',
'012_link_remember_tokens_to_sessions.sql',
'013_create_user_notifications_table.sql'
'013_create_user_notifications_table.sql',
'014_add_captcha_settings.sql'
];
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?) ON DUPLICATE KEY UPDATE migration=migration");

View File

@@ -41,6 +41,9 @@ class NotificationGroupController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/groups/create');
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
@@ -50,6 +53,21 @@ class NotificationGroupController extends Controller
return;
}
// Validate length
$nameError = \App\Helpers\InputValidator::validateLength($name, 255, 'Group name');
if ($nameError) {
$_SESSION['error'] = $nameError;
$this->redirect('/groups/create');
return;
}
$descError = \App\Helpers\InputValidator::validateLength($description, 1000, 'Description');
if ($descError) {
$_SESSION['error'] = $descError;
$this->redirect('/groups/create');
return;
}
$id = $this->groupModel->create([
'name' => $name,
'description' => $description
@@ -83,6 +101,9 @@ class NotificationGroupController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/groups');
$id = (int)$_POST['id'];
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
@@ -93,6 +114,21 @@ class NotificationGroupController extends Controller
return;
}
// Validate length
$nameError = \App\Helpers\InputValidator::validateLength($name, 255, 'Group name');
if ($nameError) {
$_SESSION['error'] = $nameError;
$this->redirect("/groups/edit?id=$id");
return;
}
$descError = \App\Helpers\InputValidator::validateLength($description, 1000, 'Description');
if ($descError) {
$_SESSION['error'] = $descError;
$this->redirect("/groups/edit?id=$id");
return;
}
$this->groupModel->update($id, [
'name' => $name,
'description' => $description
@@ -125,6 +161,9 @@ class NotificationGroupController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/groups');
$groupId = (int)$_POST['group_id'];
$channelType = $_POST['channel_type'] ?? '';

View File

@@ -80,6 +80,9 @@ class ProfileController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/profile');
$userId = Auth::id();
$fullName = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? '');
@@ -91,6 +94,14 @@ class ProfileController extends Controller
return;
}
// Validate full name length
$nameError = \App\Helpers\InputValidator::validateLength($fullName, 255, 'Full name');
if ($nameError) {
$_SESSION['error'] = $nameError;
$this->redirect('/profile');
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error'] = 'Please enter a valid email address';
$this->redirect('/profile');
@@ -131,6 +142,9 @@ class ProfileController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/profile');
$userId = Auth::id();
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
@@ -226,6 +240,14 @@ class ProfileController extends Controller
*/
public function logoutOtherSessions()
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/profile');
return;
}
// CSRF Protection
$this->verifyCsrf('/profile');
$userId = Auth::id();
$currentSessionId = session_id();
@@ -273,6 +295,9 @@ class ProfileController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/profile');
$sessionId = $params['sessionId'] ?? '';
$userId = Auth::id();
$currentSessionId = session_id();

View File

@@ -19,7 +19,7 @@ class SearchController extends Controller
public function index()
{
$query = trim($_GET['q'] ?? '');
$query = \App\Helpers\InputValidator::sanitizeSearch($_GET['q'] ?? '', 100);
if (empty($query)) {
$_SESSION['error'] = 'Please enter a search term';
@@ -86,7 +86,7 @@ class SearchController extends Controller
{
header('Content-Type: application/json');
$query = trim($_GET['q'] ?? '');
$query = \App\Helpers\InputValidator::sanitizeSearch($_GET['q'] ?? '', 100);
if (empty($query)) {
echo json_encode(['domains' => [], 'isDomainLike' => false]);

View File

@@ -26,6 +26,7 @@ class SettingsController extends Controller
$settings = $this->settingModel->getAllAsKeyValue();
$appSettings = $this->settingModel->getAppSettings();
$emailSettings = $this->settingModel->getEmailSettings();
$captchaSettings = $this->settingModel->getCaptchaSettings();
// Predefined notification day options
$notificationPresets = [
@@ -68,6 +69,7 @@ class SettingsController extends Controller
'settings' => $settings,
'appSettings' => $appSettings,
'emailSettings' => $emailSettings,
'captchaSettings' => $captchaSettings,
'notificationPresets' => $notificationPresets,
'checkIntervalPresets' => $checkIntervalPresets,
'title' => 'Settings'
@@ -81,6 +83,9 @@ class SettingsController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/settings#monitoring');
try {
// Update notification days
$notificationPreset = $_POST['notification_preset'] ?? 'standard';
@@ -144,6 +149,9 @@ class SettingsController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/settings');
// Update last check run time to show the test worked
$this->settingModel->updateLastCheckRun();
@@ -158,6 +166,9 @@ class SettingsController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/settings#maintenance');
try {
// Clear notification logs older than 30 days
$stmt = $this->settingModel->db->prepare(
@@ -182,6 +193,9 @@ class SettingsController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/settings#app');
try {
$appSettings = [
'app_name' => trim($_POST['app_name'] ?? 'Domain Monitor'),
@@ -237,6 +251,9 @@ class SettingsController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/settings#email');
try {
$emailSettings = [
'mail_host' => trim($_POST['mail_host'] ?? ''),
@@ -278,6 +295,79 @@ class SettingsController extends Controller
}
}
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') {
@@ -285,6 +375,9 @@ class SettingsController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/settings#email');
$testEmail = trim($_POST['test_email'] ?? '');
if (empty($testEmail) || !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) {

View File

@@ -6,18 +6,21 @@ use Core\Controller;
use App\Models\TldRegistry;
use App\Models\TldImportLog;
use App\Services\TldRegistryService;
use App\Services\Logger;
class TldRegistryController extends Controller
{
private TldRegistry $tldModel;
private TldImportLog $importLogModel;
private TldRegistryService $tldService;
private Logger $logger;
public function __construct()
{
$this->tldModel = new TldRegistry();
$this->importLogModel = new TldImportLog();
$this->tldService = new TldRegistryService();
$this->logger = new Logger('tld_registry_controller');
}
/**
@@ -37,7 +40,7 @@ class TldRegistryController extends Controller
*/
public function index()
{
$search = $_GET['search'] ?? '';
$search = \App\Helpers\InputValidator::sanitizeSearch($_GET['search'] ?? '', 100);
$status = $_GET['status'] ?? '';
$dataType = $_GET['data_type'] ?? '';
$page = max(1, (int)($_GET['page'] ?? 1));
@@ -95,6 +98,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importTldList();
@@ -130,6 +136,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importRdapData();
@@ -165,6 +174,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importWhoisDataForMissingTlds();
$remainingCount = $this->tldService->getTldsNeedingWhoisCount();
@@ -246,9 +258,20 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$importType = $_POST['import_type'] ?? '';
$this->logger->separator('Start Progressive Import');
$this->logger->info('Import requested', [
'type' => $importType,
'user_id' => $_SESSION['user_id'] ?? 'unknown',
'username' => $_SESSION['username'] ?? 'unknown'
]);
if (!in_array($importType, ['tld_list', 'rdap', 'whois', 'check_updates', 'complete_workflow'])) {
$this->logger->warning('Invalid import type provided', ['type' => $importType]);
$_SESSION['error'] = 'Invalid import type';
$this->redirect('/tld-registry');
return;
@@ -257,15 +280,27 @@ class TldRegistryController extends Controller
try {
$result = $this->tldService->startProgressiveImport($importType);
$this->logger->info('Import started', [
'status' => $result['status'],
'log_id' => $result['log_id'] ?? null,
'message' => $result['message'] ?? ''
]);
if ($result['status'] === 'complete') {
$_SESSION['success'] = $result['message'];
$this->redirect('/tld-registry');
} else {
// Redirect to progress page
$this->logger->info('Redirecting to progress page', ['log_id' => $result['log_id']]);
$this->redirect('/tld-registry/import-progress/' . $result['log_id']);
}
} catch (\Exception $e) {
$this->logger->error('Failed to start import', [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
$_SESSION['error'] = 'Failed to start import: ' . $e->getMessage();
$this->redirect('/tld-registry');
}
@@ -278,7 +313,10 @@ class TldRegistryController extends Controller
{
$logId = $params['log_id'] ?? 0;
$this->logger->info('Import progress page requested', ['log_id' => $logId]);
if (!$logId) {
$this->logger->warning('Progress page requested with no log_id');
$_SESSION['error'] = 'Invalid import session';
$this->redirect('/tld-registry');
return;
@@ -287,17 +325,25 @@ class TldRegistryController extends Controller
// Get import type from log
$log = $this->importLogModel->find($logId);
if (!$log) {
$this->logger->error('Import log not found', ['log_id' => $logId]);
$_SESSION['error'] = 'Import log not found';
$this->redirect('/tld-registry');
return;
}
$importType = $log['import_type'];
$this->logger->info('Showing progress page', [
'log_id' => $logId,
'import_type' => $importType,
'status' => $log['status']
]);
$titles = [
'tld_list' => 'TLD List Import Progress',
'rdap' => 'RDAP Import Progress',
'whois' => 'WHOIS Import Progress',
'check_updates' => 'Update Check Progress'
'check_updates' => 'Update Check Progress',
'complete_workflow' => 'Complete TLD Import Workflow'
];
$this->view('tld-registry/import-progress', [
@@ -312,20 +358,104 @@ class TldRegistryController extends Controller
*/
public function apiGetImportProgress()
{
$logId = $_GET['log_id'] ?? 0;
// Start detailed logging
$this->logger->separator('API Import Progress Request');
$this->logger->info('API called', [
'log_id' => $_GET['log_id'] ?? 'none',
'user_id' => $_SESSION['user_id'] ?? 'not set',
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
]);
// Start output buffering to catch any accidental output
ob_start();
if (!$logId) {
http_response_code(400);
echo json_encode(['error' => 'Log ID required']);
return;
}
try {
$result = $this->tldService->processNextBatch($logId);
echo json_encode($result);
} catch (\Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
// Clear any previous output
ob_clean();
// Set JSON header immediately
header('Content-Type: application/json');
$logId = $_GET['log_id'] ?? 0;
if (!$logId) {
$this->logger->warning('API call with missing log_id');
ob_end_clean();
$this->json(['error' => 'Log ID required'], 400);
return;
}
// Ensure user is authenticated
if (!isset($_SESSION['user_id'])) {
$this->logger->warning('Unauthenticated API call attempt', ['log_id' => $logId]);
ob_end_clean();
$this->json(['error' => 'Authentication required'], 401);
return;
}
$this->logger->info('Processing batch', ['log_id' => $logId]);
// Add detailed logging around the service call
$this->logger->info('About to call TldRegistryService::processNextBatch', [
'log_id' => $logId,
'service_class' => get_class($this->tldService)
]);
try {
$result = $this->tldService->processNextBatch($logId);
$this->logger->info('processNextBatch returned successfully');
} catch (\Throwable $e) {
$this->logger->critical('processNextBatch threw exception', [
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
throw $e;
}
$this->logger->info('Batch processing result', [
'status' => $result['status'] ?? 'unknown',
'processed' => $result['processed'] ?? 0,
'remaining' => $result['remaining'] ?? 0
]);
// Clean output buffer before sending JSON
ob_end_clean();
$this->json($result);
} catch (\Throwable $e) {
// Catch ALL errors including fatal errors
// Clean any buffered output
if (ob_get_level() > 0) {
ob_end_clean();
}
// Detailed error logging
$this->logger->critical('API Import Progress Fatal Error', [
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'log_id' => $_GET['log_id'] ?? 'none'
]);
$this->logger->error('Stack trace', ['trace' => $e->getTraceAsString()]);
$this->logger->separator('API Error End');
// Ensure we always send JSON
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json');
}
echo json_encode([
'error' => 'An error occurred while processing the import',
'message' => $e->getMessage(),
'status' => 'error',
'log_id' => $_GET['log_id'] ?? null,
'error_type' => get_class($e)
]);
exit;
}
}
@@ -341,6 +471,9 @@ class TldRegistryController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$tldIds = $_POST['tld_ids'] ?? [];
if (empty($tldIds)) {
@@ -349,6 +482,14 @@ class TldRegistryController extends Controller
return;
}
// Validate bulk operation size
$sizeError = \App\Helpers\InputValidator::validateArraySize($tldIds, 500, 'TLD selection');
if ($sizeError) {
$_SESSION['error'] = $sizeError;
$this->redirect('/tld-registry');
return;
}
try {
$deletedCount = 0;
foreach ($tldIds as $id) {
@@ -468,8 +609,7 @@ class TldRegistryController extends Controller
$domain = $_GET['domain'] ?? '';
if (empty($domain)) {
http_response_code(400);
echo json_encode(['error' => 'Domain parameter is required']);
$this->json(['error' => 'Domain parameter is required'], 400);
return;
}
@@ -477,23 +617,22 @@ class TldRegistryController extends Controller
$tldInfo = $this->tldService->getTldInfo($domain);
if ($tldInfo) {
echo json_encode([
$this->json([
'success' => true,
'data' => $tldInfo
]);
} else {
echo json_encode([
$this->json([
'success' => false,
'message' => 'TLD information not found'
]);
}
} catch (\Exception $e) {
http_response_code(500);
echo json_encode([
$this->json([
'success' => false,
'error' => $e->getMessage()
]);
], 500);
}
}

View File

@@ -28,7 +28,7 @@ class UserController extends Controller
public function index()
{
// Get filter parameters
$search = trim($_GET['search'] ?? '');
$search = \App\Helpers\InputValidator::sanitizeSearch($_GET['search'] ?? '', 100);
$roleFilter = $_GET['role'] ?? '';
$statusFilter = $_GET['status'] ?? '';
$sort = $_GET['sort'] ?? 'username';
@@ -97,6 +97,9 @@ class UserController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/users/create');
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$fullName = trim($_POST['full_name'] ?? '');
@@ -111,12 +114,28 @@ class UserController extends Controller
return;
}
// Validate username format and length
$usernameError = \App\Helpers\InputValidator::validateUsername($username, 3, 50);
if ($usernameError) {
$_SESSION['error'] = $usernameError;
$this->redirect('/users/create');
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error'] = 'Invalid email address';
$this->redirect('/users/create');
return;
}
// Validate full name length
$nameError = \App\Helpers\InputValidator::validateLength($fullName, 255, 'Full name');
if ($nameError) {
$_SESSION['error'] = $nameError;
$this->redirect('/users/create');
return;
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$_SESSION['error'] = 'Username can only contain letters, numbers, and underscores';
$this->redirect('/users/create');
@@ -208,6 +227,9 @@ class UserController extends Controller
return;
}
// CSRF Protection
$this->verifyCsrf('/users');
$userId = (int)($_POST['id'] ?? 0);
$user = $this->userModel->find($userId);
@@ -236,6 +258,14 @@ class UserController extends Controller
return;
}
// Validate full name length
$nameError = \App\Helpers\InputValidator::validateLength($fullName, 255, 'Full name');
if ($nameError) {
$_SESSION['error'] = $nameError;
$this->redirect('/users/edit?id=' . $userId);
return;
}
// Check if email is taken by another user
$existingUsers = $this->userModel->where('email', $email);
if (!empty($existingUsers) && $existingUsers[0]['id'] != $userId) {