This commit is contained in:
Peter
2025-06-17 19:22:58 +01:00
parent 623b29dea4
commit 283ea68ff8
6 changed files with 574 additions and 24 deletions

7
.recaptcha-config.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
// Google reCAPTCHA v3 Configuration
// IMPORTANT: Replace these with your actual keys from https://www.google.com/recaptcha/admin
define('RECAPTCHA_SITE_KEY', '6LcdAtUUAAAAAPX-5YJaWKJmeq7QIMjeLTS7qy6s'); // Replace with your site key
define('RECAPTCHA_SECRET_KEY', '6LcdAtUUAAAAANsEDSRbB_-EcCGtCDf5wGuUYj2u'); // Replace with your secret key
define('RECAPTCHA_THRESHOLD', 0.5); // Score threshold (0.0 - 1.0), higher is more strict
?>

233
admin/spam-dashboard.php Normal file
View File

@@ -0,0 +1,233 @@
<?php
// Spam Analysis Dashboard
session_start();
// Simple authentication check (you should implement proper admin authentication)
$authKey = $_GET['key'] ?? '';
if ($authKey !== 'your-secret-admin-key-2024') {
header('HTTP/1.0 403 Forbidden');
exit('Access Denied');
}
// Function to parse log files
function parseLogFile($filename) {
if (!file_exists($filename)) {
return [];
}
$entries = [];
$lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (preg_match('/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) - (.+)$/', $line, $matches)) {
$entries[] = [
'timestamp' => $matches[1],
'message' => $matches[2]
];
}
}
return array_reverse($entries); // Most recent first
}
// Get log data
$contactSubmissions = parseLogFile('../logs/contact-submissions.log');
$contactErrors = parseLogFile('../logs/contact-errors.log');
$blockedIPs = file_exists('../logs/blocked-ips.txt') ? file('../logs/blocked-ips.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
// Analyze spam patterns
$spamPatterns = [
'recaptcha_failures' => 0,
'direct_post_attempts' => 0,
'rate_limit_hits' => 0,
'low_interaction_scores' => 0,
'blocked_ips_count' => count($blockedIPs),
'hourly_distribution' => array_fill(0, 24, 0),
'top_ips' => []
];
$ipCounts = [];
foreach ($contactErrors as $error) {
if (strpos($error['message'], 'RECAPTCHA FAILED') !== false) {
$spamPatterns['recaptcha_failures']++;
}
if (strpos($error['message'], 'DIRECT POST') !== false) {
$spamPatterns['direct_post_attempts']++;
}
if (strpos($error['message'], 'Too many requests') !== false) {
$spamPatterns['rate_limit_hits']++;
}
if (strpos($error['message'], 'LOW INTERACTION SCORE') !== false) {
$spamPatterns['low_interaction_scores']++;
}
// Extract IP addresses
if (preg_match('/from ([0-9.]+)/', $error['message'], $matches)) {
$ip = $matches[1];
$ipCounts[$ip] = ($ipCounts[$ip] ?? 0) + 1;
}
// Track hourly distribution
$hour = (int)date('G', strtotime($error['timestamp']));
$spamPatterns['hourly_distribution'][$hour]++;
}
// Get top spam IPs
arsort($ipCounts);
$spamPatterns['top_ips'] = array_slice($ipCounts, 0, 10, true);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spam Analysis Dashboard - UK Data Services</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { margin-bottom: 30px; color: #764ba2; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-card h3 { font-size: 14px; color: #666; margin-bottom: 10px; }
.stat-card .value { font-size: 32px; font-weight: bold; color: #764ba2; }
.section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.section h2 { margin-bottom: 15px; color: #333; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f8f8; font-weight: 600; }
.log-entry { font-family: monospace; font-size: 12px; }
.error { color: #ef4444; }
.success { color: #10b981; }
.chart { margin: 20px 0; }
.bar { background: #764ba2; height: 20px; margin-bottom: 5px; transition: width 0.3s; }
.bar-label { font-size: 12px; color: #666; }
.blocked-ip { background: #fee; color: #c00; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<h1>Spam Analysis Dashboard</h1>
<div class="stats-grid">
<div class="stat-card">
<h3>reCAPTCHA Failures</h3>
<div class="value"><?php echo $spamPatterns['recaptcha_failures']; ?></div>
</div>
<div class="stat-card">
<h3>Direct POST Attempts</h3>
<div class="value"><?php echo $spamPatterns['direct_post_attempts']; ?></div>
</div>
<div class="stat-card">
<h3>Rate Limit Hits</h3>
<div class="value"><?php echo $spamPatterns['rate_limit_hits']; ?></div>
</div>
<div class="stat-card">
<h3>Low Interaction Scores</h3>
<div class="value"><?php echo $spamPatterns['low_interaction_scores']; ?></div>
</div>
<div class="stat-card">
<h3>Blocked IPs</h3>
<div class="value"><?php echo $spamPatterns['blocked_ips_count']; ?></div>
</div>
<div class="stat-card">
<h3>Successful Submissions</h3>
<div class="value"><?php echo count($contactSubmissions); ?></div>
</div>
</div>
<div class="section">
<h2>Hourly Spam Distribution</h2>
<div class="chart">
<?php
$maxHourly = max($spamPatterns['hourly_distribution']);
for ($hour = 0; $hour < 24; $hour++):
$count = $spamPatterns['hourly_distribution'][$hour];
$width = $maxHourly > 0 ? ($count / $maxHourly * 100) : 0;
?>
<div style="display: flex; align-items: center; margin-bottom: 5px;">
<div class="bar-label" style="width: 40px;"><?php echo str_pad($hour, 2, '0', STR_PAD_LEFT); ?>:00</div>
<div class="bar" style="width: <?php echo $width; ?>%;"></div>
<div class="bar-label" style="margin-left: 10px;"><?php echo $count; ?></div>
</div>
<?php endfor; ?>
</div>
</div>
<div class="section">
<h2>Top Spam Source IPs</h2>
<table>
<thead>
<tr>
<th>IP Address</th>
<th>Attempts</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($spamPatterns['top_ips'] as $ip => $count): ?>
<tr>
<td><span class="blocked-ip"><?php echo htmlspecialchars($ip); ?></span></td>
<td><?php echo $count; ?></td>
<td>
<?php
$isBlocked = false;
foreach ($blockedIPs as $blockedEntry) {
if (strpos($blockedEntry, $ip . '|') === 0) {
$isBlocked = true;
break;
}
}
echo $isBlocked ? '<span class="error">Blocked</span>' : '<span class="success">Active</span>';
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="section">
<h2>Recent Error Log (Last 20)</h2>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Error</th>
</tr>
</thead>
<tbody>
<?php foreach (array_slice($contactErrors, 0, 20) as $error): ?>
<tr>
<td><?php echo htmlspecialchars($error['timestamp']); ?></td>
<td class="log-entry"><?php echo htmlspecialchars($error['message']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="section">
<h2>Recent Successful Submissions (Last 10)</h2>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php foreach (array_slice($contactSubmissions, 0, 10) as $submission): ?>
<tr>
<td><?php echo htmlspecialchars($submission['timestamp']); ?></td>
<td class="log-entry"><?php echo htmlspecialchars($submission['message']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</body>
</html>

View File

@@ -171,13 +171,82 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Enhanced animations initialized');
// Form Validation and Enhancement
// Enhanced Form Validation with Anti-Spam Measures
const contactForm = document.querySelector('.contact-form form');
if (contactForm) {
// Track form interactions for bot detection
let formInteractions = {
mouseMovements: 0,
keystrokes: 0,
focusEvents: 0,
startTime: Date.now(),
fields: {}
};
// Add hidden timestamp field
const timestampField = document.createElement('input');
timestampField.type = 'hidden';
timestampField.name = 'form_timestamp';
timestampField.value = Date.now();
contactForm.appendChild(timestampField);
// Add hidden interaction token
const interactionToken = document.createElement('input');
interactionToken.type = 'hidden';
interactionToken.name = 'interaction_token';
contactForm.appendChild(interactionToken);
// Track mouse movements (humans move mouse)
let mouseMoveHandler = function() {
if (formInteractions.mouseMovements < 100) {
formInteractions.mouseMovements++;
}
};
document.addEventListener('mousemove', mouseMoveHandler);
// Track interactions on form fields
contactForm.querySelectorAll('input, textarea, select').forEach(field => {
field.addEventListener('keydown', function() {
formInteractions.keystrokes++;
if (!formInteractions.fields[field.name]) {
formInteractions.fields[field.name] = {
keystrokes: 0,
changes: 0,
focusTime: 0
};
}
formInteractions.fields[field.name].keystrokes++;
});
field.addEventListener('focus', function() {
formInteractions.focusEvents++;
if (formInteractions.fields[field.name]) {
formInteractions.fields[field.name].focusTime = Date.now();
}
});
field.addEventListener('change', function() {
if (formInteractions.fields[field.name]) {
formInteractions.fields[field.name].changes++;
}
});
});
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
// Calculate interaction score
const timeSpent = Date.now() - formInteractions.startTime;
const interactionScore = calculateInteractionScore(formInteractions, timeSpent);
// Set interaction token
interactionToken.value = btoa(JSON.stringify({
score: interactionScore,
time: timeSpent,
interactions: formInteractions.mouseMovements + formInteractions.keystrokes + formInteractions.focusEvents
}));
// Basic form validation
const formData = new FormData(this);
const name = formData.get('name');
@@ -203,6 +272,12 @@ document.addEventListener('DOMContentLoaded', function() {
isValid = false;
}
// Check interaction score (low score = likely bot)
if (interactionScore < 30) {
errors.push('Please complete the form normally');
isValid = false;
}
if (isValid) {
// Show loading state
const submitButton = this.querySelector('button[type="submit"]');
@@ -210,18 +285,29 @@ document.addEventListener('DOMContentLoaded', function() {
submitButton.textContent = 'Sending...';
submitButton.disabled = true;
// Submit form (you'll need to implement the backend handler)
// Submit form with XMLHttpRequest header
fetch('contact-handler.php', {
method: 'POST',
body: formData
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Message sent successfully! We\'ll get back to you soon.', 'success');
showNotification(data.message || 'Message sent successfully! We\'ll get back to you soon.', 'success');
this.reset();
// Reset interaction tracking
formInteractions = {
mouseMovements: 0,
keystrokes: 0,
focusEvents: 0,
startTime: Date.now(),
fields: {}
};
} else {
showNotification('There was an error sending your message. Please try again.', 'error');
showNotification(data.message || 'There was an error sending your message. Please try again.', 'error');
}
})
.catch(error => {
@@ -238,6 +324,29 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Calculate interaction score to detect bots
function calculateInteractionScore(interactions, timeSpent) {
let score = 0;
// Time-based scoring (bots submit too fast)
if (timeSpent > 5000) score += 20; // More than 5 seconds
if (timeSpent > 10000) score += 20; // More than 10 seconds
if (timeSpent > 30000) score += 10; // More than 30 seconds
// Mouse movement scoring (humans move mouse)
if (interactions.mouseMovements > 10) score += 20;
if (interactions.mouseMovements > 50) score += 10;
// Keystroke scoring (humans type)
if (interactions.keystrokes > 20) score += 20;
if (interactions.keystrokes > 50) score += 10;
// Focus event scoring (humans tab/click between fields)
if (interactions.focusEvents > 3) score += 10;
return Math.min(score, 100); // Cap at 100
}
// Email validation function
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

View File

@@ -2,6 +2,9 @@
// Enhanced Contact Form Handler with Security
session_start();
// Include reCAPTCHA config
require_once '.recaptcha-config.php';
// Security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
@@ -143,6 +146,54 @@ if (!checkRateLimit()) {
sendResponse(false, 'Too many requests. Please try again later.');
}
// Verify reCAPTCHA v3
if (isset($_POST['recaptcha_response'])) {
$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
$recaptcha_secret = RECAPTCHA_SECRET_KEY;
$recaptcha_response = $_POST['recaptcha_response'];
$recaptcha_data = array(
'secret' => $recaptcha_secret,
'response' => $recaptcha_response,
'remoteip' => $_SERVER['REMOTE_ADDR']
);
$recaptcha_options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($recaptcha_data)
)
);
$recaptcha_context = stream_context_create($recaptcha_options);
$recaptcha_result = @file_get_contents($recaptcha_url, false, $recaptcha_context);
if ($recaptcha_result === false) {
// Log reCAPTCHA API failure but don't block submission
error_log("reCAPTCHA API call failed for IP: " . $_SERVER['REMOTE_ADDR']);
} else {
$recaptcha_json = json_decode($recaptcha_result, true);
if (!$recaptcha_json['success'] || $recaptcha_json['score'] < RECAPTCHA_THRESHOLD) {
// Log suspicious activity and block
$logEntry = date('Y-m-d H:i:s') . " - RECAPTCHA FAILED: Score " . ($recaptcha_json['score'] ?? '0') . " from " . $_SERVER['REMOTE_ADDR'] . "\n";
file_put_contents('logs/contact-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
// Add to blocked IPs if score is very low
if (isset($recaptcha_json['score']) && $recaptcha_json['score'] < 0.3) {
$blockEntry = $_SERVER['REMOTE_ADDR'] . '|' . time() . "\n";
file_put_contents('logs/blocked-ips.txt', $blockEntry, FILE_APPEND | LOCK_EX);
}
sendResponse(false, 'Security verification failed. Please try again.');
}
}
} else {
// No reCAPTCHA response - likely a bot
sendResponse(false, 'Security verification required.');
}
// Validate and sanitize inputs
$name = validateInput($_POST['name'] ?? '', 'text');
$email = validateInput($_POST['email'] ?? '', 'email');
@@ -182,9 +233,24 @@ $spamKeywords = [
'no obligation', 'free money', 'make money fast', 'work from home', 'get rich',
'investment opportunity', 'credit repair', 'refinance', 'consolidate debt',
'weight loss', 'miracle cure', 'lose weight', 'adult content', 'porn',
'sex', 'dating', 'singles', 'webcam', 'escort', 'massage'
'sex', 'dating', 'singles', 'webcam', 'escort', 'massage', 'crypto', 'nft',
'blockchain', 'earn money', 'passive income', 'financial freedom', 'mlm',
'network marketing', 'online casino', 'gambling', 'betting', 'binary options'
];
// Check for disposable email domains
$disposableEmailDomains = [
'mailinator.com', 'guerrillamail.com', '10minutemail.com', 'temp-mail.org',
'throwawaymail.com', 'yopmail.com', 'mailnesia.com', 'trashmail.com',
'maildrop.cc', 'mailcatch.com', 'tempmail.com', 'email-fake.com',
'fakeinbox.com', 'sharklasers.com', 'guerrillamailblock.com'
];
$emailDomain = substr(strrchr($email, "@"), 1);
if (in_array(strtolower($emailDomain), $disposableEmailDomains)) {
sendResponse(false, 'Please use a valid business email address.');
}
$messageContent = strtolower($message . ' ' . $name . ' ' . $company);
foreach ($spamKeywords as $keyword) {
if (strpos($messageContent, $keyword) !== false) {
@@ -209,6 +275,39 @@ if (isset($_SESSION['form_start_time'])) {
}
}
// Check for XMLHttpRequest header (JavaScript submission)
if (!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
// Log direct POST attempt
$logEntry = date('Y-m-d H:i:s') . " - DIRECT POST attempt from " . $_SERVER['REMOTE_ADDR'] . "\n";
file_put_contents('logs/contact-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
sendResponse(false, 'Invalid submission method');
}
// Verify interaction token (human behavior verification)
if (isset($_POST['interaction_token']) && !empty($_POST['interaction_token'])) {
$tokenData = @json_decode(base64_decode($_POST['interaction_token']), true);
if ($tokenData && isset($tokenData['score'])) {
if ($tokenData['score'] < 30) {
// Log low interaction score
$logEntry = date('Y-m-d H:i:s') . " - LOW INTERACTION SCORE: " . $tokenData['score'] . " from " . $_SERVER['REMOTE_ADDR'] . "\n";
file_put_contents('logs/contact-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
sendResponse(false, 'Please complete the form normally');
}
}
}
// Verify form timestamp (prevent replay attacks)
if (isset($_POST['form_timestamp'])) {
$formTimestamp = intval($_POST['form_timestamp']);
$currentTime = time() * 1000; // Convert to milliseconds
$timeDiff = $currentTime - $formTimestamp;
// Form older than 1 hour or from the future
if ($timeDiff > 3600000 || $timeDiff < 0) {
sendResponse(false, 'Form session expired. Please refresh and try again.');
}
}
// Update rate limit counter
$ip = $_SERVER['REMOTE_ADDR'];
$key = 'contact_' . md5($ip);

View File

@@ -98,6 +98,10 @@ $twitter_card_image = "https://ukdataservices.co.uk/assets/images/ukds-main-logo
<!-- Resource Preloading for Performance -->
<link rel="preload" href="assets/css/main.min.css" as="style">
<!-- Google reCAPTCHA v3 -->
<?php require_once '.recaptcha-config.php'; ?>
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo RECAPTCHA_SITE_KEY; ?>"></script>
<link rel="preload" href="assets/images/ukds-main-logo.webp" as="image">
<link rel="preload" href="assets/js/main.min.js" as="script">
@@ -920,6 +924,7 @@ $twitter_card_image = "https://ukdataservices.co.uk/assets/images/ukds-main-logo
</div>
<button type="submit" class="btn btn-primary btn-full">Submit Enquiry</button>
<input type="hidden" name="recaptcha_response" id="recaptchaResponse">
</form>
</div>
</div>
@@ -985,6 +990,26 @@ $twitter_card_image = "https://ukdataservices.co.uk/assets/images/ukds-main-logo
<!-- Scripts -->
<script src="assets/js/main.min.js"></script>
<!-- reCAPTCHA v3 Integration -->
<script>
// Execute reCAPTCHA on form submission
document.addEventListener('DOMContentLoaded', function() {
const contactForm = document.querySelector('form[action="contact-handler.php"]');
if (contactForm) {
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
grecaptcha.ready(function() {
grecaptcha.execute('<?php echo RECAPTCHA_SITE_KEY; ?>', {action: 'contact'}).then(function(token) {
document.getElementById('recaptchaResponse').value = token;
contactForm.submit();
});
});
});
}
});
</script>
<!-- Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {

109
quote.php
View File

@@ -39,6 +39,10 @@ $canonical_url = "https://ukdataservices.co.uk/quote";
<!-- Styles -->
<link rel="stylesheet" href="assets/css/main.css">
<!-- Google reCAPTCHA v3 -->
<?php require_once '.recaptcha-config.php'; ?>
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo RECAPTCHA_SITE_KEY; ?>"></script>
<!-- Quote Page Schema -->
<script type="application/ld+json">
{
@@ -667,11 +671,67 @@ $canonical_url = "https://ukdataservices.co.uk/quote";
});
});
// Form submission
// Enhanced form submission with reCAPTCHA
const quoteForm = document.getElementById('quote-form');
// Track form interactions
let formInteractions = {
mouseMovements: 0,
keystrokes: 0,
focusEvents: 0,
startTime: Date.now(),
fields: {}
};
// Add hidden timestamp field
const timestampField = document.createElement('input');
timestampField.type = 'hidden';
timestampField.name = 'form_timestamp';
timestampField.value = Date.now();
quoteForm.appendChild(timestampField);
// Add hidden interaction token
const interactionToken = document.createElement('input');
interactionToken.type = 'hidden';
interactionToken.name = 'interaction_token';
quoteForm.appendChild(interactionToken);
// Track mouse movements
document.addEventListener('mousemove', function() {
if (formInteractions.mouseMovements < 100) {
formInteractions.mouseMovements++;
}
});
// Track form field interactions
quoteForm.querySelectorAll('input, textarea, select').forEach(field => {
field.addEventListener('keydown', function() {
formInteractions.keystrokes++;
});
field.addEventListener('focus', function() {
formInteractions.focusEvents++;
});
});
quoteForm.addEventListener('submit', function(e) {
e.preventDefault();
// Calculate interaction score
const timeSpent = Date.now() - formInteractions.startTime;
const interactionScore = Math.min(100,
(timeSpent > 5000 ? 20 : 0) +
(formInteractions.mouseMovements > 10 ? 20 : 0) +
(formInteractions.keystrokes > 20 ? 20 : 0) +
(formInteractions.focusEvents > 3 ? 10 : 0)
);
// Set interaction token
interactionToken.value = btoa(JSON.stringify({
score: interactionScore,
time: timeSpent,
interactions: formInteractions.mouseMovements + formInteractions.keystrokes + formInteractions.focusEvents
}));
// Validate form
const services = document.querySelectorAll('input[name="services[]"]:checked');
const projectScale = document.querySelector('input[name="project_scale"]:checked');
@@ -711,23 +771,40 @@ $canonical_url = "https://ukdataservices.co.uk/quote";
return;
}
// Submit form
const submitButton = this.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Sending Quote Request...';
submitButton.disabled = true;
// Execute reCAPTCHA
const self = this;
grecaptcha.ready(function() {
grecaptcha.execute('<?php echo RECAPTCHA_SITE_KEY; ?>', {action: 'quote'}).then(function(token) {
document.getElementById('recaptchaResponseQuote').value = token;
const formData = new FormData(this);
// Submit form
const submitButton = self.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Sending Quote Request...';
submitButton.disabled = true;
fetch('quote-handler.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Thank you! Your quote request has been sent. We will get back to you within 24 hours with a detailed proposal.');
this.reset();
const formData = new FormData(self);
fetch('quote-handler.php', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Thank you! Your quote request has been sent. We will get back to you within 24 hours with a detailed proposal.');
self.reset();
// Reset interaction tracking
formInteractions = {
mouseMovements: 0,
keystrokes: 0,
focusEvents: 0,
startTime: Date.now(),
fields: {}
};
// Reset styling
checkboxItems.forEach(item => item.classList.remove('checked'));
radioItems.forEach(item => item.classList.remove('checked'));