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

- -
- '; - foreach ($errors as $error) { - if (trim($error)) { - echo '
  • ' . htmlspecialchars(trim($error)) . '
  • '; - } - } - echo ''; - } else { - echo htmlspecialchars($message); - } - } - ?> -
    - -
    - - Return to Home - Read Our Blog - - - Return to Home - -
    - -
    - -

    We'll be in touch within 24 hours with a detailed proposal.

    -

    Have questions? Email us at info@ukdataservices.co.uk

    - -

    If you continue to experience issues, please contact us directly:

    -

    Email: info@ukdataservices.co.uk

    -

    Phone: +44 1692 689150

    - -
    -
    - - - $title + +

    $title

    " . htmlspecialchars($message) . "

    +

    Back to form | Home

    "; exit; } @@ -335,386 +103,197 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { sendResponse(false, 'Invalid request method'); } -// Validate CSRF token -if (!isset($_POST['csrf_token'])) { +logDebug("Form submission from " . $_SERVER['REMOTE_ADDR'] . " - Session ID: " . session_id()); + +// CSRF Validation +$csrfToken = $_POST['csrf_token'] ?? ''; +if (empty($csrfToken)) { + logDebug("CSRF: No token in POST data"); sendResponse(false, 'Security validation failed. Please refresh the page and try again.'); } -if (!validateCSRFToken($_POST['csrf_token'])) { +if (!validateCSRFToken($csrfToken)) { sendResponse(false, 'Security validation failed. Please refresh the page and try again.'); } -// Check for blocked IPs -function checkBlockedIP() { - $ip = $_SERVER['REMOTE_ADDR']; - $blockFile = 'logs/blocked-ips.txt'; - - if (file_exists($blockFile)) { - $blockedIPs = file($blockFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($blockedIPs as $blockedEntry) { - $parts = explode('|', $blockedEntry); - if (isset($parts[0]) && $parts[0] === $ip) { - $blockTime = isset($parts[1]) ? (int)$parts[1] : 0; - // Block for 24 hours - if (time() - $blockTime < 86400) { - return false; - } - } - } - } - return true; -} +logDebug("CSRF validation passed"); -// Check for blocked IPs first -if (!checkBlockedIP()) { - sendResponse(false, 'Access temporarily restricted'); -} - -// Check rate limiting +// Rate limiting if (!checkRateLimit()) { + logDebug("Rate limit exceeded for " . $_SERVER['REMOTE_ADDR']); sendResponse(false, 'Too many requests. Please try again later.'); } -// reCAPTCHA Verification -require_once '.recaptcha-config.php'; - -function validateRecaptcha($token) { - if (!RECAPTCHA_ENABLED) { - // Skip validation if reCAPTCHA is disabled (test keys) - error_log("reCAPTCHA validation skipped - test keys in use"); - return true; +// reCAPTCHA (if enabled) +require_once __DIR__ . '/.recaptcha-config.php'; +if (RECAPTCHA_ENABLED) { + $recaptchaResponse = $_POST['recaptcha_response'] ?? ''; + if (empty($recaptchaResponse)) { + sendResponse(false, 'Security verification failed. Please try again.'); } - if (empty($token)) { - return false; - } - - $secretKey = RECAPTCHA_SECRET_KEY; - $verifyURL = 'https://www.google.com/recaptcha/api/siteverify'; - - $data = [ - 'secret' => $secretKey, - 'response' => $token, + $verifyData = [ + 'secret' => RECAPTCHA_SECRET_KEY, + 'response' => $recaptchaResponse, 'remoteip' => $_SERVER['REMOTE_ADDR'] ]; - $options = [ - 'http' => [ - 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + $result = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, + stream_context_create(['http' => [ 'method' => 'POST', - 'content' => http_build_query($data) - ] - ]; + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => http_build_query($verifyData) + ]])); - $context = stream_context_create($options); - $result = file_get_contents($verifyURL, false, $context); - - if ($result === false) { - error_log('reCAPTCHA verification request failed'); - return false; + if ($result) { + $resultJson = json_decode($result, true); + if (!$resultJson['success'] || ($resultJson['score'] ?? 1) < RECAPTCHA_THRESHOLD) { + logDebug("reCAPTCHA failed: " . print_r($resultJson, true)); + sendResponse(false, 'Security verification failed. Please try again.'); + } } - - $resultJson = json_decode($result, true); - - if ($resultJson['success'] && isset($resultJson['score'])) { - return $resultJson['score'] >= RECAPTCHA_THRESHOLD; - } - - return false; } -// Verify reCAPTCHA -$recaptchaResponse = $_POST['recaptcha_response'] ?? ''; -if (!validateRecaptcha($recaptchaResponse)) { - sendResponse(false, 'Security verification failed. Please try again.'); -} - -// Spam protection - honeypot field -if (isset($_POST['website']) && !empty($_POST['website'])) { +// Honeypot check +if (!empty($_POST['website'])) { + logDebug("Honeypot triggered"); sendResponse(false, 'Spam detected'); } -// Validate and sanitize inputs -$services = $_POST['services'] ?? []; -$project_scale = validateInput($_POST['project_scale'] ?? '', 'text'); -$timeline = validateInput($_POST['timeline'] ?? '', 'text'); -$name = validateInput($_POST['name'] ?? '', 'text'); -$email = validateInput($_POST['email'] ?? '', 'email'); -$company = validateInput($_POST['company'] ?? '', 'text'); -$phone = validateInput($_POST['phone'] ?? '', 'phone'); -$data_sources = validateInput($_POST['data_sources'] ?? '', 'text'); -$requirements = validateInput($_POST['requirements'] ?? '', 'long_text'); -$budget = validateInput($_POST['budget'] ?? '', 'text'); - -// Validation -$errors = []; - -if (empty($services) || !is_array($services)) { - $errors[] = 'Please select at least one service'; -} - -if (!$project_scale) { - $errors[] = 'Please select a project scale'; -} - -if (!$timeline) { - $errors[] = 'Please select a timeline'; -} - -if (!$name || strlen($name) < 2) { - $errors[] = 'Please enter a valid name'; -} - -if (!$email) { - $errors[] = 'Please enter a valid email address'; -} - -if (!$requirements) { - $errors[] = 'Please provide detailed project requirements'; -} - -if (!empty($errors)) { - sendResponse(false, implode('. ', $errors)); -} - -// Enhanced spam protection - content filtering -$spamKeywords = [ - 'viagra', 'cialis', 'casino', 'lottery', 'bitcoin', 'forex', 'loan', 'debt', - 'pharmacy', 'click here', 'act now', 'limited time', 'risk free', 'guarantee', - 'no obligation', 'free money', 'make money fast', 'work from home', 'get rich', - 'investment opportunity', 'credit repair', 'refinance', 'consolidate debt', - 'weight loss', 'miracle cure', 'lose weight', 'adult content', 'porn', - 'sex', 'dating', 'singles', 'webcam', 'escort', 'massage' -]; - -$contentToCheck = strtolower($requirements . ' ' . $name . ' ' . $company . ' ' . $data_sources); -foreach ($spamKeywords as $keyword) { - if (strpos($contentToCheck, $keyword) !== false) { - sendResponse(false, 'Invalid content detected'); - } -} - -// Bot detection - check for suspicious patterns +// Bot detection - user agent check $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; -$suspiciousAgents = ['curl', 'wget', 'python', 'bot', 'crawler', 'spider', 'scraper']; +$suspiciousAgents = ['curl', 'wget', 'python-requests', 'scrapy']; foreach ($suspiciousAgents as $agent) { if (stripos($userAgent, $agent) !== false) { + logDebug("Suspicious user agent: $userAgent"); sendResponse(false, 'Automated submissions not allowed'); } } -// Check submission speed (too fast = likely bot) - More lenient timing -if (isset($_SESSION['form_start_time'])) { - $submissionTime = time() - $_SESSION['form_start_time']; - if ($submissionTime < 3) { // Only block if under 3 seconds (very aggressive bots) - sendResponse(false, 'Form submitted too quickly'); - } +// Extract and validate form data +$service_type = validateInput($_POST['service_type'] ?? ''); +$scale = validateInput($_POST['scale'] ?? ''); +$name = validateInput($_POST['name'] ?? ''); +$email = validateInput($_POST['email'] ?? '', 'email'); +$company = validateInput($_POST['company'] ?? ''); +$timeline = validateInput($_POST['timeline'] ?? ''); +$data_sources = validateInput($_POST['data_sources'] ?? ''); +$requirements = validateInput($_POST['requirements'] ?? ''); + +// Validation +$errors = []; +if (empty($service_type)) $errors[] = 'Please select a service'; +if (empty($scale)) $errors[] = 'Please select a scale'; +if (empty($name) || strlen($name) < 2) $errors[] = 'Please enter a valid name'; +if (!$email) $errors[] = 'Please enter a valid email address'; +if (empty($timeline)) $errors[] = 'Please select a timeline'; + +if (!empty($errors)) { + logDebug("Validation errors: " . implode(', ', $errors)); + sendResponse(false, implode('. ', $errors)); } -// Sanitize services array -$services = array_map(function($service) { - return htmlspecialchars(trim($service), ENT_QUOTES, 'UTF-8'); -}, $services); +// Spam content check +$spamKeywords = ['viagra', 'cialis', 'casino', 'lottery', 'bitcoin', 'forex', 'pharmacy', 'click here', 'act now']; +$contentToCheck = strtolower($requirements . ' ' . $name . ' ' . $company . ' ' . $data_sources); +foreach ($spamKeywords as $keyword) { + if (strpos($contentToCheck, $keyword) !== false) { + logDebug("Spam keyword detected: $keyword"); + sendResponse(false, 'Invalid content detected'); + } +} // Update rate limit counter $ip = $_SERVER['REMOTE_ADDR']; $key = 'quote_' . md5($ip); $_SESSION[$key]['count']++; -// Create friendly service names -$service_names = [ - 'web-scraping' => 'Web Scraping & Data Extraction', - 'business-intelligence' => 'Business Intelligence & Analytics', - 'data-processing' => 'Data Processing & Cleaning', - 'automation' => 'Automation & APIs', - 'consulting' => 'Custom Development', - 'other' => 'Other Services' +// Prepare friendly labels +$serviceLabels = [ + 'web-scraping' => 'Web Scraping', + 'data-cleaning' => 'Data Cleaning', + 'api-development' => 'API Development', + 'automation' => 'Automation', + 'custom' => 'Custom Solution', + 'other' => 'Other' ]; -$selected_services = array_map(function($service) use ($service_names) { - return $service_names[$service] ?? $service; -}, $services); - -// Create friendly scale names -$scale_names = [ - 'small' => 'Small Project (One-time extraction, < 10k records)', - 'medium' => 'Medium Project (Regular updates, 10k-100k records)', - 'large' => 'Large Project (Ongoing service, 100k+ records)', - 'enterprise' => 'Enterprise (Complex multi-source solution)' +$scaleLabels = [ + 'small' => 'Small (under 1,000 records)', + 'medium' => 'Medium (1,000 - 50,000 records)', + 'large' => 'Large (50,000 - 500,000 records)', + 'enterprise' => 'Enterprise (500,000+ records)', + 'unsure' => 'Not sure yet' ]; -$friendly_scale = $scale_names[$project_scale] ?? $project_scale; - -// Create friendly timeline names -$timeline_names = [ - 'asap' => 'ASAP (Rush job)', - '1-week' => 'Within 1 week', - '2-4-weeks' => '2-4 weeks', - 'flexible' => 'Flexible timeline' +$timelineLabels = [ + 'asap' => 'ASAP', + '2weeks' => 'Within 2 weeks', + '1month' => 'Within a month', + 'flexible' => 'Flexible / No rush' ]; -$friendly_timeline = $timeline_names[$timeline] ?? $timeline; - -// Prepare email content +// Build email $to = 'info@ukdataservices.co.uk'; -$subject = 'New Quote Request - UK Data Services'; +$subject = 'New Quote Request - ' . ($serviceLabels[$service_type] ?? $service_type); -// Create detailed HTML email -$emailHTML = ' - - - - - New Quote Request - - - -
    -
    -

    🚀 New Quote Request

    -

    UK Data Services

    -

    Received: ' . date('Y-m-d H:i:s') . ' UTC

    -
    - -
    -
    -
    👤 Contact Information
    -
    -
    Name:
    -
    ' . htmlspecialchars($name) . '
    -
    -
    -
    Email:
    -
    ' . htmlspecialchars($email) . '
    -
    -
    -
    Company:
    -
    ' . htmlspecialchars($company ?: 'Not provided') . '
    -
    -
    -
    Phone:
    -
    ' . htmlspecialchars($phone ?: 'Not provided') . '
    -
    -
    +$emailHTML = ' +
    +
    +

    New Quote Request

    +

    UK Data Services

    +
    +
    -
    -
    🎯 Services Required
    -
      '; +
      +

      Contact Details

      +

      Name: ' . $name . '

      +

      Email: ' . $email . '

      +

      Company: ' . ($company ?: 'Not provided') . '

      +
      -foreach ($selected_services as $service) { - $emailHTML .= '
    • ✓ ' . htmlspecialchars($service) . '
    • '; +
      +

      Project Details

      +

      Service: ' . ($serviceLabels[$service_type] ?? $service_type) . '

      +

      Scale: ' . ($scaleLabels[$scale] ?? $scale) . '

      +

      Timeline: ' . ($timelineLabels[$timeline] ?? $timeline) . '

      +

      Target Sources: ' . ($data_sources ?: 'Not specified') . '

      +
      '; + +if (!empty($requirements)) { + $emailHTML .= '
      +

      Requirements

      +

      ' . nl2br($requirements) . '

      +
      '; } -$emailHTML .= '
    -
    +$emailHTML .= '
    +

    Submitted: ' . date('Y-m-d H:i:s') . ' UTC

    +

    IP: ' . $_SERVER['REMOTE_ADDR'] . '

    +
    -
    -
    📊 Project Details
    -
    -
    Project Scale:
    -
    ' . htmlspecialchars($friendly_scale) . '
    -
    -
    -
    Timeline:
    -
    ' . htmlspecialchars($friendly_timeline) . '
    -
    -
    -
    Budget Range:
    -
    ' . htmlspecialchars($budget ?: 'Not specified') . '
    -
    -
    +
    '; -
    -
    🌐 Data Sources
    -
    ' . nl2br(htmlspecialchars($data_sources ?: 'Not specified')) . '
    -
    - -
    -
    📝 Detailed Requirements
    -
    ' . nl2br(htmlspecialchars($requirements)) . '
    -
    - -
    -
    🔍 Submission Details
    -
    -
    IP Address:
    -
    ' . htmlspecialchars($_SERVER['REMOTE_ADDR']) . '
    -
    -
    -
    User Agent:
    -
    ' . htmlspecialchars($_SERVER['HTTP_USER_AGENT']) . '
    -
    -
    -
    Referrer:
    -
    ' . htmlspecialchars($_SERVER['HTTP_REFERER'] ?? 'Direct') . '
    -
    -
    -
    -
    - -'; - -// Email headers $headers = "MIME-Version: 1.0\r\n"; $headers .= "Content-Type: text/html; charset=UTF-8\r\n"; -$headers .= "From: \"UK Data Services Quote System\" \r\n"; +$headers .= "From: \"UK Data Services\" \r\n"; $headers .= "Reply-To: " . $email . "\r\n"; -$headers .= "X-Mailer: PHP/" . phpversion() . "\r\n"; $headers .= "X-Priority: " . ($timeline === 'asap' ? '1' : '3') . "\r\n"; -// Create logs directory if it doesn't exist -if (!file_exists('logs')) { - mkdir('logs', 0755, true); -} - // Send email -try { - // Clear any previous errors - error_clear_last(); +$emailSent = @mail($to, $subject, $emailHTML, $headers); + +if ($emailSent) { + logDebug("SUCCESS: Quote from $email ($name) - Service: $service_type"); - $emailSent = mail($to, $subject, $emailHTML, $headers); + // Log to file + $logEntry = date('Y-m-d H:i:s') . " | $name | $email | $service_type | $scale | $timeline\n"; + file_put_contents($logDir . '/quote-requests.log', $logEntry, FILE_APPEND | LOCK_EX); - if ($emailSent) { - // Log successful submission - $logEntry = date('Y-m-d H:i:s') . " - Quote request from " . $email . " (" . $_SERVER['REMOTE_ADDR'] . ") - Services: " . implode(', ', $services) . "\n"; - file_put_contents('logs/quote-requests.log', $logEntry, FILE_APPEND | LOCK_EX); - - sendResponse(true, 'Thank you for your quote request! We will send you a detailed proposal within 24 hours.'); - } else { - // Get detailed error information - $lastError = error_get_last(); - $errorMsg = $lastError ? $lastError['message'] : 'Unknown mail error'; - - // Log failed email with detailed error - $logEntry = date('Y-m-d H:i:s') . " - FAILED quote request from " . $email . " (" . $_SERVER['REMOTE_ADDR'] . ") - Error: " . $errorMsg . "\n"; - file_put_contents('logs/quote-errors.log', $logEntry, FILE_APPEND | LOCK_EX); - - // Check common issues - if (strpos($errorMsg, 'sendmail') !== false) { - error_log("Mail server configuration issue: " . $errorMsg); - } - - sendResponse(false, 'There was an error sending your quote request. Please try again or contact us directly at info@ukdataservices.co.uk'); - } -} catch (Exception $e) { - // Log exception with full details - $logEntry = date('Y-m-d H:i:s') . " - EXCEPTION: " . $e->getMessage() . " from " . $email . " (" . $_SERVER['REMOTE_ADDR'] . ") - File: " . $e->getFile() . " Line: " . $e->getLine() . "\n"; - file_put_contents('logs/quote-errors.log', $logEntry, FILE_APPEND | LOCK_EX); - - sendResponse(false, 'There was an error processing your quote request. Please contact us directly at info@ukdataservices.co.uk'); + sendResponse(true, 'Thank you for your quote request! We will send you a detailed proposal within 24 hours.'); +} else { + $error = error_get_last(); + logDebug("FAILED: Email send failed - " . ($error['message'] ?? 'Unknown error')); + sendResponse(false, 'There was an error sending your request. Please try again or contact us at info@ukdataservices.co.uk'); } -?> \ No newline at end of file +?> diff --git a/quote.php b/quote.php index 0d9f8d3..68263fb 100644 --- a/quote.php +++ b/quote.php @@ -197,13 +197,13 @@ $breadcrumbs = [