Fixes.
This commit is contained in:
7
.recaptcha-config.php
Normal file
7
.recaptcha-config.php
Normal 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
233
admin/spam-dashboard.php
Normal 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>
|
||||
@@ -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@]+$/;
|
||||
|
||||
@@ -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);
|
||||
|
||||
25
index.php
25
index.php
@@ -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) {
|
||||
|
||||
87
quote.php
87
quote.php
@@ -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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Submit form
|
||||
const submitButton = this.querySelector('button[type="submit"]');
|
||||
const submitButton = self.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.textContent;
|
||||
submitButton.textContent = 'Sending Quote Request...';
|
||||
submitButton.disabled = true;
|
||||
|
||||
const formData = new FormData(this);
|
||||
const formData = new FormData(self);
|
||||
|
||||
fetch('quote-handler.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
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.');
|
||||
this.reset();
|
||||
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'));
|
||||
|
||||
Reference in New Issue
Block a user