²é¿´/±à¼ ´úÂë
ÄÚÈÝ
<?php /** * Template Engine * * Processes templates server-side with obfuscated placeholder replacement. * Never exposes raw placeholder syntax to the client. */ class TemplateEngine { /** * Safe placeholder patterns that look like normal HTML attributes * Using data-* attributes and CSS class-like patterns */ private static $placeholderMap = [ // Internal markers that look like normal attributes 'data-uid' => 'id', 'data-ref' => 'id', 'data-usr' => 'firstname', 'data-org' => 'company', 'data-dom' => 'domain', 'data-ts' => 'date', 'data-tm' => 'time', ]; /** * Render template with recipient data * All placeholders are replaced SERVER-SIDE before sending to client * * @param string $templatePath Path to template file * @param array $data Recipient data * @return string Rendered HTML with no placeholder syntax */ public static function render($templatePath, $data) { if (!file_exists($templatePath)) { return self::renderFallback($data); } $html = file_get_contents($templatePath); // Replace all placeholder patterns $html = self::replacePlaceholders($html, $data); // Sanitize any remaining placeholder-like patterns $html = self::sanitizeOutput($html); return $html; } /** * Replace placeholders with actual values */ private static function replacePlaceholders($html, $data) { // Build replacement map $replacements = self::buildReplacements($data); // Replace {{placeholder}} syntax foreach ($replacements as $key => $value) { $html = str_replace('{{' . $key . '}}', htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), $html); } // Replace any remaining {{...}} with empty string (safety) $html = preg_replace('/\{\{[^}]+\}\}/', '', $html); return $html; } /** * Build replacement values from recipient data */ private static function buildReplacements($data) { $id = $data['id'] ?? $data['session_identifier'] ?? ''; $email = $data['email'] ?? ''; $firstname = $data['name'] ?? $data['firstname'] ?? ''; $lastname = $data['lastname'] ?? ''; $company = $data['company'] ?? ''; $domain = $data['domain'] ?? self::extractDomain($email); // Generate obfuscated action URL $actionPath = self::generateActionPath($id); return [ // Identity (never expose raw) 'id' => $id, 'firstname' => $firstname ?: 'User', 'name' => $firstname ?: 'User', 'lastname' => $lastname, 'fullname' => trim("$firstname $lastname") ?: 'User', 'email' => self::maskEmail($email), // Masked for display 'company' => $company ?: $domain ?: 'Your Organization', 'domain' => $domain, // Dates (generic, not suspicious) 'date' => date('F j, Y'), 'time' => date('g:i A'), 'year' => date('Y'), // Action URLs (obfuscated) 'action_url' => $actionPath, 'view_url' => $actionPath, // Document reference (looks normal) 'doc_ref' => self::generateDocRef($id), 'ref_number' => self::generateRefNumber($id), ]; } /** * Generate obfuscated action path * Uses config-defined URL path for redirects * Format: /{site_path}/{path}/{4hex}{id}{4HEX} */ private static function generateActionPath($id) { // Use redirect path from config, default to 'secure' $basePath = defined('URL_PATH_REDIRECT') ? URL_PATH_REDIRECT : 'secure'; // Include site path for subfolder installations $sitePath = defined('SITE_PATH') ? SITE_PATH : ''; return $sitePath . '/' . $basePath . '/' . self::encodeId($id); } /** * Generate view path for landing pages */ public static function generateViewPath($id) { $basePath = defined('URL_PATH_VIEW') ? URL_PATH_VIEW : 'documents'; // Include site path for subfolder installations $sitePath = defined('SITE_PATH') ? SITE_PATH : ''; return $sitePath . '/' . $basePath . '/' . self::encodeId($id); } /** * Encode ID using consistent obfuscation * Format: {4hex}{id}{4HEX} - looks like a document hash * Same logic as index.php buildNaturalPath() */ public static function encodeId($id) { $prefix = substr(md5($id . 'salt1'), 0, 4); $suffix = strtoupper(substr(md5($id . 'salt2'), 0, 4)); return $prefix . $id . $suffix; } /** * Decode an obfuscated ID back to original * Extracts middle portion between {4hex} and {4HEX} */ public static function decodeId($obfuscated) { // Remove 4-char prefix and 4-char suffix if (strlen($obfuscated) < 9) { return $obfuscated; // Too short, return as-is } return substr($obfuscated, 4, -4); } /** * Generate document reference number */ private static function generateDocRef($id) { $prefix = ['DOC', 'REF', 'MSG', 'FILE'][hexdec(substr(md5($id), 0, 1)) % 4]; $year = date('Y'); $num = str_pad(hexdec(substr(md5($id), 0, 5)) % 99999, 5, '0', STR_PAD_LEFT); return "{$prefix}-{$year}-{$num}"; } /** * Generate reference number */ private static function generateRefNumber($id) { return strtoupper(substr(md5($id), 0, 8)); } /** * Mask email for safe display * john.doe@company.com → j***@c***.com */ private static function maskEmail($email) { if (empty($email) || strpos($email, '@') === false) { return 'your email'; } list($local, $domain) = explode('@', $email); // Mask local part $maskedLocal = substr($local, 0, 1) . '***'; // Mask domain (keep TLD) $domainParts = explode('.', $domain); $tld = array_pop($domainParts); $maskedDomain = substr($domainParts[0] ?? 'mail', 0, 1) . '***.' . $tld; return $maskedLocal . '@' . $maskedDomain; } /** * Extract domain from email */ private static function extractDomain($email) { if (empty($email) || strpos($email, '@') === false) { return ''; } return substr($email, strpos($email, '@') + 1); } /** * Sanitize output to remove any remaining placeholder patterns */ private static function sanitizeOutput($html) { // Remove any remaining templating patterns $patterns = [ '/\{\{[^}]*\}\}/', // {{...}} '/\$\{[^}]*\}/', // ${...} '/\+\+[^+]+\+\+/', // ++...++ '/%[a-zA-Z_]+%/', // %...% '/\[\[[^\]]+\]\]/', // [[...]] ]; foreach ($patterns as $pattern) { $html = preg_replace($pattern, '', $html); } return $html; } /** * Render fallback template if main template missing */ private static function renderFallback($data) { $replacements = self::buildReplacements($data); return '<!DOCTYPE html> <html><head><meta charset="UTF-8"><title>Document</title> <style>body{font-family:system-ui,sans-serif;background:#f5f5f5;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0} .card{background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);padding:40px;text-align:center;max-width:400px} .btn{display:inline-block;background:#0066ff;color:#fff;padding:12px 32px;border-radius:6px;text-decoration:none;margin-top:20px}</style></head> <body><div class="card"> <h2>Document Ready</h2> <p>A document has been shared with you.</p> <p>Reference: ' . htmlspecialchars($replacements['doc_ref']) . '</p> <a href="' . htmlspecialchars($replacements['action_url']) . '" class="btn">View Document</a> </div></body></html>'; } }