false, 'message' => 'Message ID required'], 400); } updateMessage($id); break; case 'DELETE': requireAuth(); if (!$id) { jsonResponse(['success' => false, 'message' => 'Message ID required'], 400); } deleteMessage($id); break; default: jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405); } function getMessages() { global $db; $messages = $db->getAll('messages'); // Apply filters $status = $_GET['status'] ?? null; $limit = (int)($_GET['limit'] ?? 20); $offset = (int)($_GET['offset'] ?? 0); if ($status) { $messages = array_filter($messages, function($m) use ($status) { return $m['status'] === $status; }); } // Sort by date (newest first) usort($messages, function($a, $b) { return strtotime($b['created_at']) - strtotime($a['created_at']); }); $total = count($messages); $unread = count(array_filter($messages, function($m) { return ($m['status'] ?? 'unread') === 'unread'; })); $messages = array_slice(array_values($messages), $offset, $limit); jsonResponse([ 'success' => true, 'data' => $messages, 'total' => $total, 'unread' => $unread, 'limit' => $limit, 'offset' => $offset ]); } function getMessage($id) { global $db; $message = $db->get('messages', $id); if (!$message) { jsonResponse(['success' => false, 'message' => 'Message not found'], 404); } // Mark as read if (($message['status'] ?? 'unread') === 'unread') { $db->update('messages', $id, ['status' => 'read']); $message['status'] = 'read'; } jsonResponse([ 'success' => true, 'data' => $message ]); } function submitMessage() { global $db; requireSameOriginRequest(); // Rate limiting: max 5 submissions per IP per hour $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; $oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour')); $now = date('Y-m-d H:i:s'); $recentCount = $db->countByDateRange('messages', 'created_at', $oneHourAgo, $now, [ 'ip_address' => $ip ]); if ($recentCount >= 5) { jsonResponse(['success' => false, 'message' => 'Too many submissions. Please try again later.'], 429); } $data = getRequestBody(); // ── Cloudflare Turnstile verification (active only when TURNSTILE_SECRET_KEY is set) ── $turnstileSecret = env('TURNSTILE_SECRET_KEY', ''); if ($turnstileSecret !== '') { $turnstileToken = $data['cf-turnstile-response'] ?? ''; if (empty($turnstileToken)) { jsonResponse(['success' => false, 'message' => 'Human verification is required.'], 400); } if (function_exists('curl_init')) { $ch = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ 'secret' => $turnstileSecret, 'response' => $turnstileToken, 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '' ]) ]); $tvResult = json_decode(curl_exec($ch), true); curl_close($ch); if (!($tvResult['success'] ?? false)) { jsonResponse(['success' => false, 'message' => 'Human verification failed. Please try again.'], 400); } } } // Validate required fields $required = ['first_name', 'last_name', 'email', 'message']; foreach ($required as $field) { if (empty($data[$field])) { jsonResponse(['success' => false, 'message' => ucfirst(str_replace('_', ' ', $field)) . ' is required'], 400); } } // Validate email if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { jsonResponse(['success' => false, 'message' => 'Invalid email address'], 400); } // Honeypot check (if implemented in form) if (!empty($data['website'])) { // Likely a bot jsonResponse(['success' => true, 'message' => 'Message sent successfully']); } // Prepare message data $messageData = [ 'first_name' => sanitize($data['first_name']), 'last_name' => sanitize($data['last_name']), 'email' => sanitize($data['email']), 'phone' => sanitize($data['phone'] ?? ''), 'company' => sanitize($data['company'] ?? ''), 'service' => sanitize($data['service'] ?? ''), 'budget' => sanitize($data['budget'] ?? ''), 'message' => sanitize($data['message']), 'newsletter' => $data['newsletter'] ?? false, 'status' => 'unread', 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '' ]; $message = $db->insert('messages', $messageData); // Send email notification (optional) sendNotificationEmail($messageData); // Handle newsletter subscription if (!empty($data['newsletter'])) { subscribeNewsletter($data['email'], $data['first_name'] . ' ' . $data['last_name']); } jsonResponse([ 'success' => true, 'message' => 'Thank you for your message! We\'ll be in touch soon.' ], 201); } function updateMessage($id) { global $db; $existing = $db->get('messages', $id); if (!$existing) { jsonResponse(['success' => false, 'message' => 'Message not found'], 404); } $data = getRequestBody(); $allowed = ['status', 'notes']; $updateData = []; foreach ($allowed as $field) { if (isset($data[$field])) { $updateData[$field] = $data[$field]; } } $message = $db->update('messages', $id, $updateData); jsonResponse([ 'success' => true, 'message' => 'Message updated successfully', 'data' => $message ]); } function deleteMessage($id) { global $db; $existing = $db->get('messages', $id); if (!$existing) { jsonResponse(['success' => false, 'message' => 'Message not found'], 404); } $db->delete('messages', $id); jsonResponse([ 'success' => true, 'message' => 'Message deleted successfully' ]); } function sendNotificationEmail($data) { $to = getSetting('admin_email', getSetting('contact_email', ADMIN_EMAIL)); $subject = 'New Contact Form Submission - ' . SITE_NAME; $plain = "You have received a new message from your website contact form.\n\n"; $plain .= "Name: {$data['first_name']} {$data['last_name']}\n"; $plain .= "Email: {$data['email']}\n"; $plain .= "Phone: {$data['phone']}\n"; $plain .= "Company: {$data['company']}\n"; $plain .= "Service Interest: {$data['service']}\n"; $plain .= "Budget: {$data['budget']}\n\n"; $plain .= "Message:\n{$data['message']}\n"; $html = '
Name: ' . htmlspecialchars($data['first_name'] . ' ' . $data['last_name']) . '
' . 'Email: ' . htmlspecialchars($data['email']) . '
' . 'Phone: ' . htmlspecialchars($data['phone']) . '
' . 'Company: ' . htmlspecialchars($data['company']) . '
' . 'Service Interest: ' . htmlspecialchars($data['service']) . '
' . 'Budget: ' . htmlspecialchars($data['budget']) . '
' . 'Message:
' . nl2br(htmlspecialchars($data['message'])) . '