Fix quote page gap: wrap noscript fallback, reduce hero/form padding
This commit is contained in:
16
.htaccess
16
.htaccess
@@ -24,11 +24,17 @@
|
|||||||
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"
|
||||||
|
|
||||||
|
# CRITICAL: No caching for form pages (contain session-specific CSRF tokens)
|
||||||
|
<FilesMatch "(quote|contact)\.php$">
|
||||||
|
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"
|
||||||
|
</FilesMatch>
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
# Enhanced Gzip compression
|
# Enhanced Gzip compression
|
||||||
<IfModule mod_deflate.c>
|
<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 text/html text/plain text/xml text/css text/javascript
|
||||||
AddOutputFilterByType DEFLATE application/javascript application/x-javascript
|
AddOutputFilterByType DEFLATE application/javascript application/x-javascript
|
||||||
AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml
|
AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml
|
||||||
@@ -36,7 +42,6 @@
|
|||||||
AddOutputFilterByType DEFLATE image/svg+xml
|
AddOutputFilterByType DEFLATE image/svg+xml
|
||||||
AddOutputFilterByType DEFLATE font/ttf font/otf font/eot font/woff font/woff2
|
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 gzip-only-text/html
|
||||||
BrowserMatch ^Mozilla/4\.0[678] no-gzip
|
BrowserMatch ^Mozilla/4\.0[678] no-gzip
|
||||||
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
|
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
|
||||||
@@ -106,8 +111,8 @@
|
|||||||
Header set Cache-Control "max-age=2592000, public"
|
Header set Cache-Control "max-age=2592000, public"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
# HTML/PHP - 1 hour
|
# Regular HTML/PHP - 1 hour (but form pages are excluded above)
|
||||||
<FilesMatch "\.(html|php)$">
|
<FilesMatch "\.(html)$">
|
||||||
Header set Cache-Control "max-age=3600, public, must-revalidate"
|
Header set Cache-Control "max-age=3600, public, must-revalidate"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
@@ -117,7 +122,6 @@
|
|||||||
|
|
||||||
# HTTP/2 Server Push
|
# HTTP/2 Server Push
|
||||||
<IfModule mod_http2.c>
|
<IfModule mod_http2.c>
|
||||||
# Push critical resources
|
|
||||||
<FilesMatch "index\.php">
|
<FilesMatch "index\.php">
|
||||||
Header add Link "</assets/css/main.min.css>; rel=preload; as=style"
|
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/images/ukds-main-logo.webp>; rel=preload; as=image"
|
||||||
@@ -140,7 +144,7 @@ Options -Indexes
|
|||||||
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]
|
||||||
|
|||||||
171
backups/.htaccess.pre-csrf-fix
Normal file
171
backups/.htaccess.pre-csrf-fix
Normal 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
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) {
|
// Rate limiting
|
||||||
$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
|
|
||||||
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) {
|
|
||||||
if (!RECAPTCHA_ENABLED) {
|
|
||||||
// Skip validation if reCAPTCHA is disabled (test keys)
|
|
||||||
error_log("reCAPTCHA validation skipped - test keys in use");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($token)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$secretKey = RECAPTCHA_SECRET_KEY;
|
|
||||||
$verifyURL = 'https://www.google.com/recaptcha/api/siteverify';
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'secret' => $secretKey,
|
|
||||||
'response' => $token,
|
|
||||||
'remoteip' => $_SERVER['REMOTE_ADDR']
|
|
||||||
];
|
|
||||||
|
|
||||||
$options = [
|
|
||||||
'http' => [
|
|
||||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
|
||||||
'method' => 'POST',
|
|
||||||
'content' => http_build_query($data)
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$context = stream_context_create($options);
|
|
||||||
$result = file_get_contents($verifyURL, false, $context);
|
|
||||||
|
|
||||||
if ($result === false) {
|
|
||||||
error_log('reCAPTCHA verification request failed');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$resultJson = json_decode($result, true);
|
|
||||||
|
|
||||||
if ($resultJson['success'] && isset($resultJson['score'])) {
|
|
||||||
return $resultJson['score'] >= RECAPTCHA_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify reCAPTCHA
|
|
||||||
$recaptchaResponse = $_POST['recaptcha_response'] ?? '';
|
$recaptchaResponse = $_POST['recaptcha_response'] ?? '';
|
||||||
if (!validateRecaptcha($recaptchaResponse)) {
|
if (empty($recaptchaResponse)) {
|
||||||
sendResponse(false, 'Security verification failed. Please try again.');
|
sendResponse(false, 'Security verification failed. Please try again.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spam protection - honeypot field
|
$verifyData = [
|
||||||
if (isset($_POST['website']) && !empty($_POST['website'])) {
|
'secret' => RECAPTCHA_SECRET_KEY,
|
||||||
|
'response' => $recaptchaResponse,
|
||||||
|
'remoteip' => $_SERVER['REMOTE_ADDR']
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', false,
|
||||||
|
stream_context_create(['http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => 'Content-type: application/x-www-form-urlencoded',
|
||||||
|
'content' => http_build_query($verifyData)
|
||||||
|
]]));
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Honeypot check
|
||||||
|
if (!empty($_POST['website'])) {
|
||||||
|
logDebug("Honeypot triggered");
|
||||||
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; }
|
<div style="background:white;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid #28a745;">
|
||||||
.container { max-width: 700px; margin: 0 auto; padding: 20px; }
|
<h3 style="margin:0 0 10px;color:#144784;">Contact Details</h3>
|
||||||
.header { background: linear-gradient(135deg, #144784 0%, #179e83 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
|
<p><strong>Name:</strong> ' . $name . '</p>
|
||||||
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
|
<p><strong>Email:</strong> ' . $email . '</p>
|
||||||
.section { margin-bottom: 30px; padding: 20px; background: white; border-radius: 8px; border-left: 4px solid #179e83; }
|
<p><strong>Company:</strong> ' . ($company ?: 'Not provided') . '</p>
|
||||||
.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>
|
||||||
|
|
||||||
<div class="content">
|
<div style="background:white;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid #179e83;">
|
||||||
<div class="section contact-info">
|
<h3 style="margin:0 0 10px;color:#144784;">Project Details</h3>
|
||||||
<div class="section-title">👤 Contact Information</div>
|
<p><strong>Service:</strong> ' . ($serviceLabels[$service_type] ?? $service_type) . '</p>
|
||||||
<div class="field">
|
<p><strong>Scale:</strong> ' . ($scaleLabels[$scale] ?? $scale) . '</p>
|
||||||
<div class="field-label">Name:</div>
|
<p><strong>Timeline:</strong> ' . ($timelineLabels[$timeline] ?? $timeline) . '</p>
|
||||||
<div class="field-value">' . htmlspecialchars($name) . '</div>
|
<p><strong>Target Sources:</strong> ' . ($data_sources ?: 'Not specified') . '</p>
|
||||||
</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">
|
if (!empty($requirements)) {
|
||||||
<div class="section-title">🎯 Services Required</div>
|
$emailHTML .= '<div style="background:white;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid #ffc107;">
|
||||||
<ul class="services-list">';
|
<h3 style="margin:0 0 10px;color:#144784;">Requirements</h3>
|
||||||
|
<p>' . nl2br($requirements) . '</p>
|
||||||
foreach ($selected_services as $service) {
|
</div>';
|
||||||
$emailHTML .= '<li>✓ ' . htmlspecialchars($service) . '</li>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$emailHTML .= '</ul>
|
$emailHTML .= '<div style="background:#e9ecef;padding:10px;border-radius:8px;font-size:12px;color:#666;">
|
||||||
|
<p><strong>Submitted:</strong> ' . date('Y-m-d H:i:s') . ' UTC</p>
|
||||||
|
<p><strong>IP:</strong> ' . $_SERVER['REMOTE_ADDR'] . '</p>
|
||||||
</div>
|
</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();
|
|
||||||
|
|
||||||
$emailSent = mail($to, $subject, $emailHTML, $headers);
|
|
||||||
|
|
||||||
if ($emailSent) {
|
if ($emailSent) {
|
||||||
// Log successful submission
|
logDebug("SUCCESS: Quote from $email ($name) - Service: $service_type");
|
||||||
$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);
|
// 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);
|
||||||
|
|
||||||
sendResponse(true, 'Thank you for your quote request! We will send you a detailed proposal within 24 hours.');
|
sendResponse(true, 'Thank you for your quote request! We will send you a detailed proposal within 24 hours.');
|
||||||
} else {
|
} else {
|
||||||
// Get detailed error information
|
$error = error_get_last();
|
||||||
$lastError = error_get_last();
|
logDebug("FAILED: Email send failed - " . ($error['message'] ?? 'Unknown error'));
|
||||||
$errorMsg = $lastError ? $lastError['message'] : 'Unknown mail error';
|
sendResponse(false, 'There was an error sending your request. Please try again or contact us at info@ukdataservices.co.uk');
|
||||||
|
|
||||||
// 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');
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -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,9 +500,9 @@ $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;">
|
||||||
@@ -516,9 +516,9 @@ $breadcrumbs = [
|
|||||||
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 -->
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user