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:
@@ -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'] ?? '';
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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'] ?? '';
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user