diff --git a/.htaccess b/.htaccess
index 8e88e9f..3c51502 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,146 +1,150 @@
-# Security Rules for UK Data Services
-
-# Protect sensitive files and configs
-
- Require all denied
-
-
-# Protect contact handlers from direct browser access (POST only)
-
-
- Require all denied
-
-
-
-
-
- Require all denied
-
-
-
-# Security headers
-
- Header always set X-Content-Type-Options "nosniff"
- Header always set X-Frame-Options "SAMEORIGIN"
- Header always set X-XSS-Protection "1; mode=block"
- Header always set Referrer-Policy "strict-origin-when-cross-origin"
-
-
-# Enhanced Gzip compression
-
- # Enable compression for all text-based files
- AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
- AddOutputFilterByType DEFLATE application/javascript application/x-javascript
- AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml
- AddOutputFilterByType DEFLATE application/json application/ld+json
- AddOutputFilterByType DEFLATE image/svg+xml
- AddOutputFilterByType DEFLATE font/ttf font/otf font/eot font/woff font/woff2
-
- # Remove browser bugs for older browsers
- BrowserMatch ^Mozilla/4 gzip-only-text/html
- BrowserMatch ^Mozilla/4\.0[678] no-gzip
- BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
- Header append Vary User-Agent
-
-
-# Enable Brotli compression if available
-
- AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript
- AddOutputFilterByType BROTLI_COMPRESS application/javascript application/x-javascript
- AddOutputFilterByType BROTLI_COMPRESS application/xml application/xhtml+xml application/rss+xml
- AddOutputFilterByType BROTLI_COMPRESS application/json application/ld+json
- AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
- AddOutputFilterByType BROTLI_COMPRESS font/ttf font/otf font/woff font/woff2
-
-
-# Browser Caching Headers
-
- ExpiresActive On
-
- # Images - 1 year
- ExpiresByType image/jpeg "access plus 1 year"
- ExpiresByType image/jpg "access plus 1 year"
- ExpiresByType image/gif "access plus 1 year"
- ExpiresByType image/png "access plus 1 year"
- ExpiresByType image/webp "access plus 1 year"
- ExpiresByType image/svg+xml "access plus 1 year"
- ExpiresByType image/x-icon "access plus 1 year"
- ExpiresByType image/ico "access plus 1 year"
-
- # Fonts - 1 year
- ExpiresByType font/ttf "access plus 1 year"
- ExpiresByType font/otf "access plus 1 year"
- ExpiresByType font/woff "access plus 1 year"
- ExpiresByType font/woff2 "access plus 1 year"
- ExpiresByType application/font-woff "access plus 1 year"
- ExpiresByType application/font-woff2 "access plus 1 year"
-
- # CSS and JavaScript - 1 month
- ExpiresByType text/css "access plus 1 month"
- ExpiresByType application/javascript "access plus 1 month"
- ExpiresByType text/javascript "access plus 1 month"
- ExpiresByType application/x-javascript "access plus 1 month"
-
- # HTML and PHP - 1 hour
- ExpiresByType text/html "access plus 1 hour"
- ExpiresByType application/xhtml+xml "access plus 1 hour"
-
- # Data - no cache
- ExpiresByType application/json "access plus 0 seconds"
- ExpiresByType application/xml "access plus 0 seconds"
- ExpiresByType text/xml "access plus 0 seconds"
-
- # Default - 1 week
- ExpiresDefault "access plus 1 week"
-
-
-# Cache-Control Headers
-
- # Static assets - 1 year
-
- Header set Cache-Control "max-age=31536000, public, immutable"
-
-
- # CSS and JS - 1 month
-
- Header set Cache-Control "max-age=2592000, public"
-
-
- # HTML/PHP - 1 hour
-
- Header set Cache-Control "max-age=3600, public, must-revalidate"
-
-
- # Keep-alive
- Header set Connection keep-alive
-
-
-# HTTP/2 Server Push
-
- # Push critical resources
-
- Header add Link "; rel=preload; as=style"
- Header add Link "; rel=preload; as=image"
- Header add Link "; rel=preload; as=script"
-
-
-
-# ETags
-FileETag None
-Header unset ETag
-
-# Disable directory browsing
-Options -Indexes
-
-# Prevent access to logs and database directories
-
- RewriteEngine On
-
+# Security Rules for UK Data Services
+
+# Protect sensitive files and configs
+
+ Require all denied
+
+
+# Protect contact handlers from direct browser access (POST only)
+
+
+ Require all denied
+
+
+
+
+
+ Require all denied
+
+
+
+# Security headers
+
+ Header always set X-Content-Type-Options "nosniff"
+ Header always set X-Frame-Options "SAMEORIGIN"
+ Header always set X-XSS-Protection "1; mode=block"
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
+
+ # CRITICAL: No caching for form pages (contain session-specific CSRF tokens)
+
+ Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
+ Header set Pragma "no-cache"
+ Header set Expires "Sat, 01 Jan 2000 00:00:00 GMT"
+
+
+
+# Enhanced Gzip compression
+
+ AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
+ AddOutputFilterByType DEFLATE application/javascript application/x-javascript
+ AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml
+ AddOutputFilterByType DEFLATE application/json application/ld+json
+ AddOutputFilterByType DEFLATE image/svg+xml
+ AddOutputFilterByType DEFLATE font/ttf font/otf font/eot font/woff font/woff2
+
+ BrowserMatch ^Mozilla/4 gzip-only-text/html
+ BrowserMatch ^Mozilla/4\.0[678] no-gzip
+ BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
+ Header append Vary User-Agent
+
+
+# Enable Brotli compression if available
+
+ AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript
+ AddOutputFilterByType BROTLI_COMPRESS application/javascript application/x-javascript
+ AddOutputFilterByType BROTLI_COMPRESS application/xml application/xhtml+xml application/rss+xml
+ AddOutputFilterByType BROTLI_COMPRESS application/json application/ld+json
+ AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
+ AddOutputFilterByType BROTLI_COMPRESS font/ttf font/otf font/woff font/woff2
+
+
+# Browser Caching Headers
+
+ ExpiresActive On
+
+ # Images - 1 year
+ ExpiresByType image/jpeg "access plus 1 year"
+ ExpiresByType image/jpg "access plus 1 year"
+ ExpiresByType image/gif "access plus 1 year"
+ ExpiresByType image/png "access plus 1 year"
+ ExpiresByType image/webp "access plus 1 year"
+ ExpiresByType image/svg+xml "access plus 1 year"
+ ExpiresByType image/x-icon "access plus 1 year"
+ ExpiresByType image/ico "access plus 1 year"
+
+ # Fonts - 1 year
+ ExpiresByType font/ttf "access plus 1 year"
+ ExpiresByType font/otf "access plus 1 year"
+ ExpiresByType font/woff "access plus 1 year"
+ ExpiresByType font/woff2 "access plus 1 year"
+ ExpiresByType application/font-woff "access plus 1 year"
+ ExpiresByType application/font-woff2 "access plus 1 year"
+
+ # CSS and JavaScript - 1 month
+ ExpiresByType text/css "access plus 1 month"
+ ExpiresByType application/javascript "access plus 1 month"
+ ExpiresByType text/javascript "access plus 1 month"
+ ExpiresByType application/x-javascript "access plus 1 month"
+
+ # HTML and PHP - 1 hour
+ ExpiresByType text/html "access plus 1 hour"
+ ExpiresByType application/xhtml+xml "access plus 1 hour"
+
+ # Data - no cache
+ ExpiresByType application/json "access plus 0 seconds"
+ ExpiresByType application/xml "access plus 0 seconds"
+ ExpiresByType text/xml "access plus 0 seconds"
+
+ # Default - 1 week
+ ExpiresDefault "access plus 1 week"
+
+
+# Cache-Control Headers
+
+ # Static assets - 1 year
+
+ Header set Cache-Control "max-age=31536000, public, immutable"
+
+
+ # CSS and JS - 1 month
+
+ Header set Cache-Control "max-age=2592000, public"
+
+
+ # Regular HTML/PHP - 1 hour (but form pages are excluded above)
+
+ Header set Cache-Control "max-age=3600, public, must-revalidate"
+
+
+ # Keep-alive
+ Header set Connection keep-alive
+
+
+# HTTP/2 Server Push
+
+
+ Header add Link "; rel=preload; as=style"
+ Header add Link "; rel=preload; as=image"
+ Header add Link "; rel=preload; as=script"
+
+
+
+# ETags
+FileETag None
+Header unset ETag
+
+# Disable directory browsing
+Options -Indexes
+
+# Prevent access to logs and database directories
+
+ RewriteEngine On
+
# Skip already processed .php files
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^services/.*\.php$ - [L]
- # Explicitly allow existing service pages (skip redirects)
+ # Explicitly allow existing service pages
RewriteRule ^services/competitive-intelligence/?$ /services/competitive-intelligence.php [L]
RewriteRule ^services/data-cleaning/?$ /services/data-cleaning.php [L]
RewriteRule ^services/financial-data-services/?$ /services/financial-data-services.php [L]
@@ -153,19 +157,19 @@ Options -Indexes
# Redirect unknown service pages to project-types
RewriteRule ^services/(.+)$ /project-types [R=301,L]
-
- # Clean URL rewriting - remove .php extension
- RewriteCond %{REQUEST_FILENAME} !-d
- RewriteCond %{REQUEST_FILENAME} !-f
- RewriteCond %{REQUEST_FILENAME}.php -f
- RewriteRule ^(.+?)/?$ $1.php [L]
-
- # Security rules
- RewriteRule ^logs(/.*)?$ - [F,L]
- RewriteRule ^database(/.*)?$ - [F,L]
- RewriteRule ^\.git(/.*)?$ - [F,L]
- RewriteRule ^docker(/.*)?$ - [F,L]
-
-
-# Disable server signature
-ServerSignature Off
\ No newline at end of file
+
+ # Clean URL rewriting - remove .php extension
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME}.php -f
+ RewriteRule ^(.+?)/?$ $1.php [L]
+
+ # Security rules
+ RewriteRule ^logs(/.*)?$ - [F,L]
+ RewriteRule ^database(/.*)?$ - [F,L]
+ RewriteRule ^\.git(/.*)?$ - [F,L]
+ RewriteRule ^docker(/.*)?$ - [F,L]
+
+
+# Disable server signature
+ServerSignature Off
diff --git a/backups/.htaccess.pre-csrf-fix b/backups/.htaccess.pre-csrf-fix
new file mode 100644
index 0000000..8e88e9f
--- /dev/null
+++ b/backups/.htaccess.pre-csrf-fix
@@ -0,0 +1,171 @@
+# Security Rules for UK Data Services
+
+# Protect sensitive files and configs
+
+ Require all denied
+
+
+# Protect contact handlers from direct browser access (POST only)
+
+
+ Require all denied
+
+
+
+
+
+ Require all denied
+
+
+
+# Security headers
+
+ Header always set X-Content-Type-Options "nosniff"
+ Header always set X-Frame-Options "SAMEORIGIN"
+ Header always set X-XSS-Protection "1; mode=block"
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
+
+
+# Enhanced Gzip compression
+
+ # Enable compression for all text-based files
+ AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
+ AddOutputFilterByType DEFLATE application/javascript application/x-javascript
+ AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml
+ AddOutputFilterByType DEFLATE application/json application/ld+json
+ AddOutputFilterByType DEFLATE image/svg+xml
+ AddOutputFilterByType DEFLATE font/ttf font/otf font/eot font/woff font/woff2
+
+ # Remove browser bugs for older browsers
+ BrowserMatch ^Mozilla/4 gzip-only-text/html
+ BrowserMatch ^Mozilla/4\.0[678] no-gzip
+ BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
+ Header append Vary User-Agent
+
+
+# Enable Brotli compression if available
+
+ AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript
+ AddOutputFilterByType BROTLI_COMPRESS application/javascript application/x-javascript
+ AddOutputFilterByType BROTLI_COMPRESS application/xml application/xhtml+xml application/rss+xml
+ AddOutputFilterByType BROTLI_COMPRESS application/json application/ld+json
+ AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
+ AddOutputFilterByType BROTLI_COMPRESS font/ttf font/otf font/woff font/woff2
+
+
+# Browser Caching Headers
+
+ ExpiresActive On
+
+ # Images - 1 year
+ ExpiresByType image/jpeg "access plus 1 year"
+ ExpiresByType image/jpg "access plus 1 year"
+ ExpiresByType image/gif "access plus 1 year"
+ ExpiresByType image/png "access plus 1 year"
+ ExpiresByType image/webp "access plus 1 year"
+ ExpiresByType image/svg+xml "access plus 1 year"
+ ExpiresByType image/x-icon "access plus 1 year"
+ ExpiresByType image/ico "access plus 1 year"
+
+ # Fonts - 1 year
+ ExpiresByType font/ttf "access plus 1 year"
+ ExpiresByType font/otf "access plus 1 year"
+ ExpiresByType font/woff "access plus 1 year"
+ ExpiresByType font/woff2 "access plus 1 year"
+ ExpiresByType application/font-woff "access plus 1 year"
+ ExpiresByType application/font-woff2 "access plus 1 year"
+
+ # CSS and JavaScript - 1 month
+ ExpiresByType text/css "access plus 1 month"
+ ExpiresByType application/javascript "access plus 1 month"
+ ExpiresByType text/javascript "access plus 1 month"
+ ExpiresByType application/x-javascript "access plus 1 month"
+
+ # HTML and PHP - 1 hour
+ ExpiresByType text/html "access plus 1 hour"
+ ExpiresByType application/xhtml+xml "access plus 1 hour"
+
+ # Data - no cache
+ ExpiresByType application/json "access plus 0 seconds"
+ ExpiresByType application/xml "access plus 0 seconds"
+ ExpiresByType text/xml "access plus 0 seconds"
+
+ # Default - 1 week
+ ExpiresDefault "access plus 1 week"
+
+
+# Cache-Control Headers
+
+ # Static assets - 1 year
+
+ Header set Cache-Control "max-age=31536000, public, immutable"
+
+
+ # CSS and JS - 1 month
+
+ Header set Cache-Control "max-age=2592000, public"
+
+
+ # HTML/PHP - 1 hour
+
+ Header set Cache-Control "max-age=3600, public, must-revalidate"
+
+
+ # Keep-alive
+ Header set Connection keep-alive
+
+
+# HTTP/2 Server Push
+
+ # Push critical resources
+
+ Header add Link "; rel=preload; as=style"
+ Header add Link "; rel=preload; as=image"
+ Header add Link "; rel=preload; as=script"
+
+
+
+# ETags
+FileETag None
+Header unset ETag
+
+# Disable directory browsing
+Options -Indexes
+
+# Prevent access to logs and database directories
+
+ RewriteEngine On
+
+ # Skip already processed .php files
+ RewriteCond %{REQUEST_FILENAME} -f
+ RewriteRule ^services/.*\.php$ - [L]
+
+ # Explicitly allow existing service pages (skip redirects)
+ RewriteRule ^services/competitive-intelligence/?$ /services/competitive-intelligence.php [L]
+ RewriteRule ^services/data-cleaning/?$ /services/data-cleaning.php [L]
+ RewriteRule ^services/financial-data-services/?$ /services/financial-data-services.php [L]
+ RewriteRule ^services/price-monitoring/?$ /services/price-monitoring.php [L]
+ RewriteRule ^services/property-data-extraction/?$ /services/property-data-extraction.php [L]
+ RewriteRule ^services/web-scraping/?$ /services/web-scraping.php [L]
+
+ # Redirect /services index to project-types
+ RewriteRule ^services/?$ /project-types [R=301,L]
+
+ # Redirect unknown service pages to project-types
+ RewriteRule ^services/(.+)$ /project-types [R=301,L]
+
+ # Clean URL rewriting - remove .php extension
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME}.php -f
+ RewriteRule ^(.+?)/?$ $1.php [L]
+
+ # Security rules
+ RewriteRule ^logs(/.*)?$ - [F,L]
+ RewriteRule ^database(/.*)?$ - [F,L]
+ RewriteRule ^\.git(/.*)?$ - [F,L]
+ RewriteRule ^docker(/.*)?$ - [F,L]
+
+
+# Disable server signature
+ServerSignature Off
\ No newline at end of file
diff --git a/index.php b/index.php
index 3846086..7ae5c3f 100644
--- a/index.php
+++ b/index.php
@@ -5,6 +5,11 @@ ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
session_start();
+
+// Prevent caching - page contains session-specific tokens
+header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
+header("Pragma: no-cache");
+header("Expires: Sat, 01 Jan 2000 00:00:00 GMT");
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
diff --git a/quote-handler.php b/quote-handler.php
index c5f753d..50edf3e 100644
--- a/quote-handler.php
+++ b/quote-handler.php
@@ -1,26 +1,38 @@
= 3) {
- return false;
- }
-
- return true;
+ return $data['count'] < 5; // Allow 5 submissions per hour
}
// Input validation
@@ -55,12 +63,8 @@ function validateInput($data, $type = 'text') {
switch ($type) {
case 'email':
return filter_var($data, FILTER_VALIDATE_EMAIL) ? $data : false;
- case 'phone':
- return preg_match('/^[\+]?[0-9\s\-\(\)]+$/', $data) ? $data : false;
case 'text':
- return strlen($data) > 0 ? $data : false;
- case 'long_text':
- return strlen($data) >= 20 ? $data : false;
+ return strlen($data) > 0 ? $data : '';
default:
return $data;
}
@@ -73,260 +77,24 @@ function isAjaxRequest() {
}
// Response function
-function sendResponse($success, $message, $data = null) {
- // If AJAX request, send JSON
+function sendResponse($success, $message) {
if (isAjaxRequest()) {
- $response = [
- 'success' => $success,
- 'message' => $message
- ];
-
- if ($data !== null) {
- $response['data'] = $data;
- }
-
header('Content-Type: application/json');
- echo json_encode($response);
+ echo json_encode(['success' => $success, 'message' => $message]);
exit;
}
- // Otherwise, display HTML response page
- displayHTMLResponse($success, $message);
-}
-
-// Display HTML response page
-function displayHTMLResponse($success, $message) {
- $pageTitle = $success ? 'Quote Request Sent Successfully' : 'Quote Submission Error';
- $messageClass = $success ? 'success' : 'error';
- ?>
-
-
-
-
-
- - UK Data Services
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-
- ';
- foreach ($errors as $error) {
- if (trim($error)) {
- echo '
' . htmlspecialchars(trim($error)) . '';
- }
- }
- echo '';
- } else {
- echo htmlspecialchars($message);
- }
- }
- ?>
-
-
-
-
-
-
-
-
- $title
+
+ ";
exit;
}
@@ -335,386 +103,197 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendResponse(false, 'Invalid request method');
}
-// Validate CSRF token
-if (!isset($_POST['csrf_token'])) {
+logDebug("Form submission from " . $_SERVER['REMOTE_ADDR'] . " - Session ID: " . session_id());
+
+// CSRF Validation
+$csrfToken = $_POST['csrf_token'] ?? '';
+if (empty($csrfToken)) {
+ logDebug("CSRF: No token in POST data");
sendResponse(false, 'Security validation failed. Please refresh the page and try again.');
}
-if (!validateCSRFToken($_POST['csrf_token'])) {
+if (!validateCSRFToken($csrfToken)) {
sendResponse(false, 'Security validation failed. Please refresh the page and try again.');
}
-// Check for blocked IPs
-function checkBlockedIP() {
- $ip = $_SERVER['REMOTE_ADDR'];
- $blockFile = 'logs/blocked-ips.txt';
-
- if (file_exists($blockFile)) {
- $blockedIPs = file($blockFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
- foreach ($blockedIPs as $blockedEntry) {
- $parts = explode('|', $blockedEntry);
- if (isset($parts[0]) && $parts[0] === $ip) {
- $blockTime = isset($parts[1]) ? (int)$parts[1] : 0;
- // Block for 24 hours
- if (time() - $blockTime < 86400) {
- return false;
- }
- }
- }
- }
- return true;
-}
+logDebug("CSRF validation passed");
-// Check for blocked IPs first
-if (!checkBlockedIP()) {
- sendResponse(false, 'Access temporarily restricted');
-}
-
-// Check rate limiting
+// Rate limiting
if (!checkRateLimit()) {
+ logDebug("Rate limit exceeded for " . $_SERVER['REMOTE_ADDR']);
sendResponse(false, 'Too many requests. Please try again later.');
}
-// reCAPTCHA Verification
-require_once '.recaptcha-config.php';
-
-function validateRecaptcha($token) {
- if (!RECAPTCHA_ENABLED) {
- // Skip validation if reCAPTCHA is disabled (test keys)
- error_log("reCAPTCHA validation skipped - test keys in use");
- return true;
+// reCAPTCHA (if enabled)
+require_once __DIR__ . '/.recaptcha-config.php';
+if (RECAPTCHA_ENABLED) {
+ $recaptchaResponse = $_POST['recaptcha_response'] ?? '';
+ if (empty($recaptchaResponse)) {
+ sendResponse(false, 'Security verification failed. Please try again.');
}
- if (empty($token)) {
- return false;
- }
-
- $secretKey = RECAPTCHA_SECRET_KEY;
- $verifyURL = 'https://www.google.com/recaptcha/api/siteverify';
-
- $data = [
- 'secret' => $secretKey,
- 'response' => $token,
+ $verifyData = [
+ 'secret' => RECAPTCHA_SECRET_KEY,
+ 'response' => $recaptchaResponse,
'remoteip' => $_SERVER['REMOTE_ADDR']
];
- $options = [
- 'http' => [
- 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ $result = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', false,
+ stream_context_create(['http' => [
'method' => 'POST',
- 'content' => http_build_query($data)
- ]
- ];
+ 'header' => 'Content-type: application/x-www-form-urlencoded',
+ 'content' => http_build_query($verifyData)
+ ]]));
- $context = stream_context_create($options);
- $result = file_get_contents($verifyURL, false, $context);
-
- if ($result === false) {
- error_log('reCAPTCHA verification request failed');
- return false;
+ if ($result) {
+ $resultJson = json_decode($result, true);
+ if (!$resultJson['success'] || ($resultJson['score'] ?? 1) < RECAPTCHA_THRESHOLD) {
+ logDebug("reCAPTCHA failed: " . print_r($resultJson, true));
+ sendResponse(false, 'Security verification failed. Please try again.');
+ }
}
-
- $resultJson = json_decode($result, true);
-
- if ($resultJson['success'] && isset($resultJson['score'])) {
- return $resultJson['score'] >= RECAPTCHA_THRESHOLD;
- }
-
- return false;
}
-// Verify reCAPTCHA
-$recaptchaResponse = $_POST['recaptcha_response'] ?? '';
-if (!validateRecaptcha($recaptchaResponse)) {
- sendResponse(false, 'Security verification failed. Please try again.');
-}
-
-// Spam protection - honeypot field
-if (isset($_POST['website']) && !empty($_POST['website'])) {
+// Honeypot check
+if (!empty($_POST['website'])) {
+ logDebug("Honeypot triggered");
sendResponse(false, 'Spam detected');
}
-// Validate and sanitize inputs
-$services = $_POST['services'] ?? [];
-$project_scale = validateInput($_POST['project_scale'] ?? '', 'text');
-$timeline = validateInput($_POST['timeline'] ?? '', 'text');
-$name = validateInput($_POST['name'] ?? '', 'text');
-$email = validateInput($_POST['email'] ?? '', 'email');
-$company = validateInput($_POST['company'] ?? '', 'text');
-$phone = validateInput($_POST['phone'] ?? '', 'phone');
-$data_sources = validateInput($_POST['data_sources'] ?? '', 'text');
-$requirements = validateInput($_POST['requirements'] ?? '', 'long_text');
-$budget = validateInput($_POST['budget'] ?? '', 'text');
-
-// Validation
-$errors = [];
-
-if (empty($services) || !is_array($services)) {
- $errors[] = 'Please select at least one service';
-}
-
-if (!$project_scale) {
- $errors[] = 'Please select a project scale';
-}
-
-if (!$timeline) {
- $errors[] = 'Please select a timeline';
-}
-
-if (!$name || strlen($name) < 2) {
- $errors[] = 'Please enter a valid name';
-}
-
-if (!$email) {
- $errors[] = 'Please enter a valid email address';
-}
-
-if (!$requirements) {
- $errors[] = 'Please provide detailed project requirements';
-}
-
-if (!empty($errors)) {
- sendResponse(false, implode('. ', $errors));
-}
-
-// Enhanced spam protection - content filtering
-$spamKeywords = [
- 'viagra', 'cialis', 'casino', 'lottery', 'bitcoin', 'forex', 'loan', 'debt',
- 'pharmacy', 'click here', 'act now', 'limited time', 'risk free', 'guarantee',
- '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'
-];
-
-$contentToCheck = strtolower($requirements . ' ' . $name . ' ' . $company . ' ' . $data_sources);
-foreach ($spamKeywords as $keyword) {
- if (strpos($contentToCheck, $keyword) !== false) {
- sendResponse(false, 'Invalid content detected');
- }
-}
-
-// Bot detection - check for suspicious patterns
+// Bot detection - user agent check
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
-$suspiciousAgents = ['curl', 'wget', 'python', 'bot', 'crawler', 'spider', 'scraper'];
+$suspiciousAgents = ['curl', 'wget', 'python-requests', 'scrapy'];
foreach ($suspiciousAgents as $agent) {
if (stripos($userAgent, $agent) !== false) {
+ logDebug("Suspicious user agent: $userAgent");
sendResponse(false, 'Automated submissions not allowed');
}
}
-// Check submission speed (too fast = likely bot) - More lenient timing
-if (isset($_SESSION['form_start_time'])) {
- $submissionTime = time() - $_SESSION['form_start_time'];
- if ($submissionTime < 3) { // Only block if under 3 seconds (very aggressive bots)
- sendResponse(false, 'Form submitted too quickly');
- }
+// Extract and validate form data
+$service_type = validateInput($_POST['service_type'] ?? '');
+$scale = validateInput($_POST['scale'] ?? '');
+$name = validateInput($_POST['name'] ?? '');
+$email = validateInput($_POST['email'] ?? '', 'email');
+$company = validateInput($_POST['company'] ?? '');
+$timeline = validateInput($_POST['timeline'] ?? '');
+$data_sources = validateInput($_POST['data_sources'] ?? '');
+$requirements = validateInput($_POST['requirements'] ?? '');
+
+// Validation
+$errors = [];
+if (empty($service_type)) $errors[] = 'Please select a service';
+if (empty($scale)) $errors[] = 'Please select a scale';
+if (empty($name) || strlen($name) < 2) $errors[] = 'Please enter a valid name';
+if (!$email) $errors[] = 'Please enter a valid email address';
+if (empty($timeline)) $errors[] = 'Please select a timeline';
+
+if (!empty($errors)) {
+ logDebug("Validation errors: " . implode(', ', $errors));
+ sendResponse(false, implode('. ', $errors));
}
-// Sanitize services array
-$services = array_map(function($service) {
- return htmlspecialchars(trim($service), ENT_QUOTES, 'UTF-8');
-}, $services);
+// Spam content check
+$spamKeywords = ['viagra', 'cialis', 'casino', 'lottery', 'bitcoin', 'forex', 'pharmacy', 'click here', 'act now'];
+$contentToCheck = strtolower($requirements . ' ' . $name . ' ' . $company . ' ' . $data_sources);
+foreach ($spamKeywords as $keyword) {
+ if (strpos($contentToCheck, $keyword) !== false) {
+ logDebug("Spam keyword detected: $keyword");
+ sendResponse(false, 'Invalid content detected');
+ }
+}
// Update rate limit counter
$ip = $_SERVER['REMOTE_ADDR'];
$key = 'quote_' . md5($ip);
$_SESSION[$key]['count']++;
-// Create friendly service names
-$service_names = [
- 'web-scraping' => 'Web Scraping & Data Extraction',
- 'business-intelligence' => 'Business Intelligence & Analytics',
- 'data-processing' => 'Data Processing & Cleaning',
- 'automation' => 'Automation & APIs',
- 'consulting' => 'Custom Development',
- 'other' => 'Other Services'
+// Prepare friendly labels
+$serviceLabels = [
+ 'web-scraping' => 'Web Scraping',
+ 'data-cleaning' => 'Data Cleaning',
+ 'api-development' => 'API Development',
+ 'automation' => 'Automation',
+ 'custom' => 'Custom Solution',
+ 'other' => 'Other'
];
-$selected_services = array_map(function($service) use ($service_names) {
- return $service_names[$service] ?? $service;
-}, $services);
-
-// Create friendly scale names
-$scale_names = [
- 'small' => 'Small Project (One-time extraction, < 10k records)',
- 'medium' => 'Medium Project (Regular updates, 10k-100k records)',
- 'large' => 'Large Project (Ongoing service, 100k+ records)',
- 'enterprise' => 'Enterprise (Complex multi-source solution)'
+$scaleLabels = [
+ 'small' => 'Small (under 1,000 records)',
+ 'medium' => 'Medium (1,000 - 50,000 records)',
+ 'large' => 'Large (50,000 - 500,000 records)',
+ 'enterprise' => 'Enterprise (500,000+ records)',
+ 'unsure' => 'Not sure yet'
];
-$friendly_scale = $scale_names[$project_scale] ?? $project_scale;
-
-// Create friendly timeline names
-$timeline_names = [
- 'asap' => 'ASAP (Rush job)',
- '1-week' => 'Within 1 week',
- '2-4-weeks' => '2-4 weeks',
- 'flexible' => 'Flexible timeline'
+$timelineLabels = [
+ 'asap' => 'ASAP',
+ '2weeks' => 'Within 2 weeks',
+ '1month' => 'Within a month',
+ 'flexible' => 'Flexible / No rush'
];
-$friendly_timeline = $timeline_names[$timeline] ?? $timeline;
-
-// Prepare email content
+// Build email
$to = 'info@ukdataservices.co.uk';
-$subject = 'New Quote Request - UK Data Services';
+$subject = 'New Quote Request - ' . ($serviceLabels[$service_type] ?? $service_type);
-// Create detailed HTML email
-$emailHTML = '
-
-
-
-
- New Quote Request
-
-
-
-
-
-
-
-
+$emailHTML = '
+
+
+
New Quote Request
+
UK Data Services
+
+
-
-
🎯 Services Required
-
';
+
+
Contact Details
+
Name: ' . $name . '
+
Email: ' . $email . '
+
Company: ' . ($company ?: 'Not provided') . '
+
-foreach ($selected_services as $service) {
- $emailHTML .= '- ✓ ' . htmlspecialchars($service) . '
';
+
+
Project Details
+
Service: ' . ($serviceLabels[$service_type] ?? $service_type) . '
+
Scale: ' . ($scaleLabels[$scale] ?? $scale) . '
+
Timeline: ' . ($timelineLabels[$timeline] ?? $timeline) . '
+
Target Sources: ' . ($data_sources ?: 'Not specified') . '
+
';
+
+if (!empty($requirements)) {
+ $emailHTML .= '
+
Requirements
+
' . nl2br($requirements) . '
+
';
}
-$emailHTML .= '
-
+$emailHTML .= '
+
Submitted: ' . date('Y-m-d H:i:s') . ' UTC
+
IP: ' . $_SERVER['REMOTE_ADDR'] . '
+
-
-
📊 Project Details
-
-
Project Scale:
-
' . htmlspecialchars($friendly_scale) . '
-
-
-
Timeline:
-
' . htmlspecialchars($friendly_timeline) . '
-
-
-
Budget Range:
-
' . htmlspecialchars($budget ?: 'Not specified') . '
-
-
+
';
-
-
🌐 Data Sources
-
' . nl2br(htmlspecialchars($data_sources ?: 'Not specified')) . '
-
-
-
-
📝 Detailed Requirements
-
' . nl2br(htmlspecialchars($requirements)) . '
-
-
-
-
🔍 Submission Details
-
-
IP Address:
-
' . htmlspecialchars($_SERVER['REMOTE_ADDR']) . '
-
-
-
User Agent:
-
' . htmlspecialchars($_SERVER['HTTP_USER_AGENT']) . '
-
-
-
Referrer:
-
' . htmlspecialchars($_SERVER['HTTP_REFERER'] ?? 'Direct') . '
-
-
-
-
-
-';
-
-// Email headers
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
-$headers .= "From: \"UK Data Services Quote System\" \r\n";
+$headers .= "From: \"UK Data Services\" \r\n";
$headers .= "Reply-To: " . $email . "\r\n";
-$headers .= "X-Mailer: PHP/" . phpversion() . "\r\n";
$headers .= "X-Priority: " . ($timeline === 'asap' ? '1' : '3') . "\r\n";
-// Create logs directory if it doesn't exist
-if (!file_exists('logs')) {
- mkdir('logs', 0755, true);
-}
-
// Send email
-try {
- // Clear any previous errors
- error_clear_last();
+$emailSent = @mail($to, $subject, $emailHTML, $headers);
+
+if ($emailSent) {
+ logDebug("SUCCESS: Quote from $email ($name) - Service: $service_type");
- $emailSent = mail($to, $subject, $emailHTML, $headers);
+ // Log to file
+ $logEntry = date('Y-m-d H:i:s') . " | $name | $email | $service_type | $scale | $timeline\n";
+ file_put_contents($logDir . '/quote-requests.log', $logEntry, FILE_APPEND | LOCK_EX);
- if ($emailSent) {
- // Log successful submission
- $logEntry = date('Y-m-d H:i:s') . " - Quote request from " . $email . " (" . $_SERVER['REMOTE_ADDR'] . ") - Services: " . implode(', ', $services) . "\n";
- file_put_contents('logs/quote-requests.log', $logEntry, FILE_APPEND | LOCK_EX);
-
- sendResponse(true, 'Thank you for your quote request! We will send you a detailed proposal within 24 hours.');
- } else {
- // Get detailed error information
- $lastError = error_get_last();
- $errorMsg = $lastError ? $lastError['message'] : 'Unknown mail error';
-
- // Log failed email with detailed error
- $logEntry = date('Y-m-d H:i:s') . " - FAILED quote request from " . $email . " (" . $_SERVER['REMOTE_ADDR'] . ") - Error: " . $errorMsg . "\n";
- file_put_contents('logs/quote-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
-
- // Check common issues
- if (strpos($errorMsg, 'sendmail') !== false) {
- error_log("Mail server configuration issue: " . $errorMsg);
- }
-
- sendResponse(false, 'There was an error sending your quote request. Please try again or contact us directly at info@ukdataservices.co.uk');
- }
-} catch (Exception $e) {
- // Log exception with full details
- $logEntry = date('Y-m-d H:i:s') . " - EXCEPTION: " . $e->getMessage() . " from " . $email . " (" . $_SERVER['REMOTE_ADDR'] . ") - File: " . $e->getFile() . " Line: " . $e->getLine() . "\n";
- file_put_contents('logs/quote-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
-
- sendResponse(false, 'There was an error processing your quote request. Please contact us directly at info@ukdataservices.co.uk');
+ sendResponse(true, 'Thank you for your quote request! We will send you a detailed proposal within 24 hours.');
+} else {
+ $error = error_get_last();
+ logDebug("FAILED: Email send failed - " . ($error['message'] ?? 'Unknown error'));
+ sendResponse(false, 'There was an error sending your request. Please try again or contact us at info@ukdataservices.co.uk');
}
-?>
\ No newline at end of file
+?>
diff --git a/quote.php b/quote.php
index 0d9f8d3..68263fb 100644
--- a/quote.php
+++ b/quote.php
@@ -197,13 +197,13 @@ $breadcrumbs = [