Fix quote page gap: wrap noscript fallback, reduce hero/form padding

This commit is contained in:
root
2026-02-04 03:17:55 +00:00
parent b0234123a3
commit 3a0d8034c7
5 changed files with 521 additions and 762 deletions

314
.htaccess
View File

@@ -1,146 +1,150 @@
# Security Rules for UK Data Services # Security Rules for UK Data Services
# Protect sensitive files and configs # Protect sensitive files and configs
<FilesMatch "^\.(.*)$|\.log$|\.sql$|\.conf$|config\.php$|\.email-config\.php$|\.htaccess|\.htpasswd|\.ini|\.sh|\.inc|\.bak$"> <FilesMatch "^\.(.*)$|\.log$|\.sql$|\.conf$|config\.php$|\.email-config\.php$|\.htaccess|\.htpasswd|\.ini|\.sh|\.inc|\.bak$">
Require all denied Require all denied
</FilesMatch> </FilesMatch>
# Protect contact handlers from direct browser access (POST only) # Protect contact handlers from direct browser access (POST only)
<Files "contact-handler.php"> <Files "contact-handler.php">
<LimitExcept POST> <LimitExcept POST>
Require all denied Require all denied
</LimitExcept> </LimitExcept>
</Files> </Files>
<Files "quote-handler.php"> <Files "quote-handler.php">
<LimitExcept POST> <LimitExcept POST>
Require all denied Require all denied
</LimitExcept> </LimitExcept>
</Files> </Files>
# Security headers # Security headers
<IfModule mod_headers.c> <IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff" Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN" Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block" Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# CRITICAL: No caching for form pages (contain session-specific CSRF tokens)
# Enhanced Gzip compression <FilesMatch "(quote|contact)\.php$">
<IfModule mod_deflate.c> Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
# Enable compression for all text-based files Header set Pragma "no-cache"
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript Header set Expires "Sat, 01 Jan 2000 00:00:00 GMT"
AddOutputFilterByType DEFLATE application/javascript application/x-javascript </FilesMatch>
AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml </IfModule>
AddOutputFilterByType DEFLATE application/json application/ld+json
AddOutputFilterByType DEFLATE image/svg+xml # Enhanced Gzip compression
AddOutputFilterByType DEFLATE font/ttf font/otf font/eot font/woff font/woff2 <IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
# Remove browser bugs for older browsers AddOutputFilterByType DEFLATE application/javascript application/x-javascript
BrowserMatch ^Mozilla/4 gzip-only-text/html AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml
BrowserMatch ^Mozilla/4\.0[678] no-gzip AddOutputFilterByType DEFLATE application/json application/ld+json
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html AddOutputFilterByType DEFLATE image/svg+xml
Header append Vary User-Agent AddOutputFilterByType DEFLATE font/ttf font/otf font/eot font/woff font/woff2
</IfModule>
BrowserMatch ^Mozilla/4 gzip-only-text/html
# Enable Brotli compression if available BrowserMatch ^Mozilla/4\.0[678] no-gzip
<IfModule mod_brotli.c> BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript Header append Vary User-Agent
AddOutputFilterByType BROTLI_COMPRESS application/javascript application/x-javascript </IfModule>
AddOutputFilterByType BROTLI_COMPRESS application/xml application/xhtml+xml application/rss+xml
AddOutputFilterByType BROTLI_COMPRESS application/json application/ld+json # Enable Brotli compression if available
AddOutputFilterByType BROTLI_COMPRESS image/svg+xml <IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS font/ttf font/otf font/woff font/woff2 AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript
</IfModule> AddOutputFilterByType BROTLI_COMPRESS application/javascript application/x-javascript
AddOutputFilterByType BROTLI_COMPRESS application/xml application/xhtml+xml application/rss+xml
# Browser Caching Headers AddOutputFilterByType BROTLI_COMPRESS application/json application/ld+json
<IfModule mod_expires.c> AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
ExpiresActive On AddOutputFilterByType BROTLI_COMPRESS font/ttf font/otf font/woff font/woff2
</IfModule>
# Images - 1 year
ExpiresByType image/jpeg "access plus 1 year" # Browser Caching Headers
ExpiresByType image/jpg "access plus 1 year" <IfModule mod_expires.c>
ExpiresByType image/gif "access plus 1 year" ExpiresActive On
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year" # Images - 1 year
ExpiresByType image/svg+xml "access plus 1 year" ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year" ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/ico "access plus 1 year" ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
# Fonts - 1 year ExpiresByType image/webp "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year" ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType font/otf "access plus 1 year" ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff "access plus 1 year" ExpiresByType image/ico "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/font-woff "access plus 1 year" # Fonts - 1 year
ExpiresByType application/font-woff2 "access plus 1 year" ExpiresByType font/ttf "access plus 1 year"
ExpiresByType font/otf "access plus 1 year"
# CSS and JavaScript - 1 month ExpiresByType font/woff "access plus 1 year"
ExpiresByType text/css "access plus 1 month" ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/javascript "access plus 1 month" ExpiresByType application/font-woff "access plus 1 year"
ExpiresByType text/javascript "access plus 1 month" ExpiresByType application/font-woff2 "access plus 1 year"
ExpiresByType application/x-javascript "access plus 1 month"
# CSS and JavaScript - 1 month
# HTML and PHP - 1 hour ExpiresByType text/css "access plus 1 month"
ExpiresByType text/html "access plus 1 hour" ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/xhtml+xml "access plus 1 hour" ExpiresByType text/javascript "access plus 1 month"
ExpiresByType application/x-javascript "access plus 1 month"
# Data - no cache
ExpiresByType application/json "access plus 0 seconds" # HTML and PHP - 1 hour
ExpiresByType application/xml "access plus 0 seconds" ExpiresByType text/html "access plus 1 hour"
ExpiresByType text/xml "access plus 0 seconds" ExpiresByType application/xhtml+xml "access plus 1 hour"
# Default - 1 week # Data - no cache
ExpiresDefault "access plus 1 week" ExpiresByType application/json "access plus 0 seconds"
</IfModule> ExpiresByType application/xml "access plus 0 seconds"
ExpiresByType text/xml "access plus 0 seconds"
# Cache-Control Headers
<IfModule mod_headers.c> # Default - 1 week
# Static assets - 1 year ExpiresDefault "access plus 1 week"
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|otf|eot)$"> </IfModule>
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch> # Cache-Control Headers
<IfModule mod_headers.c>
# CSS and JS - 1 month # Static assets - 1 year
<FilesMatch "\.(css|js)$"> <FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|otf|eot)$">
Header set Cache-Control "max-age=2592000, public" Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch> </FilesMatch>
# HTML/PHP - 1 hour # CSS and JS - 1 month
<FilesMatch "\.(html|php)$"> <FilesMatch "\.(css|js)$">
Header set Cache-Control "max-age=3600, public, must-revalidate" Header set Cache-Control "max-age=2592000, public"
</FilesMatch> </FilesMatch>
# Keep-alive # Regular HTML/PHP - 1 hour (but form pages are excluded above)
Header set Connection keep-alive <FilesMatch "\.(html)$">
</IfModule> Header set Cache-Control "max-age=3600, public, must-revalidate"
</FilesMatch>
# HTTP/2 Server Push
<IfModule mod_http2.c> # Keep-alive
# Push critical resources Header set Connection keep-alive
<FilesMatch "index\.php"> </IfModule>
Header add Link "</assets/css/main.min.css>; rel=preload; as=style"
Header add Link "</assets/images/ukds-main-logo.webp>; rel=preload; as=image" # HTTP/2 Server Push
Header add Link "</assets/js/main.min.js>; rel=preload; as=script" <IfModule mod_http2.c>
</FilesMatch> <FilesMatch "index\.php">
</IfModule> Header add Link "</assets/css/main.min.css>; rel=preload; as=style"
Header add Link "</assets/images/ukds-main-logo.webp>; rel=preload; as=image"
# ETags Header add Link "</assets/js/main.min.js>; rel=preload; as=script"
FileETag None </FilesMatch>
Header unset ETag </IfModule>
# Disable directory browsing # ETags
Options -Indexes FileETag None
Header unset ETag
# Prevent access to logs and database directories
<IfModule mod_rewrite.c> # Disable directory browsing
RewriteEngine On Options -Indexes
# Prevent access to logs and database directories
<IfModule mod_rewrite.c>
RewriteEngine On
# Skip already processed .php files # Skip already processed .php files
RewriteCond %{REQUEST_FILENAME} -f RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^services/.*\.php$ - [L] 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/competitive-intelligence/?$ /services/competitive-intelligence.php [L]
RewriteRule ^services/data-cleaning/?$ /services/data-cleaning.php [L] RewriteRule ^services/data-cleaning/?$ /services/data-cleaning.php [L]
RewriteRule ^services/financial-data-services/?$ /services/financial-data-services.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 # Redirect unknown service pages to project-types
RewriteRule ^services/(.+)$ /project-types [R=301,L] RewriteRule ^services/(.+)$ /project-types [R=301,L]
# Clean URL rewriting - remove .php extension # Clean URL rewriting - remove .php extension
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME}.php -f RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+?)/?$ $1.php [L] RewriteRule ^(.+?)/?$ $1.php [L]
# Security rules # Security rules
RewriteRule ^logs(/.*)?$ - [F,L] RewriteRule ^logs(/.*)?$ - [F,L]
RewriteRule ^database(/.*)?$ - [F,L] RewriteRule ^database(/.*)?$ - [F,L]
RewriteRule ^\.git(/.*)?$ - [F,L] RewriteRule ^\.git(/.*)?$ - [F,L]
RewriteRule ^docker(/.*)?$ - [F,L] RewriteRule ^docker(/.*)?$ - [F,L]
</IfModule> </IfModule>
# Disable server signature # Disable server signature
ServerSignature Off ServerSignature Off

View File

@@ -0,0 +1,171 @@
# Security Rules for UK Data Services
# Protect sensitive files and configs
<FilesMatch "^\.(.*)$|\.log$|\.sql$|\.conf$|config\.php$|\.email-config\.php$|\.htaccess|\.htpasswd|\.ini|\.sh|\.inc|\.bak$">
Require all denied
</FilesMatch>
# Protect contact handlers from direct browser access (POST only)
<Files "contact-handler.php">
<LimitExcept POST>
Require all denied
</LimitExcept>
</Files>
<Files "quote-handler.php">
<LimitExcept POST>
Require all denied
</LimitExcept>
</Files>
# Security headers
<IfModule mod_headers.c>
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"
</IfModule>
# Enhanced Gzip compression
<IfModule mod_deflate.c>
# 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
</IfModule>
# Enable Brotli compression if available
<IfModule mod_brotli.c>
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
</IfModule>
# Browser Caching Headers
<IfModule mod_expires.c>
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"
</IfModule>
# Cache-Control Headers
<IfModule mod_headers.c>
# Static assets - 1 year
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|otf|eot)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>
# CSS and JS - 1 month
<FilesMatch "\.(css|js)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>
# HTML/PHP - 1 hour
<FilesMatch "\.(html|php)$">
Header set Cache-Control "max-age=3600, public, must-revalidate"
</FilesMatch>
# Keep-alive
Header set Connection keep-alive
</IfModule>
# HTTP/2 Server Push
<IfModule mod_http2.c>
# Push critical resources
<FilesMatch "index\.php">
Header add Link "</assets/css/main.min.css>; rel=preload; as=style"
Header add Link "</assets/images/ukds-main-logo.webp>; rel=preload; as=image"
Header add Link "</assets/js/main.min.js>; rel=preload; as=script"
</FilesMatch>
</IfModule>
# ETags
FileETag None
Header unset ETag
# Disable directory browsing
Options -Indexes
# Prevent access to logs and database directories
<IfModule mod_rewrite.c>
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]
</IfModule>
# Disable server signature
ServerSignature Off

View File

@@ -5,6 +5,11 @@ ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_httponly', '1'); ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1'); ini_set('session.cookie_secure', '1');
session_start(); 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'])) { if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} }

View File

@@ -1,26 +1,38 @@
<?php <?php
// Quote Form Handler with Enhanced Security // Quote Form Handler with Enhanced Security
// Ensure session cookie is available for AJAX requests
ini_set('session.cookie_samesite', 'Lax'); ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_httponly', '1'); ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1'); // Set to '1' if using HTTPS ini_set('session.cookie_secure', '1');
session_start(); session_start();
// Logging
$logDir = __DIR__ . '/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
function logDebug($message) {
global $logDir;
$entry = date('Y-m-d H:i:s') . " - " . $message . "\n";
file_put_contents($logDir . '/quote-debug.log', $entry, FILE_APPEND | LOCK_EX);
}
// Security headers // Security headers
header('X-Content-Type-Options: nosniff'); header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY'); header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block'); header('X-XSS-Protection: 1; mode=block');
// CSRF Protection // CSRF Protection
function generateCSRFToken() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function validateCSRFToken($token) { function validateCSRFToken($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); if (!isset($_SESSION['csrf_token'])) {
logDebug("CSRF: No session token exists");
return false;
}
$match = hash_equals($_SESSION['csrf_token'], $token);
if (!$match) {
logDebug("CSRF: Token mismatch - Session: " . substr($_SESSION['csrf_token'], 0, 16) . "... POST: " . substr($token, 0, 16) . "...");
}
return $match;
} }
// Rate limiting // Rate limiting
@@ -39,11 +51,7 @@ function checkRateLimit() {
$data = $_SESSION[$key]; $data = $_SESSION[$key];
} }
if ($data['count'] >= 3) { return $data['count'] < 5; // Allow 5 submissions per hour
return false;
}
return true;
} }
// Input validation // Input validation
@@ -55,12 +63,8 @@ function validateInput($data, $type = 'text') {
switch ($type) { switch ($type) {
case 'email': case 'email':
return filter_var($data, FILTER_VALIDATE_EMAIL) ? $data : false; return filter_var($data, FILTER_VALIDATE_EMAIL) ? $data : false;
case 'phone':
return preg_match('/^[\+]?[0-9\s\-\(\)]+$/', $data) ? $data : false;
case 'text': case 'text':
return strlen($data) > 0 ? $data : false; return strlen($data) > 0 ? $data : '';
case 'long_text':
return strlen($data) >= 20 ? $data : false;
default: default:
return $data; return $data;
} }
@@ -73,260 +77,24 @@ function isAjaxRequest() {
} }
// Response function // Response function
function sendResponse($success, $message, $data = null) { function sendResponse($success, $message) {
// If AJAX request, send JSON
if (isAjaxRequest()) { if (isAjaxRequest()) {
$response = [
'success' => $success,
'message' => $message
];
if ($data !== null) {
$response['data'] = $data;
}
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($response); echo json_encode(['success' => $success, 'message' => $message]);
exit; exit;
} }
// Otherwise, display HTML response page // HTML fallback
displayHTMLResponse($success, $message); $title = $success ? 'Quote Request Sent' : 'Error';
} $class = $success ? 'success' : 'error';
echo "<!DOCTYPE html><html><head><title>$title</title>
// Display HTML response page <style>body{font-family:sans-serif;padding:40px;text-align:center;}
function displayHTMLResponse($success, $message) { .msg{padding:20px;border-radius:8px;max-width:500px;margin:0 auto;}
$pageTitle = $success ? 'Quote Request Sent Successfully' : 'Quote Submission Error'; .success{background:#d4edda;color:#155724;border:1px solid #c3e6cb;}
$messageClass = $success ? 'success' : 'error'; .error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;}
?> a{color:#007bff;}</style></head>
<!DOCTYPE html> <body><div class='msg $class'><h2>$title</h2><p>" . htmlspecialchars($message) . "</p>
<html lang="en"> <p><a href='/quote'>Back to form</a> | <a href='/'>Home</a></p></div></body></html>";
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($pageTitle); ?> - UK Data Services</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #144784 0%, #179e83 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.response-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
max-width: 600px;
width: 100%;
padding: 40px;
text-align: center;
animation: slideUp 0.4s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo {
margin-bottom: 30px;
}
.logo img {
height: 50px;
width: auto;
}
.icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
}
.icon.success {
background: rgba(23, 158, 131, 0.1);
color: #179e83;
}
.icon.error {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
h1 {
font-size: 28px;
margin-bottom: 15px;
color: #1a1a1a;
font-weight: 700;
}
.message {
font-size: 16px;
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
.message.error {
color: #721c24;
background: #fee;
padding: 15px;
border-radius: 8px;
border: 1px solid #fcc;
}
.message.success {
color: #155724;
background: #efe;
padding: 15px;
border-radius: 8px;
border: 1px solid #cfc;
}
.buttons {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
display: inline-block;
padding: 14px 32px;
border-radius: 50px;
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
cursor: pointer;
border: none;
font-size: 16px;
}
.btn-primary {
background: #179e83;
color: white;
box-shadow: 0 4px 14px rgba(23, 158, 131, 0.25);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(23, 158, 131, 0.35);
background: #15896f;
}
.btn-secondary {
background: transparent;
color: #144784;
border: 2px solid #144784;
}
.btn-secondary:hover {
background: #144784;
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(20, 71, 132, 0.25);
}
.contact-info {
margin-top: 30px;
padding-top: 30px;
border-top: 1px solid #e1e5e9;
font-size: 14px;
color: #666;
}
.contact-info a {
color: #179e83;
text-decoration: none;
font-weight: 600;
}
.contact-info a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="response-container">
<div class="logo">
<img src="/assets/images/ukds-main-logo.webp" alt="UK Data Services" onerror="this.onerror=null; this.src='/assets/images/ukds-main-logo.png';">
</div>
<div class="icon <?php echo $messageClass; ?>">
<?php echo $success ? '✓' : '✕'; ?>
</div>
<h1><?php echo $success ? 'Thank You!' : 'Oops! Something Went Wrong'; ?></h1>
<div class="message <?php echo $messageClass; ?>">
<?php
if ($success) {
echo htmlspecialchars($message);
} else {
// Parse multiple error messages if present
if (strpos($message, '. ') !== false) {
$errors = explode('. ', $message);
echo '<ul style="text-align: left; padding-left: 20px; list-style: disc;">';
foreach ($errors as $error) {
if (trim($error)) {
echo '<li>' . htmlspecialchars(trim($error)) . '</li>';
}
}
echo '</ul>';
} else {
echo htmlspecialchars($message);
}
}
?>
</div>
<div class="buttons">
<?php if ($success): ?>
<a href="/" class="btn btn-primary">Return to Home</a>
<a href="/blog/" class="btn btn-secondary">Read Our Blog</a>
<?php else: ?>
<button onclick="history.back()" class="btn btn-primary">Go Back & Try Again</button>
<a href="/" class="btn btn-secondary">Return to Home</a>
<?php endif; ?>
</div>
<div class="contact-info">
<?php if ($success): ?>
<p>We'll be in touch within 24 hours with a detailed proposal.</p>
<p>Have questions? Email us at <a href="mailto:info@ukdataservices.co.uk">info@ukdataservices.co.uk</a></p>
<?php else: ?>
<p>If you continue to experience issues, please contact us directly:</p>
<p>Email: <a href="mailto:info@ukdataservices.co.uk">info@ukdataservices.co.uk</a></p>
<p>Phone: +44 1692 689150</p>
<?php endif; ?>
</div>
</div>
</body>
</html>
<?php
exit; exit;
} }
@@ -335,386 +103,197 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendResponse(false, 'Invalid request method'); sendResponse(false, 'Invalid request method');
} }
// Validate CSRF token logDebug("Form submission from " . $_SERVER['REMOTE_ADDR'] . " - Session ID: " . session_id());
if (!isset($_POST['csrf_token'])) {
// 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.'); 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.'); sendResponse(false, 'Security validation failed. Please refresh the page and try again.');
} }
// Check for blocked IPs logDebug("CSRF validation passed");
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;
}
// Check for blocked IPs first // Rate limiting
if (!checkBlockedIP()) {
sendResponse(false, 'Access temporarily restricted');
}
// Check rate limiting
if (!checkRateLimit()) { if (!checkRateLimit()) {
logDebug("Rate limit exceeded for " . $_SERVER['REMOTE_ADDR']);
sendResponse(false, 'Too many requests. Please try again later.'); sendResponse(false, 'Too many requests. Please try again later.');
} }
// reCAPTCHA Verification // reCAPTCHA (if enabled)
require_once '.recaptcha-config.php'; require_once __DIR__ . '/.recaptcha-config.php';
if (RECAPTCHA_ENABLED) {
function validateRecaptcha($token) { $recaptchaResponse = $_POST['recaptcha_response'] ?? '';
if (!RECAPTCHA_ENABLED) { if (empty($recaptchaResponse)) {
// Skip validation if reCAPTCHA is disabled (test keys) sendResponse(false, 'Security verification failed. Please try again.');
error_log("reCAPTCHA validation skipped - test keys in use");
return true;
} }
if (empty($token)) { $verifyData = [
return false; 'secret' => RECAPTCHA_SECRET_KEY,
} 'response' => $recaptchaResponse,
$secretKey = RECAPTCHA_SECRET_KEY;
$verifyURL = 'https://www.google.com/recaptcha/api/siteverify';
$data = [
'secret' => $secretKey,
'response' => $token,
'remoteip' => $_SERVER['REMOTE_ADDR'] 'remoteip' => $_SERVER['REMOTE_ADDR']
]; ];
$options = [ $result = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', false,
'http' => [ stream_context_create(['http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST', '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); if ($result) {
$result = file_get_contents($verifyURL, false, $context); $resultJson = json_decode($result, true);
if (!$resultJson['success'] || ($resultJson['score'] ?? 1) < RECAPTCHA_THRESHOLD) {
if ($result === false) { logDebug("reCAPTCHA failed: " . print_r($resultJson, true));
error_log('reCAPTCHA verification request failed'); sendResponse(false, 'Security verification failed. Please try again.');
return false; }
} }
$resultJson = json_decode($result, true);
if ($resultJson['success'] && isset($resultJson['score'])) {
return $resultJson['score'] >= RECAPTCHA_THRESHOLD;
}
return false;
} }
// Verify reCAPTCHA // Honeypot check
$recaptchaResponse = $_POST['recaptcha_response'] ?? ''; if (!empty($_POST['website'])) {
if (!validateRecaptcha($recaptchaResponse)) { logDebug("Honeypot triggered");
sendResponse(false, 'Security verification failed. Please try again.');
}
// Spam protection - honeypot field
if (isset($_POST['website']) && !empty($_POST['website'])) {
sendResponse(false, 'Spam detected'); sendResponse(false, 'Spam detected');
} }
// Validate and sanitize inputs // Bot detection - user agent check
$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
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$suspiciousAgents = ['curl', 'wget', 'python', 'bot', 'crawler', 'spider', 'scraper']; $suspiciousAgents = ['curl', 'wget', 'python-requests', 'scrapy'];
foreach ($suspiciousAgents as $agent) { foreach ($suspiciousAgents as $agent) {
if (stripos($userAgent, $agent) !== false) { if (stripos($userAgent, $agent) !== false) {
logDebug("Suspicious user agent: $userAgent");
sendResponse(false, 'Automated submissions not allowed'); sendResponse(false, 'Automated submissions not allowed');
} }
} }
// Check submission speed (too fast = likely bot) - More lenient timing // Extract and validate form data
if (isset($_SESSION['form_start_time'])) { $service_type = validateInput($_POST['service_type'] ?? '');
$submissionTime = time() - $_SESSION['form_start_time']; $scale = validateInput($_POST['scale'] ?? '');
if ($submissionTime < 3) { // Only block if under 3 seconds (very aggressive bots) $name = validateInput($_POST['name'] ?? '');
sendResponse(false, 'Form submitted too quickly'); $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 // Spam content check
$services = array_map(function($service) { $spamKeywords = ['viagra', 'cialis', 'casino', 'lottery', 'bitcoin', 'forex', 'pharmacy', 'click here', 'act now'];
return htmlspecialchars(trim($service), ENT_QUOTES, 'UTF-8'); $contentToCheck = strtolower($requirements . ' ' . $name . ' ' . $company . ' ' . $data_sources);
}, $services); foreach ($spamKeywords as $keyword) {
if (strpos($contentToCheck, $keyword) !== false) {
logDebug("Spam keyword detected: $keyword");
sendResponse(false, 'Invalid content detected');
}
}
// Update rate limit counter // Update rate limit counter
$ip = $_SERVER['REMOTE_ADDR']; $ip = $_SERVER['REMOTE_ADDR'];
$key = 'quote_' . md5($ip); $key = 'quote_' . md5($ip);
$_SESSION[$key]['count']++; $_SESSION[$key]['count']++;
// Create friendly service names // Prepare friendly labels
$service_names = [ $serviceLabels = [
'web-scraping' => 'Web Scraping & Data Extraction', 'web-scraping' => 'Web Scraping',
'business-intelligence' => 'Business Intelligence & Analytics', 'data-cleaning' => 'Data Cleaning',
'data-processing' => 'Data Processing & Cleaning', 'api-development' => 'API Development',
'automation' => 'Automation & APIs', 'automation' => 'Automation',
'consulting' => 'Custom Development', 'custom' => 'Custom Solution',
'other' => 'Other Services' 'other' => 'Other'
]; ];
$selected_services = array_map(function($service) use ($service_names) { $scaleLabels = [
return $service_names[$service] ?? $service; 'small' => 'Small (under 1,000 records)',
}, $services); 'medium' => 'Medium (1,000 - 50,000 records)',
'large' => 'Large (50,000 - 500,000 records)',
// Create friendly scale names 'enterprise' => 'Enterprise (500,000+ records)',
$scale_names = [ 'unsure' => 'Not sure yet'
'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)'
]; ];
$friendly_scale = $scale_names[$project_scale] ?? $project_scale; $timelineLabels = [
'asap' => 'ASAP',
// Create friendly timeline names '2weeks' => 'Within 2 weeks',
$timeline_names = [ '1month' => 'Within a month',
'asap' => 'ASAP (Rush job)', 'flexible' => 'Flexible / No rush'
'1-week' => 'Within 1 week',
'2-4-weeks' => '2-4 weeks',
'flexible' => 'Flexible timeline'
]; ];
$friendly_timeline = $timeline_names[$timeline] ?? $timeline; // Build email
// Prepare email content
$to = 'info@ukdataservices.co.uk'; $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 = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Arial,sans-serif;line-height:1.6;">
$emailHTML = ' <div style="max-width:600px;margin:0 auto;padding:20px;">
<!DOCTYPE html> <div style="background:linear-gradient(135deg,#144784,#179e83);color:white;padding:20px;border-radius:8px 8px 0 0;text-align:center;">
<html> <h1 style="margin:0;">New Quote Request</h1>
<head> <p style="margin:5px 0 0;">UK Data Services</p>
<meta charset="UTF-8"> </div>
<title>New Quote Request</title> <div style="background:#f9f9f9;padding:20px;border-radius:0 0 8px 8px;">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 700px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #144784 0%, #179e83 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
.section { margin-bottom: 30px; padding: 20px; background: white; border-radius: 8px; border-left: 4px solid #179e83; }
.section-title { font-size: 18px; font-weight: bold; color: #144784; margin-bottom: 15px; }
.field { margin-bottom: 12px; }
.field-label { font-weight: bold; color: #555; }
.field-value { margin-top: 5px; padding: 8px; background: #f8f9fa; border-radius: 4px; }
.services-list { list-style: none; padding: 0; }
.services-list li { padding: 8px; background: #e3f2fd; margin: 5px 0; border-radius: 4px; }
.priority { background: #fff3cd; border-left-color: #ffc107; }
.contact-info { background: #d4edda; border-left-color: #28a745; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 New Quote Request</h1>
<p>UK Data Services</p>
<p style="font-size: 14px; opacity: 0.9;">Received: ' . date('Y-m-d H:i:s') . ' UTC</p>
</div>
<div class="content">
<div class="section contact-info">
<div class="section-title">👤 Contact Information</div>
<div class="field">
<div class="field-label">Name:</div>
<div class="field-value">' . htmlspecialchars($name) . '</div>
</div>
<div class="field">
<div class="field-label">Email:</div>
<div class="field-value">' . htmlspecialchars($email) . '</div>
</div>
<div class="field">
<div class="field-label">Company:</div>
<div class="field-value">' . htmlspecialchars($company ?: 'Not provided') . '</div>
</div>
<div class="field">
<div class="field-label">Phone:</div>
<div class="field-value">' . htmlspecialchars($phone ?: 'Not provided') . '</div>
</div>
</div>
<div class="section"> <div style="background:white;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid #28a745;">
<div class="section-title">🎯 Services Required</div> <h3 style="margin:0 0 10px;color:#144784;">Contact Details</h3>
<ul class="services-list">'; <p><strong>Name:</strong> ' . $name . '</p>
<p><strong>Email:</strong> ' . $email . '</p>
<p><strong>Company:</strong> ' . ($company ?: 'Not provided') . '</p>
</div>
foreach ($selected_services as $service) { <div style="background:white;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid #179e83;">
$emailHTML .= '<li>✓ ' . htmlspecialchars($service) . '</li>'; <h3 style="margin:0 0 10px;color:#144784;">Project Details</h3>
<p><strong>Service:</strong> ' . ($serviceLabels[$service_type] ?? $service_type) . '</p>
<p><strong>Scale:</strong> ' . ($scaleLabels[$scale] ?? $scale) . '</p>
<p><strong>Timeline:</strong> ' . ($timelineLabels[$timeline] ?? $timeline) . '</p>
<p><strong>Target Sources:</strong> ' . ($data_sources ?: 'Not specified') . '</p>
</div>';
if (!empty($requirements)) {
$emailHTML .= '<div style="background:white;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid #ffc107;">
<h3 style="margin:0 0 10px;color:#144784;">Requirements</h3>
<p>' . nl2br($requirements) . '</p>
</div>';
} }
$emailHTML .= '</ul> $emailHTML .= '<div style="background:#e9ecef;padding:10px;border-radius:8px;font-size:12px;color:#666;">
</div> <p><strong>Submitted:</strong> ' . date('Y-m-d H:i:s') . ' UTC</p>
<p><strong>IP:</strong> ' . $_SERVER['REMOTE_ADDR'] . '</p>
</div>
<div class="section ' . ($timeline === 'asap' ? 'priority' : '') . '"> </div></div></body></html>';
<div class="section-title">📊 Project Details</div>
<div class="field">
<div class="field-label">Project Scale:</div>
<div class="field-value">' . htmlspecialchars($friendly_scale) . '</div>
</div>
<div class="field">
<div class="field-label">Timeline:</div>
<div class="field-value">' . htmlspecialchars($friendly_timeline) . '</div>
</div>
<div class="field">
<div class="field-label">Budget Range:</div>
<div class="field-value">' . htmlspecialchars($budget ?: 'Not specified') . '</div>
</div>
</div>
<div class="section">
<div class="section-title">🌐 Data Sources</div>
<div class="field-value">' . nl2br(htmlspecialchars($data_sources ?: 'Not specified')) . '</div>
</div>
<div class="section">
<div class="section-title">📝 Detailed Requirements</div>
<div class="field-value">' . nl2br(htmlspecialchars($requirements)) . '</div>
</div>
<div class="section">
<div class="section-title">🔍 Submission Details</div>
<div class="field">
<div class="field-label">IP Address:</div>
<div class="field-value">' . htmlspecialchars($_SERVER['REMOTE_ADDR']) . '</div>
</div>
<div class="field">
<div class="field-label">User Agent:</div>
<div class="field-value">' . htmlspecialchars($_SERVER['HTTP_USER_AGENT']) . '</div>
</div>
<div class="field">
<div class="field-label">Referrer:</div>
<div class="field-value">' . htmlspecialchars($_SERVER['HTTP_REFERER'] ?? 'Direct') . '</div>
</div>
</div>
</div>
</div>
</body>
</html>';
// Email headers
$headers = "MIME-Version: 1.0\r\n"; $headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n"; $headers .= "Content-Type: text/html; charset=UTF-8\r\n";
$headers .= "From: \"UK Data Services Quote System\" <noreply@ukdataservices.co.uk>\r\n"; $headers .= "From: \"UK Data Services\" <noreply@ukdataservices.co.uk>\r\n";
$headers .= "Reply-To: " . $email . "\r\n"; $headers .= "Reply-To: " . $email . "\r\n";
$headers .= "X-Mailer: PHP/" . phpversion() . "\r\n";
$headers .= "X-Priority: " . ($timeline === 'asap' ? '1' : '3') . "\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 // Send email
try { $emailSent = @mail($to, $subject, $emailHTML, $headers);
// Clear any previous errors
error_clear_last(); 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) { sendResponse(true, 'Thank you for your quote request! We will send you a detailed proposal within 24 hours.');
// Log successful submission } else {
$logEntry = date('Y-m-d H:i:s') . " - Quote request from " . $email . " (" . $_SERVER['REMOTE_ADDR'] . ") - Services: " . implode(', ', $services) . "\n"; $error = error_get_last();
file_put_contents('logs/quote-requests.log', $logEntry, FILE_APPEND | LOCK_EX); 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');
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');
} }
?> ?>

View File

@@ -197,13 +197,13 @@ $breadcrumbs = [
<style> <style>
.quote-hero { .quote-hero {
padding: 120px 0 60px; padding: 100px 0 30px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
text-align: center; text-align: center;
} }
.quote-form-section { .quote-form-section {
padding: 60px 0; padding: 30px 0 60px;
background: white; background: white;
} }
@@ -500,25 +500,25 @@ $breadcrumbs = [
</section> </section>
<!-- Server-rendered content for SEO and no-JS fallback --> <!-- Server-rendered content for SEO and no-JS fallback -->
<noscript>
<section class="quote-intro" style="padding: 40px 0; background: #fff;"> <section class="quote-intro" style="padding: 40px 0; background: #fff;">
<div class="container" style="max-width: 900px; margin: 0 auto; padding: 0 20px;"> <div class="container" style="max-width: 900px; margin: 0 auto; padding: 0 20px;">
<noscript> <div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 24px; margin: 24px 0;">
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 24px; margin: 24px 0;"> <h3 style="color: #856404; margin: 0 0 12px 0; font-size: 1.2rem;">JavaScript Required for Online Form</h3>
<h3 style="color: #856404; margin: 0 0 12px 0; font-size: 1.2rem;">JavaScript Required for Online Form</h3> <p style="margin: 0 0 16px 0; color: #856404; line-height: 1.6;">
<p style="margin: 0 0 16px 0; color: #856404; line-height: 1.6;"> Our interactive quote form requires JavaScript to function. If you're unable to enable JavaScript, please contact us directly:
Our interactive quote form requires JavaScript to function. If you're unable to enable JavaScript, please contact us directly: </p>
</p> <ul style="margin: 0; padding-left: 20px; color: #856404; line-height: 1.8;">
<ul style="margin: 0; padding-left: 20px; color: #856404; line-height: 1.8;"> <li><strong>Email:</strong> <a href="mailto:hello@ukdataservices.co.uk" style="color: #0056b3;">hello@ukdataservices.co.uk</a></li>
<li><strong>Email:</strong> <a href="mailto:hello@ukdataservices.co.uk" style="color: #0056b3;">hello@ukdataservices.co.uk</a></li> <li><strong>Phone:</strong> Available on request</li>
<li><strong>Phone:</strong> Available on request</li> </ul>
</ul> <p style="margin: 16px 0 0 0; color: #856404; line-height: 1.6;">
<p style="margin: 16px 0 0 0; color: #856404; line-height: 1.6;"> Include your name, email, company (if applicable), and a description of your data requirements. We'll respond within 24 hours with a detailed proposal.
Include your name, email, company (if applicable), and a description of your data requirements. We'll respond within 24 hours with a detailed proposal. </p>
</p> </div>
</div>
</noscript>
</div> </div>
</section> </section>
</noscript>
<!-- Quote Form --> <!-- Quote Form -->