connect_error) { @error_log("Database Connection Error in openDatabaseConnection: " . $conn->connect_error, 3, dirname(dirname(__DIR__)) . "/logs/db_errors.log"); return null; } return $conn; } function progress_log($message, $context = null) { try { // Site root (same logic you already use elsewhere) $rootPath = dirname(dirname(__DIR__)); $logFile = $rootPath . '/progress.log'; $timestamp = date('Y-m-d H:i:s'); // Normalize message if (is_array($message) || is_object($message)) { $message = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } // Normalize context (optional extra data) if ($context !== null) { if (is_array($context) || is_object($context)) { $context = json_encode($context, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } $message .= ' | CONTEXT: ' . $context; } $line = "[{$timestamp}] {$message}" . PHP_EOL; // Append atomically file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX); } catch (Throwable $e) { // Never allow logging failures to break execution // Silent by design } } function getPriceByDescription($description) { $conn = openDatabaseConnection(); $stmt = $conn->prepare("SELECT amount FROM prices WHERE description = ? LIMIT 1"); if (!$stmt) { return null; } $stmt->bind_param("s", $description); $stmt->execute(); $stmt->bind_result($amount); if ($stmt->fetch()) { $stmt->close(); return $amount; } else { $stmt->close(); return null; } } function getTripCount() { // Database connection $conn = openDatabaseConnection(); // SQL query to count the number of upcoming trips $stmt = $conn->prepare("SELECT COUNT(*) AS total FROM trips WHERE published = ? AND start_date > CURDATE()"); $published = 1; $stmt->bind_param("i", $published); $stmt->execute(); $result = $stmt->get_result(); // Fetch the count from the result if ($result->num_rows > 0) { $row = $result->fetch_assoc(); $totalTrips = $row['total']; } else { $totalTrips = 0; } // Close connection $conn->close(); // Return the number of rows return $totalTrips; } function convertDate($dateString) { // Create a DateTime object from the input date string $date = DateTime::createFromFormat('Y-m-d', $dateString); // Check if the date is valid if ($date) { // Format the date as 'D, d M Y' return $date->format('D, d M Y'); } else { // Return an error message if the date is invalid return "Invalid date format"; } } function calculateDaysAndNights($startDate, $endDate) { // Create DateTime objects for both start and end dates $start = DateTime::createFromFormat('Y-m-d', $startDate); $end = DateTime::createFromFormat('Y-m-d', $endDate); // Check if both dates are valid if ($start && $end) { // Calculate the difference between the two dates $interval = $start->diff($end); // Number of days includes the start date, so we add 1 day to the difference $days = $interval->days + 1; // Number of nights is one less than the number of days $nights = $days - 1; // Return the formatted result return "$days days $nights nights"; } else { // Return an error message if the dates are invalid return "Invalid date format"; } } function sendVerificationEmail($email, $name, $token) { $message = [ 'Messages' => [ [ 'From' => [ 'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ 'Email' => $email, 'Name' => $name ] ], 'TemplateID' => 6689736, 'TemplateLanguage' => true, 'Subject' => "4WDCSA - Verify your Email", 'Variables' => [ 'token' => $token, 'first_name' => $name ] ] ] ]; $client = new Client([ 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { return true; } else { return false; } } } function sendInvoice($email, $name, $eft_id, $amount, $description) { $message = [ 'Messages' => [ [ 'From' => [ 'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ 'Email' => $email, 'Name' => $name ] ], 'TemplateID' => 6891432, 'TemplateLanguage' => true, 'Subject' => "4WDCSA - Thank you for your booking.", 'Variables' => [ 'eft_id' => $eft_id, 'amount' => $amount, 'description' => $description, ] ] ] ]; $client = new Client([ 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { return true; } else { return false; } } } function getEFTDetails($eft_id) { $conn = openDatabaseConnection(); $stmt = $conn->prepare("SELECT amount, description FROM efts WHERE eft_id = ? LIMIT 1"); $stmt->bind_param("s", $eft_id); $stmt->execute(); $result = $stmt->get_result(); if ($row = $result->fetch_assoc()) { return [ 'amount' => $row['amount'], 'description' => $row['description'] ]; } else { return false; // EFT not found } } function sendPOP($fullname, $eft_id, $amount, $description) { // Build the 'To' array from environment variables $toAddresses = []; // Parse comma-separated email addresses from .env $emailsEnv = $_ENV['POP_NOTIFICATION_EMAILS'] ?? ''; if (!empty($emailsEnv)) { $emails = array_map('trim', explode(',', $emailsEnv)); foreach ($emails as $email) { if (!empty($email)) { $toAddresses[] = ['Email' => $email]; } } } // Fallback to default if no emails configured if (empty($toAddresses)) { $toAddresses = [ ['Email' => 'info@4wdcsa.co.za'] ]; } $message = [ 'Messages' => [ [ 'From' => [ 'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin' ], 'To' => $toAddresses, 'TemplateID' => 7054062, 'TemplateLanguage' => true, 'Subject' => "4WDCSA - Proof of Payment Received", 'Variables' => [ 'fullname' => $fullname, 'eft_id' => $eft_id, 'amount' => $amount, 'description' => $description, ] ] ] ]; $client = new Client([ 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { return true; } else { return false; } } } function sendEmail($email, $subject, $message) { $messageData = [ 'Messages' => [ [ 'From' => [ 'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ 'Email' => $email ] ], 'Subject' => $subject, 'TextPart' => $message ] ] ]; $client = new Client([ 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $messageData, 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { return true; } else { return false; } } } function sendAdminNotification($subject, $message) { $mail = [ 'Messages' => [ [ 'From' => [ 'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ 'Email' => $_ENV['NOTIFICATION_ADDR'], 'Name' => 'Jacqui Boshoff' ] ], 'TemplateID' => 6896720, 'TemplateLanguage' => true, 'Subject' => $subject, 'Variables' => [ 'message' => $message, ] ] ] ]; $client = new Client([ 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $mail, 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { return true; } else { return false; } } } function sendPaymentConfirmation($email, $name, $description) { $message = [ 'Messages' => [ [ 'From' => [ 'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Name' => $_ENV['MAILJET_FROM_NAME'] ], 'To' => [ [ 'Email' => $email, 'Name' => $name ] ], 'TemplateID' => 6896744, 'TemplateLanguage' => true, 'Subject' => '4WDCSA - Payment Confirmation', 'Variables' => [ 'description' => $description, 'name' => $name, ] ] ] ]; $client = new Client([ 'base_uri' => 'https://api.mailjet.com/v3.1/', ]); $response = $client->request('POST', 'send', [ 'json' => $message, 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] ]); if ($response->getStatusCode() == 200) { $body = $response->getBody(); $response = json_decode($body); if ($response->Messages[0]->Status == 'success') { return true; } else { return false; } } } function getUserMemberStatus($user_id) { $conn = openDatabaseConnection(); // Return early if no database connection if ($conn === null) { return false; } // Step 1: Check if the user is a direct member $queryUser = "SELECT member FROM users WHERE user_id = ?"; $stmtUser = $conn->prepare($queryUser); if (!$stmtUser) { error_log("Failed to prepare user query: " . $conn->error); return false; } $stmtUser->bind_param('i', $user_id); $stmtUser->execute(); $resultUser = $stmtUser->get_result(); $stmtUser->close(); if ($resultUser->num_rows === 0) { error_log("User not found for user_id: $user_id"); return false; } // Step 2: Check the membership_application table for accept_indemnity status $queryApplication = "SELECT accept_indemnity FROM membership_application WHERE user_id = ?"; $stmtApplication = $conn->prepare($queryApplication); if (!$stmtApplication) { error_log("Failed to prepare application query: " . $conn->error); return false; } $stmtApplication->bind_param('i', $user_id); $stmtApplication->execute(); $resultApplication = $stmtApplication->get_result(); $stmtApplication->close(); if ($resultApplication->num_rows === 0) { error_log("No membership application found for user_id: $user_id - checking if linked to another membership"); // Check if user is linked to another user's membership $linkedStatus = getUserMembershipLink($user_id); $conn->close(); return $linkedStatus['has_access']; } $application = $resultApplication->fetch_assoc(); $accept_indemnity = $application['accept_indemnity']; // Validate accept_indemnity if ($accept_indemnity !== 1) { error_log("User has not accepted indemnity for user_id: $user_id - checking if linked to another membership"); // User hasn't accepted indemnity directly, but check if they're linked to an active membership $linkedStatus = getUserMembershipLink($user_id); $conn->close(); return $linkedStatus['has_access']; } // Step 3: Check membership fees table for valid payment status and membership_end_date $queryFees = "SELECT payment_status, membership_end_date FROM membership_fees WHERE user_id = ?"; $stmtFees = $conn->prepare($queryFees); if (!$stmtFees) { error_log("Failed to prepare fees query: " . $conn->error); return false; } $stmtFees->bind_param('i', $user_id); $stmtFees->execute(); $resultFees = $stmtFees->get_result(); $stmtFees->close(); if ($resultFees->num_rows === 0) { error_log("Membership fees not found for user_id: $user_id - checking if linked to another membership"); // No direct membership fees, check if linked $linkedStatus = getUserMembershipLink($user_id); $conn->close(); return $linkedStatus['has_access']; } $fees = $resultFees->fetch_assoc(); $payment_status = $fees['payment_status']; $membership_end_date = $fees['membership_end_date']; // Validate payment status and membership_end_date $current_date = new DateTime(); $membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date); if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) { $conn->close(); return true; // Direct membership is active } else { // Direct membership is not active, check if user is linked to another active membership error_log("Direct membership not active for user_id: $user_id - checking linked memberships"); $linkedStatus = getUserMembershipLink($user_id); $conn->close(); return $linkedStatus['has_access']; } } function getUserMemberStatusPending($user_id) { $conn = openDatabaseConnection(); // Return early if no database connection if ($conn === null) { return false; } // Step 1: Check if the user is a member $queryUser = "SELECT member FROM users WHERE user_id = ?"; $stmtUser = $conn->prepare($queryUser); if (!$stmtUser) { error_log("Failed to prepare user query: " . $conn->error); return false; } $stmtUser->bind_param('i', $user_id); $stmtUser->execute(); $resultUser = $stmtUser->get_result(); $stmtUser->close(); if ($resultUser->num_rows === 0) { error_log("User not found for user_id: $user_id"); return false; } // Step 3: Check the membership_application table for accept_indemnity status $queryApplication = "SELECT accept_indemnity FROM membership_application WHERE user_id = ?"; $stmtApplication = $conn->prepare($queryApplication); if (!$stmtApplication) { error_log("Failed to prepare application query: " . $conn->error); return false; } $stmtApplication->bind_param('i', $user_id); $stmtApplication->execute(); $resultApplication = $stmtApplication->get_result(); $stmtApplication->close(); if ($resultApplication->num_rows === 0) { error_log("No membership application found for user_id: $user_id"); return false; } $application = $resultApplication->fetch_assoc(); $accept_indemnity = $application['accept_indemnity']; // Validate accept_indemnity if ($accept_indemnity !== 1) { error_log("User has not accepted indemnity for user_id: $user_id"); return false; } // Step 2: Check membership fees table for valid payment status and membership_end_date $queryFees = "SELECT payment_status, membership_end_date FROM membership_fees WHERE user_id = ?"; $stmtFees = $conn->prepare($queryFees); if (!$stmtFees) { error_log("Failed to prepare fees query: " . $conn->error); return false; } $stmtFees->bind_param('i', $user_id); $stmtFees->execute(); $resultFees = $stmtFees->get_result(); $stmtFees->close(); if ($resultFees->num_rows === 0) { error_log("Membership fees not found for user_id: $user_id"); return false; } $fees = $resultFees->fetch_assoc(); $payment_status = $fees['payment_status']; $membership_end_date = $fees['membership_end_date']; // Validate payment status and membership_end_date $current_date = new DateTime(); $membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date); if ($payment_status === "AWAITING PAYMENT" && $current_date <= $membership_end_date_obj) { return true; // Membership is pending } else { return false; } return false; // Membership is not pending } function checkUserSession() { // Check if user_id is set in the session if (!isset($_SESSION['user_id'])) { // Redirect to login.php if user_id is not set header('Location: login.php'); exit(); // Stop further script execution } } function processPayment($payment_id, $amount, $description) { $conn = openDatabaseConnection(); $status = "AWAITING PAYMENT"; $domain = $_ENV['PAYFAST_DOMAIN']; $user_id = $_SESSION['user_id']; // Insert the order into the orders table $stmt = $conn->prepare(" INSERT INTO payments (payment_id, user_id, amount, status, description) VALUES (?, ?, ?, ?, ?) "); $stmt->bind_param( 'ssdss', $payment_id, $user_id, $amount, $status, $description ); if (!$stmt->execute()) { echo json_encode([ 'status' => 'error', 'message' => $stmt->error, 'error' => $stmt->error ]); exit(); } // Get the last inserted order ID $encryptedId = base64_encode($payment_id); // Return success response /** * @param array $data * @param null $passPhrase * @return string */ function generateSignature($data, $passPhrase = null) { // Create parameter string $pfOutput = ''; foreach ($data as $key => $val) { if (!empty($val)) { $pfOutput .= $key . '=' . urlencode(trim($val)) . '&'; } } // Remove last ampersand $getString = substr($pfOutput, 0, -1); if ($passPhrase !== null) { $getString .= '&passphrase=' . urlencode(trim($passPhrase)); } return md5($getString); } // Construct variables $data = array( // Merchant details 'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'], 'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'], 'return_url' => 'https://' . $domain . '/bookings.php', 'cancel_url' => 'https://' . $domain . '/cancel_booking.php?booking_id=' . $encryptedId, 'notify_url' => 'https://' . $domain . '/confirm.php', // Buyer details 'name_first' => get_user_info('first_name'), // You should have these values from your order process 'name_last' => get_user_info('last_name'), 'email_address' => get_user_info('email'), // Transaction details 'm_payment_id' => $payment_id, // The unique order ID you generated 'amount' => number_format(sprintf('%.2f', $amount), 2, '.', ''), 'item_name' => '4WDCSA: ' . $description // Describe the item(s) or use a generic description ); $signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']); $data['signature'] = $signature; // Determine the PayFast URL based on the mode $testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true'; $pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; // Generate the HTML form with hidden inputs and an auto-submit script $htmlForm = '
'; // JavaScript to automatically submit the form $htmlForm .= ''; // Output the form and script to the browser echo $htmlForm; ob_end_flush(); // Ensure any buffered output is sent to the browser } function createIkhokhaPayment($payment_id, $amount, $description, $publicRef) { // Base requester URL: prefer explicit env var, otherwise build from request $baseUrl = rtrim($_ENV['IKHOKHA_REQUESTER_URL'] ?? ($_SERVER['REQUEST_SCHEME'] ?? 'https') . '://' . ($_SERVER['HTTP_HOST'] ?? ''), '/'); $endpoint = $_ENV['IKHOKHA_ENDPOINT']; $appID = $_ENV['IKHOKHA_APP_ID']; $appSecret = $_ENV['IKHOKHA_APP_SECRET']; $requestBody = [ "entityID" => $payment_id, "externalEntityID" => $payment_id, "amount" => $amount * 100, "currency" => "ZAR", "requesterUrl" => $_ENV['IKHOKHA_REQUESTER_URL'] ?? $baseUrl, "description" => $description, "paymentReference" => $description, "mode" => $_ENV['IKHOKHA_MODE'] ?? 'live', "externalTransactionID" => $payment_id, "urls" => [ "callbackUrl" => $_ENV['IKHOKHA_CALLBACK_URL'], "successPageUrl" => $_ENV['IKHOKHA_SUCCESS_URL'] . "?ref=" . urlencode($publicRef), "failurePageUrl" => $_ENV['IKHOKHA_FAILURE_URL'] . "?ref=" . urlencode($publicRef), "cancelUrl" => $_ENV['IKHOKHA_CANCEL_URL'] . "?ref=" . urlencode($publicRef), ] ]; $stringifiedBody = json_encode($requestBody); $payloadToSign = createPayloadToSign($endpoint, $stringifiedBody); $ikSign = generateSignature($payloadToSign, $appSecret); // Initialize cURL session $ch = curl_init($endpoint); // Set cURL options curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Content-Type: application/json", "IK-APPID: $appID", "IK-SIGN: $ikSign" ]); // Execute cURL session $response = curl_exec($ch); curl_close($ch); // Decode and output the response $resp = json_decode($response, true); // Persist provider metadata into payments table if we have a response $conn = openDatabaseConnection(); if ($conn === null) { return false; } $provider = 'ikhokha'; $provider_payment_id = $resp['paylinkID'] ?? $resp['paylinkId'] ?? $resp['paylink_id'] ?? null; $payment_link = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null; $provider_status = $resp['responseCode'] ?? ($resp['status'] ?? null); $provider_response = json_encode($resp); // Update payments row with provider info. If a paylink was created (responseCode == '00'), keep status awaiting payment. $newStatus = null; if (!empty($payment_link) && ($provider_status === '00' || $provider_status === '0' || $provider_status === 0)) { $newStatus = 'AWAITING PAYMENT'; } if ($newStatus) { $stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ?, status = ? WHERE payment_id = ? LIMIT 1"); if ($stmt) { $stmt->bind_param('sssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $newStatus, $payment_id); $stmt->execute(); $stmt->close(); } } else { $stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ? WHERE payment_id = ? LIMIT 1"); if ($stmt) { $stmt->bind_param('ssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $payment_id); $stmt->execute(); $stmt->close(); } } $conn->close(); return $resp; } function escapeString($str) { $escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str); $cleaned = str_replace('\/', '/', $escaped); return $cleaned; } function createPayloadToSign($urlPath, $body) { $parsedUrl = parse_url($urlPath); $basePath = $parsedUrl['path']; if (!$basePath) { throw new Exception("No path present in the URL"); } $payload = $basePath . $body; $escapedPayloadString = escapeString($payload); return $escapedPayloadString; } function generateSignature($payloadToSign, $secret) { return hash_hmac('sha256', $payloadToSign, $secret); } function processMembershipPayment($payment_id, $amount, $description) { $conn = openDatabaseConnection(); $status = "AWAITING PAYMENT"; $domain = $_ENV['PAYFAST_DOMAIN']; $user_id = $_SESSION['user_id']; // Insert the order into the orders table $stmt = $conn->prepare(" INSERT INTO payments (payment_id, user_id, amount, status, description) VALUES (?, ?, ?, ?, ?) "); $stmt->bind_param( 'ssdss', $payment_id, $user_id, $amount, $status, $description ); if (!$stmt->execute()) { echo json_encode([ 'status' => 'error', 'message' => $stmt->error, 'error' => $stmt->error ]); exit(); } // Get the last inserted order ID $encryptedId = base64_encode($payment_id); // Return success response /** * @param array $data * @param null $passPhrase * @return string */ function generateSignature($data, $passPhrase = null) { // Create parameter string $pfOutput = ''; foreach ($data as $key => $val) { if (!empty($val)) { $pfOutput .= $key . '=' . urlencode(trim($val)) . '&'; } } // Remove last ampersand $getString = substr($pfOutput, 0, -1); if ($passPhrase !== null) { $getString .= '&passphrase=' . urlencode(trim($passPhrase)); } return md5($getString); } // Construct variables $data = array( // Merchant details 'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'], 'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'], 'return_url' => 'https://' . $domain . '/account_settings.php', 'cancel_url' => 'https://' . $domain . '/cancel_application.php?id=' . $encryptedId, 'notify_url' => 'https://' . $domain . '/confirm2.php', // Buyer details 'name_first' => get_user_info('first_name'), // You should have these values from your order process 'name_last' => get_user_info('last_name'), 'email_address' => get_user_info('email'), // Transaction details 'm_payment_id' => $payment_id, // The unique order ID you generated 'amount' => number_format(sprintf('%.2f', $amount), 2, '.', ''), 'item_name' => $description // Describe the item(s) or use a generic description ); $signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']); $data['signature'] = $signature; // Determine the PayFast URL based on the mode $testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true'; $pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; // Generate the HTML form with hidden inputs and an auto-submit script $htmlForm = ''; // JavaScript to automatically submit the form $htmlForm .= ''; // Output the form and script to the browser echo $htmlForm; ob_end_flush(); // Ensure any buffered output is sent to the browser } function processPaymentTest($payment_id, $amount, $description) { $conn = openDatabaseConnection(); $status = "PAID"; $domain = 'www.thepinto.co.za/4wdcsa'; $user_id = $_SESSION['user_id']; // Insert the order into the payments table $stmt = $conn->prepare(" INSERT INTO payments (payment_id, user_id, amount, status, description) VALUES (?, ?, ?, ?, ?) "); $stmt->bind_param( 'ssdss', $payment_id, $user_id, $amount, $status, $description ); if (!$stmt->execute()) { echo json_encode([ 'status' => 'error', 'message' => $stmt->error, 'error' => $stmt->error ]); exit(); } // Update the bookings table to set the status to "PAID" $updateStmt = $conn->prepare(" UPDATE bookings SET status = 'PAID' WHERE payment_id = ? "); $updateStmt->bind_param('s', $payment_id); if (!$updateStmt->execute()) { echo json_encode([ 'status' => 'error', 'message' => $updateStmt->error, 'error' => $updateStmt->error ]); exit(); } // Success response echo json_encode([ 'status' => 'success', 'message' => 'Payment processed and booking status updated.' ]); $stmt->close(); $updateStmt->close(); $conn->close(); // Redirect to bookings.php with the booking_id parameter header("Location: bookings.php"); exit(); // Ensure no further code is executed after the redirect } function processZeroPayment($payment_id, $amount, $description) { $conn = openDatabaseConnection(); $status = "BOOKED"; $user_id = $_SESSION['user_id']; // Update the bookings table to set the status to "PAID" $updateStmt = $conn->prepare(" UPDATE bookings SET status = 'PAID' WHERE payment_id = ? "); $updateStmt->bind_param('s', $payment_id); if (!$updateStmt->execute()) { echo json_encode([ 'status' => 'error', 'message' => $updateStmt->error, 'error' => $updateStmt->error ]); exit(); } // Success response echo json_encode([ 'status' => 'success', 'message' => 'Payment processed and booking status updated.' ]); $updateStmt->close(); $conn->close(); // Redirect to bookings.php with the booking_id parameter header("Location: bookings.php"); exit(); // Ensure no further code is executed after the redirect } function get_user_info($info) { if (!isset($_SESSION['user_id'])) { return "User is not logged in."; } // Get the user_id from the session $user_id = $_SESSION['user_id']; $conn = openDatabaseConnection(); $query = "SELECT $info FROM users WHERE user_id = ?"; // Prepare the statement if ($stmt = $conn->prepare($query)) { // Bind the user_id parameter $stmt->bind_param("i", $user_id); // Execute the query $stmt->execute(); // Bind the result to a variable $stmt->bind_result($info); // Fetch the result if ($stmt->fetch()) { // Return the requested variable return $info; } else { // Return null if no result is found return null; } // Close the statement $stmt->close(); } else { // Handle query preparation error die("Query preparation failed: " . $conn->error); } } function getAvailableSpaces($trip_id) { $conn = openDatabaseConnection(); try { // Ensure trip_id is an integer to prevent SQL injection $trip_id = intval($trip_id); // Step 1: Get the vehicle capacity for the trip from the trips table $stmt = $conn->prepare("SELECT vehicle_capacity FROM trips WHERE trip_id = ?"); $stmt->bind_param("i", $trip_id); $stmt->execute(); $result = $stmt->get_result(); // Check if the trip exists if ($result->num_rows === 0) { return "Trip not found."; } // Fetch the vehicle capacity $trip = $result->fetch_assoc(); $vehicle_capacity = $trip['vehicle_capacity']; // Step 2: Get the total number of booked vehicles for this trip from the bookings table $stmt = $conn->prepare("SELECT SUM(num_vehicles) as total_booked FROM bookings WHERE trip_id = ?"); $stmt->bind_param("i", $trip_id); $stmt->execute(); $result = $stmt->get_result(); // Fetch the total number of vehicles booked $bookings = $result->fetch_assoc(); $total_booked = $bookings['total_booked'] ?? 0; // Default to 0 if no bookings // Step 3: Calculate the available spaces $available_spaces = $vehicle_capacity - $total_booked; // Return the result (available spaces) return max($available_spaces, 0); // Ensure that available spaces cannot be negative } catch (Exception $e) { // If there's an error, return the error message return "Error: " . $e->getMessage(); } } function countUpcomingBookings($user_id) { $conn = openDatabaseConnection(); // Prepare the SQL query to count upcoming bookings $sql = "SELECT COUNT(*) AS upcoming_count FROM bookings WHERE user_id = ? AND to_date >= CURDATE()"; // Prepare the statement $stmt = $conn->prepare($sql); if ($stmt === false) { return "Error preparing statement: " . $conn->error; } // Bind parameters $stmt->bind_param("i", $user_id); // Execute the query $stmt->execute(); // Get the result $result = $stmt->get_result(); if ($result) { $row = $result->fetch_assoc(); return $row['upcoming_count']; } else { return "Error executing query: " . $stmt->error; } } function getUserRole() { $conn = openDatabaseConnection(); // Start the session if not already started if (session_status() === PHP_SESSION_NONE) { session_start(); } // Return early if no database connection if ($conn === null) { return null; } // Check if the user_id is set in the session if (!isset($_SESSION['user_id'])) { return null; // or handle the case where the user is not logged in } $user_id = $_SESSION['user_id']; $role = null; // Prepare the SQL statement $stmt = $conn->prepare("SELECT role FROM users WHERE user_id = ?"); if ($stmt) { // Bind the user_id parameter to the query $stmt->bind_param("i", $user_id); // Execute the query $stmt->execute(); // Bind the result to a variable $stmt->bind_result($role); // Fetch the result $stmt->fetch(); // Close the statement $stmt->close(); } else { // Handle errors in statement preparation error_log("Database error: " . $conn->error); } return $role; } function checkAdmin() { $conn = openDatabaseConnection(); // Ensure the user is logged in if (!isset($_SESSION['user_id'])) { header('Location: index.php'); // echo "user not logged in"; exit; } $userId = $_SESSION['user_id']; // Query to check the role $stmt = $conn->prepare("SELECT role FROM users WHERE user_id = ?"); $stmt->bind_param('i', $userId); $stmt->execute(); $result = $stmt->get_result(); // Fetch the result if ($row = $result->fetch_assoc()) { $role = $row['role']; // If the role is not admin or superadmin, redirect to index.php if ($role !== 'admin' && $role !== 'superadmin') { header('Location: index.php'); // echo "user is not admin or superadmin"; exit; } } else { // No user found, redirect to index.php header('Location: index.php'); // echo "No user found"; exit; } // Close the statement and connection $stmt->close(); $conn->close(); } function checkSuperAdmin() { $conn = openDatabaseConnection(); // Ensure the user is logged in if (!isset($_SESSION['user_id'])) { header('Location: index.php'); // echo "user not logged in"; exit; } $userId = $_SESSION['user_id']; // Query to check the role $stmt = $conn->prepare("SELECT role FROM users WHERE user_id = ?"); $stmt->bind_param('i', $userId); $stmt->execute(); $result = $stmt->get_result(); // Fetch the result if ($row = $result->fetch_assoc()) { $role = $row['role']; // If the role is not admin or superadmin, redirect to index.php if ($role !== 'superadmin') { header('Location: index.php'); // echo "user is not admin or superadmin"; exit; } } else { // No user found, redirect to index.php header('Location: index.php'); // echo "No user found"; exit; } // Close the statement and connection $stmt->close(); $conn->close(); } function calculateProrata($prorata) { // Get current month number (1 = January, 12 = December) $currentMonth = date('n'); // Shift months so March becomes month 1 in the cycle // (March=1, April=2, ..., February=12) $shiftedMonth = ($currentMonth - 3 + 12) % 12 + 1; // Total months in a "March to February" year $totalMonths = 12; // Calculate remaining months including the current month $remainingMonths = $totalMonths - $shiftedMonth + 1; // Multiply by prorata value return $remainingMonths * $prorata; } function getFullName($user_id) { $conn = openDatabaseConnection(); // Prepare the SQL query to get first_name and last_name $query = "SELECT first_name, last_name FROM users WHERE user_id = ?"; // Prepare the statement if ($stmt = $conn->prepare($query)) { // Bind the user_id parameter to the query $stmt->bind_param("i", $user_id); // Execute the query $stmt->execute(); // Bind the result to variables $stmt->bind_result($first_name, $last_name); // Fetch the data if ($stmt->fetch()) { // Return the full name by concatenating first_name and last_name return $first_name . " " . $last_name; } else { // Handle case where no records are found return null; // No user found with the given user_id } // Close the statement $stmt->close(); } else { // Handle query preparation failure throw new Exception("Query preparation failed: " . $conn->error); } } function getEmail($user_id) { $conn = openDatabaseConnection(); // Prepare the SQL query to get first_name and last_name $query = "SELECT email FROM users WHERE user_id = ?"; // Prepare the statement if ($stmt = $conn->prepare($query)) { // Bind the user_id parameter to the query $stmt->bind_param("i", $user_id); // Execute the query $stmt->execute(); // Bind the result to variables $stmt->bind_result($email); // Fetch the data if ($stmt->fetch()) { // Return the full name by concatenating first_name and last_name return $email; } else { // Handle case where no records are found return null; // No user found with the given user_id } // Close the statement $stmt->close(); } else { // Handle query preparation failure throw new Exception("Query preparation failed: " . $conn->error); } } function getProfilePic($user_id) { $conn = openDatabaseConnection(); // Prepare the SQL query to get first_name and last_name $query = "SELECT profile_pic FROM users WHERE user_id = ?"; // Prepare the statement if ($stmt = $conn->prepare($query)) { // Bind the user_id parameter to the query $stmt->bind_param("i", $user_id); // Execute the query $stmt->execute(); // Bind the result to variables $stmt->bind_result($profilepic); // Fetch the data if ($stmt->fetch()) { // Return the full name by concatenating first_name and last_name return $profilepic; } else { // Handle case where no records are found return null; // No user found with the given user_id } // Close the statement $stmt->close(); } else { // Handle query preparation failure throw new Exception("Query preparation failed: " . $conn->error); } } function getInitialSurname($user_id) { $conn = openDatabaseConnection(); $query = "SELECT first_name, last_name FROM users WHERE user_id = ?"; if ($stmt = $conn->prepare($query)) { $stmt->bind_param("i", $user_id); $stmt->execute(); $stmt->bind_result($first_name, $last_name); if ($stmt->fetch()) { $initial = strtoupper(substr($first_name, 0, 1)); return $initial . ". " . $last_name; } else { return null; } $stmt->close(); } else { throw new Exception("Query preparation failed: " . $conn->error); } } function getLastName($user_id) { $conn = openDatabaseConnection(); // Prepare the SQL query to get first_name and last_name $query = "SELECT last_name FROM users WHERE user_id = ?"; // Prepare the statement if ($stmt = $conn->prepare($query)) { // Bind the user_id parameter to the query $stmt->bind_param("i", $user_id); // Execute the query $stmt->execute(); // Bind the result to variables $stmt->bind_result($last_name); // Fetch the data if ($stmt->fetch()) { return $last_name; } else { // Handle case where no records are found return null; // No user found with the given user_id } // Close the statement $stmt->close(); } else { // Handle query preparation failure throw new Exception("Query preparation failed: " . $conn->error); } } function addEFT($eft_id, $booking_id, $user_id, $status, $amount, $description) { // Database connection $conn = openDatabaseConnection(); // Prepare the SQL statement $stmt = $conn->prepare("INSERT INTO efts (eft_id, booking_id, user_id, status, amount, description) VALUES (?, ?, ?, ?, ?, ?)"); if (!$stmt) { die("Prepare failed: " . $conn->error); } // Bind parameters $stmt->bind_param("siisds", $eft_id, $booking_id, $user_id, $status, $amount, $description); // Execute the statement and check for errors if ($stmt->execute()) { // echo "EFT record added successfully."; } else { // echo "Error inserting EFT: " . $stmt->error; } // Close the statement and connection $stmt->close(); $conn->close(); } function addSubsEFT($eft_id, $user_id, $status, $amount, $description) { // Database connection $conn = openDatabaseConnection(); // Prepare the SQL statement $stmt = $conn->prepare("INSERT INTO efts (eft_id, user_id, status, amount, description) VALUES (?, ?, ?, ?, ?)"); if (!$stmt) { die("Prepare failed: " . $conn->error); } // Bind parameters $stmt->bind_param("sisds", $eft_id, $user_id, $status, $amount, $description); // Execute the statement and check for errors if ($stmt->execute()) { // echo "EFT record added successfully."; } else { // echo "Error inserting EFT: " . $stmt->error; } // Close the statement and connection $stmt->close(); $conn->close(); } function encryptData($input, $salt) { $method = "AES-256-CBC"; $key = hash('sha256', $salt, true); $iv = substr(hash('sha256', $salt . 'iv'), 0, 16); // Generate IV from salt return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(openssl_encrypt($input, $method, $key, OPENSSL_RAW_DATA, $iv))); } function decryptData($encrypted, $salt) { $method = "AES-256-CBC"; $key = hash('sha256', $salt, true); $iv = substr(hash('sha256', $salt . 'iv'), 0, 16); // Generate IV from salt $encrypted = str_replace(['-', '_'], ['+', '/'], $encrypted); return openssl_decrypt(base64_decode($encrypted), $method, $key, OPENSSL_RAW_DATA, $iv); } function hasAcceptedIndemnity($user_id) { // Database connection $conn = openDatabaseConnection(); // Prepare the SQL statement $stmt = $conn->prepare("SELECT accept_indemnity FROM membership_application WHERE user_id = ?"); if (!$stmt) { return false; // Query preparation failed } // Bind the parameter and execute the statement $stmt->bind_param("i", $user_id); $stmt->execute(); // Get the result $stmt->bind_result($accepted_indemnity); $stmt->fetch(); // Close the statement $stmt->close(); // Return true if indemnity is accepted (assuming 1 means accepted) return (bool) $accepted_indemnity; } function checkMembershipApplication($user_id) { // Database connection $conn = openDatabaseConnection(); // Prepare the SQL query to check if the record exists $sql = "SELECT COUNT(*) FROM membership_application WHERE user_id = ?"; $stmt = $conn->prepare($sql); $stmt->bind_param("i", $user_id); // "i" is the type for integer // Execute the query $stmt->execute(); $stmt->bind_result($count); $stmt->fetch(); // Close the prepared statement and connection $stmt->close(); $conn->close(); // Check if the record exists and redirect if ($count > 0) { // Set a session message before redirecting if (!isset($_SESSION['message'])) { $_SESSION['message'] = 'You have already submitted a membership application.'; } header("Location: membership_details.php"); exit(); } } function checkMembershipApplication2($user_id) { // Database connection $conn = openDatabaseConnection(); // Prepare the SQL query to check if the record exists $sql = "SELECT COUNT(*) FROM membership_application WHERE user_id = ?"; $stmt = $conn->prepare($sql); $stmt->bind_param("i", $user_id); // "i" is the type for integer // Execute the query $stmt->execute(); $stmt->bind_result($count); $stmt->fetch(); // Close the prepared statement and connection $stmt->close(); $conn->close(); // Check if the record exists and redirect if ($count < 1) { header("Location: membership.php"); exit(); } } function checkMembershipPaymentStatus($user_id) { // Open database connection $conn = openDatabaseConnection(); // Query to check the payment status for the given user_id $query = "SELECT payment_status FROM membership_fees WHERE user_id = ?"; $stmt = $conn->prepare($query); // Check if the query preparation was successful if (!$stmt) { error_log("Failed to prepare payment status query: " . $conn->error); return false; } // Bind the user_id parameter to the query $stmt->bind_param('i', $user_id); // Execute the query $stmt->execute(); // Get the result $result = $stmt->get_result(); // Close the prepared statement $stmt->close(); // Check if any record is found for the user_id if ($result->num_rows === 0) { error_log("No payment record found for user_id: $user_id"); return false; // No payment record found } // Fetch the payment status $payment = $result->fetch_assoc(); $payment_status = $payment['payment_status']; // Check if the payment status is "PAID" if ($payment_status === 'PAID') { return true; // Payment has been made } return false; // Payment has not been made } function checkAndRedirectBooking($trip_id) { // Open database connection $conn = openDatabaseConnection(); if (!isset($_SESSION['user_id'])) { die("User not logged in."); } $user_id = $_SESSION['user_id']; // Prepare and execute the SQL query $stmt = $conn->prepare("SELECT COUNT(*) FROM bookings WHERE user_id = ? AND trip_id = ?"); $stmt->bind_param("ii", $user_id, $trip_id); $stmt->execute(); $stmt->bind_result($count); $stmt->fetch(); $stmt->close(); // Redirect if booking exists if ($count > 0) { $_SESSION['message'] = "You already have an active booking for this trip. Please contact info@4wdcsa.co.za for further assistance."; header("Location: bookings.php"); exit(); } } function checkAndRedirectCourseBooking($course_id) { // Open database connection $conn = openDatabaseConnection(); if (!isset($_SESSION['user_id'])) { die("User not logged in."); } $user_id = $_SESSION['user_id']; // Prepare and execute the SQL query $stmt = $conn->prepare("SELECT COUNT(*) FROM bookings WHERE user_id = ? AND course_id = ?"); $stmt->bind_param("ii", $user_id, $course_id); $stmt->execute(); $stmt->bind_result($count); $stmt->fetch(); $stmt->close(); // Redirect if booking exists if ($count > 0) { $_SESSION['message'] = "You already have an active booking for this course. Please contact info@4wdcsa.co.za for further assistance."; header("Location: bookings.php"); exit(); } } function countUpcomingTrips() { // Open database connection $conn = openDatabaseConnection(); // Return 0 if no database connection if ($conn === null) { return 0; } $stmt = $conn->prepare("SELECT COUNT(*) AS trip_count FROM trips WHERE published = ? AND start_date > CURDATE()"); $published = 1; $stmt->bind_param("i", $published); $stmt->execute(); if ($result = $stmt->get_result()) { $row = $result->fetch_assoc(); return (int)$row['trip_count']; } else { // Optional: Handle query error error_log("MySQL Error: " . $conn->error); return 0; } } function logVisitor() { if (session_status() === PHP_SESSION_NONE) { session_start(); } $conn = openDatabaseConnection(); // Return early if no database connection if ($conn === null) { return; } // Collect visitor data $ip_address = getUserIP(); $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $country = guessCountry($ip_address); $page_url = "http://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; $referrer_url = $_SERVER['HTTP_REFERER'] ?? null; $visit_time = date("Y-m-d H:i:s"); $user_id = $_SESSION['user_id'] ?? null; // BOT DETECTION: Filter based on user-agent $bot_keywords = ['bot', 'crawl', 'spider', 'slurp', 'wget', 'curl', 'python-requests', 'scrapy', 'httpclient']; foreach ($bot_keywords as $bot_keyword) { if (stripos($user_agent, $bot_keyword) !== false) { return; // Stop logging, it's a bot } } // BOT DETECTION: Check for JavaScript-executed cookie if (!isset($_COOKIE['js_enabled'])) { return; // Could be a bot that doesn't execute JS } // BOT DETECTION (optional): IP blacklist (custom) $blacklisted_ips = ['1.2.3.4', '5.6.7.8']; // Populate with real IPs or from a service if (in_array($ip_address, $blacklisted_ips)) { return; } // Check if IP has accessed the site in the last 30 minutes $stmt = $conn->prepare(" SELECT id FROM visitor_logs WHERE ip_address = ? AND visit_time >= NOW() - INTERVAL 30 MINUTE LIMIT 1 "); if ($stmt) { $stmt->bind_param("s", $ip_address); $stmt->execute(); $stmt->store_result(); $seen_recently = $stmt->num_rows > 0; $stmt->close(); if (!$seen_recently) { // sendEmail('chrispintoza@gmail.com', '4WDCSA: New Visitor', 'A new IP ' . $ip_address . ', has just accessed ' . $page_url); } } // Prepare and insert log $stmt = $conn->prepare("INSERT INTO visitor_logs (ip_address, page_url, referrer_url, visit_time, user_id, country) VALUES (?, ?, ?, ?, ?, ?)"); if ($stmt) { $stmt->bind_param("ssssis", $ip_address, $page_url, $referrer_url, $visit_time, $user_id, $country); $stmt->execute(); $stmt->close(); } } function getUserIP() { if (!empty($_SERVER['HTTP_CLIENT_IP'])) { return $_SERVER['HTTP_CLIENT_IP']; } if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { // In case of multiple IPs (e.g. "client, proxy1, proxy2"), take the first return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } return $_SERVER['REMOTE_ADDR']; } function getNextOpenDayDate() { $conn = openDatabaseConnection(); $stmt = $conn->prepare(" SELECT date FROM events WHERE name = ? AND date >= NOW() ORDER BY date ASC LIMIT 1 "); $event_name = '4WDCSA Open Day'; $stmt->bind_param("s", $event_name); $stmt->execute(); $result = $stmt->get_result(); if ($result && $row = $result->fetch_assoc()) { return $row['date']; // e.g. "2025-05-01 10:00:00" } return null; // No upcoming events found } function formatCurrency($amount, $currency = 'R') { return strtoupper($currency) . ' ' . number_format($amount, 2, '.', ','); } function guessCountry($ip) { // Use cURL instead of file_get_contents for compatibility with allow_url_fopen=0 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://ip-api.com/json/$ip"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 5); $response = curl_exec($ch); curl_close($ch); if ($response === false) { return null; } $data = json_decode($response, true); if ($data && isset($data['status']) && $data['status'] == 'success') { return $data['country']; // e.g., South Africa } return null; } function getUserIdFromEFT($eft_id) { $conn = openDatabaseConnection(); $stmt = $conn->prepare("SELECT user_id FROM efts WHERE eft_id = ?"); if (!$stmt) { // Optional: handle prepare error return null; } $stmt->bind_param("s", $eft_id); // "i" for integer $stmt->execute(); $stmt->bind_result($user_id); if ($stmt->fetch()) { $stmt->close(); return $user_id; } else { $stmt->close(); return null; } } function getEftDescription($eft_id) { $conn = openDatabaseConnection(); $stmt = $conn->prepare("SELECT description FROM efts WHERE eft_id = ?"); $stmt->bind_param("s", $eft_id); // "i" for integer $stmt->execute(); $stmt->bind_result($description); if ($stmt->fetch()) { $stmt->close(); return $description; } else { $stmt->close(); return null; } } function getPrice($description, $memberType) { $conn = openDatabaseConnection(); // Validate member type if (!in_array($memberType, ['member', 'nonmember'])) { throw new InvalidArgumentException("Invalid member type. Must be 'member' or 'nonmember'."); } // Prepare column name based on member type $column = $memberType === 'member' ? 'amount' : 'amount_nonmembers'; // Prepare and execute the SQL query $stmt = $conn->prepare("SELECT $column FROM prices WHERE description = ?"); if (!$stmt) { throw new Exception("Prepare failed: " . $conn->error); } $stmt->bind_param("s", $description); $stmt->execute(); $stmt->bind_result($price); // Fetch and return the result if ($stmt->fetch()) { $stmt->close(); return $price; } else { $stmt->close(); return null; // Or throw an exception if preferred } } function getDetail($description) { $conn = openDatabaseConnection(); // Prepare and execute the SQL query $stmt = $conn->prepare("SELECT detail FROM prices WHERE description = ?"); if (!$stmt) { throw new Exception("Prepare failed: " . $conn->error); } $stmt->bind_param("s", $description); $stmt->execute(); $stmt->bind_result($detail); // Fetch and return the result if ($stmt->fetch()) { $stmt->close(); return $detail; } else { $stmt->close(); return null; // Or throw an exception if preferred } } function getUserType($user_id) { // Prepare the statement to prevent SQL injection $conn = openDatabaseConnection(); $stmt = $conn->prepare("SELECT type FROM users WHERE user_id = ?"); if (!$stmt) { return false; // or handle error } // Bind the parameter $stmt->bind_param("i", $user_id); // Execute the statement $stmt->execute(); // Bind result variable $stmt->bind_result($type); // Fetch the result if ($stmt->fetch()) { $stmt->close(); return $type; } else { $stmt->close(); return null; // or false depending on your error handling preference } } function matchLegacyMember($userId) { $conn = openDatabaseConnection(); // Get the applicant's details $stmt = $conn->prepare("SELECT first_name, last_name, email FROM users WHERE user_id = ?"); $stmt->bind_param("i", $userId); $stmt->execute(); $applicantResult = $stmt->get_result(); if ($applicantResult->num_rows === 0) { return null; // No such user_id } $applicant = $applicantResult->fetch_assoc(); // Fetch all legacy members $result = $conn->query("SELECT * FROM legacy_members"); $bestMatch = null; $highestScore = 0; while ($member = $result->fetch_assoc()) { // Compare full names $nameScore = 0; similar_text( strtolower($applicant['first_name'] . ' ' . $applicant['last_name']), strtolower($member['first_name'] . ' ' . $member['last_name']), $nameScore ); // Compare email $emailScore = 0; if (!empty($applicant['email']) && !empty($member['email'])) { similar_text( strtolower($applicant['email']), strtolower($member['email']), $emailScore ); } // Weighted total score $totalScore = ($nameScore * 0.7) + ($emailScore * 0.3); if ($totalScore > $highestScore && $totalScore >= 70) { $highestScore = $totalScore; $bestMatch = $member; $bestMatch['match_score'] = round($totalScore, 2); // Add score to result } } return $bestMatch; // Returns array or null } function processLegacyMembership($user_id) { // Get legacy match $conn = openDatabaseConnection(); $match = matchLegacyMember($user_id); if ($match) { $legacy_id = $match['legacy_id']; $eftamount = getResultFromTable('legacy_members', 'amount', 'legacy_id', $legacy_id); // Get user info from users table $stmt = $conn->prepare('SELECT first_name, last_name, phone_number, email FROM users WHERE user_id = ?'); $stmt->bind_param('i', $user_id); $stmt->execute(); $stmt->bind_result($first_name, $last_name, $tel_cell, $email); $stmt->fetch(); $stmt->close(); // Insert into membership_application $stmt = $conn->prepare('INSERT INTO membership_application (user_id, first_name, last_name, tel_cell, email) VALUES (?, ?, ?, ?, ?)'); $stmt->bind_param('issss', $user_id, $first_name, $last_name, $tel_cell, $email); $stmt->execute(); $stmt->close(); // Prepare membership fees info $payment_status = "PAID"; $membership_start_date = "2025-01-01"; $membership_end_date = "2025-12-31"; $initial_surname = getInitialSurname($user_id); $payment_id = strtoupper($user_id . " SUBS " . date("Y") . " " . $initial_surname); $description = 'Membership Fees ' . date("Y") . " " . $initial_surname; // Insert into membership_fees $stmt = $conn->prepare('INSERT INTO membership_fees (user_id, payment_amount, payment_status, membership_start_date, membership_end_date, payment_id) VALUES (?, ?, ?, ?, ?, ?)'); $stmt->bind_param('idssss', $user_id, $eftamount, $payment_status, $membership_start_date, $membership_end_date, $payment_id); $stmt->execute(); $stmt->close(); // Add to EFT addSubsEFT($payment_id, $user_id, $payment_status, $eftamount, $description); } } /** * SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection. * ONLY call this function with whitelisted table and column names. * NEVER accept table/column names directly from user input. * * Retrieves a single value from a database table. * @param string $table Table name (MUST be whitelisted - see allowed_tables array) * @param string $column Column name to retrieve (MUST be whitelisted - see allowed_columns array) * @param string $match Column name for WHERE clause (MUST be whitelisted) * @param mixed $identifier Value to match in WHERE clause (parameterized - safe) * @return mixed The result value or null if not found */ function getResultFromTable($table, $column, $match, $identifier) { // WHITELIST: Define allowed tables to prevent table name injection $allowed_tables = [ 'users', 'membership_application', 'membership_fees', 'bookings', 'payments', 'efts', 'trips', 'courses', 'blogs', 'events', 'campsites', 'bar_transactions', 'login_attempts', 'legacy_members' ]; // WHITELIST: Define allowed columns per table (simplified - add more as needed) $allowed_columns = [ 'legacy_members' => ['amount', 'legacy_id', 'email', 'name'], 'users' => ['user_id', 'email', 'first_name', 'last_name', 'phone_number', 'password', 'profile_pic', 'is_verified', 'type', 'locked_until'], 'membership_fees' => ['payment_id', 'user_id', 'amount', 'payment_status', 'payment_date'], 'bookings' => ['booking_id', 'user_id', 'total_amount', 'status', 'booking_type'], 'payments' => ['payment_id', 'user_id', 'amount', 'status'], 'trips' => ['trip_id', 'trip_name', 'description'], 'courses' => ['course_id', 'course_name', 'description'], 'blogs' => ['blog_id', 'title', 'content'], 'events' => ['event_id', 'event_name', 'description'], 'campsites' => ['campsite_id', 'name', 'location'], 'efts' => ['eft_id', 'amount', 'status', 'booking_id'], 'bar_transactions' => ['transaction_id', 'amount', 'date'], 'login_attempts' => ['attempt_id', 'email', 'ip_address', 'success'] ]; // Validate table name is in whitelist if (!in_array($table, $allowed_tables, true)) { error_log("Security Warning: getResultFromTable() called with non-whitelisted table: $table"); return null; } // Validate column name is in whitelist for this table if (!isset($allowed_columns[$table]) || !in_array($column, $allowed_columns[$table], true)) { error_log("Security Warning: getResultFromTable() called with non-whitelisted column: $column for table: $table"); return null; } // Validate match column is in whitelist for this table if (!isset($allowed_columns[$table]) || !in_array($match, $allowed_columns[$table], true)) { error_log("Security Warning: getResultFromTable() called with non-whitelisted match column: $match for table: $table"); return null; } $conn = openDatabaseConnection(); // Use backticks for table and column identifiers (safe after whitelist validation) $sql = "SELECT `" . $column . "` FROM `" . $table . "` WHERE `" . $match . "` = ?"; $stmt = $conn->prepare($sql); if (!$stmt) { error_log("Database prepare error: " . $conn->error); return null; } // Determine parameter type based on identifier $paramType = is_int($identifier) ? 'i' : 's'; $stmt->bind_param($paramType, $identifier); $stmt->execute(); $stmt->bind_result($result); $stmt->fetch(); $stmt->close(); return $result; } function blockBlacklistedIP() { // Get the visitor's IP $conn = openDatabaseConnection(); $ip = getUserIP(); // Prepare and execute the SQL query $stmt = $conn->prepare("SELECT 1 FROM blacklist WHERE ip_address = ?"); $stmt->bind_param("s", $ip); $stmt->execute(); $stmt->store_result(); // If IP is found in blacklist, block access if ($stmt->num_rows > 0) { http_response_code(403); echo "Access denied."; exit; } $stmt->close(); } function getCommentCount($page_id) { // Database connection $conn = openDatabaseConnection(); // Prepare statement to avoid SQL injection $stmt = $conn->prepare("SELECT COUNT(*) FROM comments WHERE page_id = ?"); $stmt->bind_param("s", $page_id); $stmt->execute(); // Get result $stmt->bind_result($count); $stmt->fetch(); // Close connections $stmt->close(); $conn->close(); return $count; } function hasPhoneNumber($user_id) { $conn = openDatabaseConnection(); // Prepare SQL $stmt = $conn->prepare("SELECT phone_number FROM users WHERE id = ? LIMIT 1"); $stmt->bind_param("i", $user_id); $stmt->execute(); $stmt->bind_result($phone_number); $stmt->fetch(); $stmt->close(); // Return true only if a phone number exists and is not empty return !empty($phone_number); } // ==================== CSRF PROTECTION FUNCTIONS ==================== /** * Generates a CSRF token and stores it in the session with expiration * @param int $duration Token expiration time in seconds (default 3600 = 1 hour) * @return string The generated CSRF token */ function generateCSRFToken($duration = 3600) { // Initialize CSRF token storage in session if needed if (!isset($_SESSION['csrf_tokens'])) { $_SESSION['csrf_tokens'] = []; } // Clean up expired tokens cleanupExpiredTokens(); // Generate a random token $token = bin2hex(random_bytes(32)); // Store token with expiration timestamp $_SESSION['csrf_tokens'][$token] = time() + $duration; return $token; } /** * Validates a CSRF token from user input * @param string $token The token to validate (typically from $_POST['csrf_token']) * @return bool True if token is valid, false otherwise */ function validateCSRFToken($token) { // Check if token exists in session if (!isset($_SESSION['csrf_tokens']) || !isset($_SESSION['csrf_tokens'][$token])) { return false; } // Check if token has expired if ($_SESSION['csrf_tokens'][$token] < time()) { unset($_SESSION['csrf_tokens'][$token]); return false; } // Token is valid - remove it from session (single-use) unset($_SESSION['csrf_tokens'][$token]); return true; } /** * Removes expired tokens from the session */ function cleanupExpiredTokens() { if (!isset($_SESSION['csrf_tokens'])) { return; } $currentTime = time(); foreach ($_SESSION['csrf_tokens'] as $token => $expiration) { if ($expiration < $currentTime) { unset($_SESSION['csrf_tokens'][$token]); } } } // ==================== INPUT VALIDATION FUNCTIONS ==================== /** * Validates and sanitizes email input * @param string $email The email to validate * @param int $maxLength Maximum allowed length (default 254 per RFC 5321) * @return string|false Sanitized email or false if invalid */ function validateEmail($email, $maxLength = 254) { // Check length if (strlen($email) > $maxLength) { return false; } // Filter and validate $filtered = filter_var($email, FILTER_VALIDATE_EMAIL); // Sanitize if valid return $filtered ? filter_var($filtered, FILTER_SANITIZE_EMAIL) : false; } /** * Validates phone number format * @param string $phone The phone number to validate * @return string|false Sanitized phone number or false if invalid */ function validatePhoneNumber($phone) { // Remove common formatting characters $cleaned = preg_replace('/[^\d+\-\s().]/', '', $phone); // Check length (between 7 and 20 digits) $digitCount = strlen(preg_replace('/[^\d]/', '', $cleaned)); if ($digitCount < 7 || $digitCount > 20) { return false; } return $cleaned; } /** * Validates and sanitizes a name (first name, last name) * @param string $name The name to validate * @param int $minLength Minimum allowed length (default 2) * @param int $maxLength Maximum allowed length (default 100) * @return string|false Sanitized name or false if invalid */ function validateName($name, $minLength = 2, $maxLength = 100) { // Trim whitespace $name = trim($name); // Check length if (strlen($name) < $minLength || strlen($name) > $maxLength) { return false; } // Allow letters, numbers, spaces, hyphens, and apostrophes if (!preg_match('/^[a-zA-Z0-9\s\'-]+$/', $name)) { return false; } return htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); } /** * Validates a date string in YYYY-MM-DD format * @param string $date The date string to validate * @param string $format Expected date format (default 'Y-m-d') * @return string|false Valid date string or false if invalid */ function validateDate($date, $format = 'Y-m-d') { $d = DateTime::createFromFormat($format, $date); // Check if date is valid and in correct format if (!$d || $d->format($format) !== $date) { return false; } return $date; } /** * Validates a numeric amount (for currency) * @param mixed $amount The amount to validate * @param float $min Minimum allowed amount (default 0) * @param float $max Maximum allowed amount (default 999999.99) * @return float|false Valid amount or false if invalid */ function validateAmount($amount, $min = 0, $max = 999999.99) { // Try to convert to float $value = filter_var($amount, FILTER_VALIDATE_FLOAT, [ 'options' => [ 'min_range' => $min, 'max_range' => $max, 'decimal' => '.' ] ]); // Must have at most 2 decimal places if ($value !== false) { $parts = explode('.', (string)$amount); if (isset($parts[1]) && strlen($parts[1]) > 2) { return false; } } return $value; } /** * Validates an integer within a range * @param mixed $int The integer to validate * @param int $min Minimum allowed value (default 0) * @param int $max Maximum allowed value (default 2147483647) * @return int|false Valid integer or false if invalid */ function validateInteger($int, $min = 0, $max = 2147483647) { $value = filter_var($int, FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => $min, 'max_range' => $max ] ]); return $value !== false ? $value : false; } /** * Validates South African ID number (13 digits) * @param string $idNumber The ID number to validate * @return string|false Valid ID number or false if invalid */ function validateSAIDNumber($idNumber) { // Remove any whitespace $idNumber = preg_replace('/\s/', '', $idNumber); // Must be exactly 13 digits if (!preg_match('/^\d{13}$/', $idNumber)) { return false; } // Optional: Validate checksum (Luhn algorithm) // $sum = 0; // for ($i = 0; $i < 13; $i++) { // $digit = (int)$idNumber[$i]; // // Double every even-positioned digit (0-indexed) // if ($i % 2 == 0) { // $digit *= 2; // if ($digit > 9) { // $digit -= 9; // } // } // $sum += $digit; // } // // Last digit should make sum divisible by 10 // if ($sum % 10 != 0) { // return false; // } return $idNumber; } /** * Sanitizes text input, removing potentially dangerous characters * @param string $text The text to sanitize * @param int $maxLength Maximum allowed length * @return string Sanitized text */ function sanitizeTextInput($text, $maxLength = 1000) { // Trim whitespace $text = trim($text); // Limit length $text = substr($text, 0, $maxLength); // Encode HTML special characters return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); } /** * Validates file uploads for security * @param array $file The $_FILES element to validate * @param array $allowedTypes Array of allowed MIME types (e.g., ['image/jpeg', 'image/png', 'application/pdf']) * @param int $maxSize Maximum file size in bytes * @return string|false Sanitized filename or false if invalid */ /** * HARDENED FILE UPLOAD VALIDATION * * Validates file uploads with strict security checks: * - Verifies upload completion * - Enforces strict MIME type validation using finfo * - Enforces strict file size limits per type * - Validates extensions against whitelist * - Prevents double extensions (e.g., .php.jpg) * - Generates random filenames to prevent path traversal * - Validates actual file content matches extension * * @param array $file The $_FILES['fieldname'] array * @param string $fileType The type of file being uploaded (profile_picture, proof_of_payment, document) * @return array|false Returns ['filename' => randomName, 'extension' => ext] on success, false on failure */ function validateFileUpload($file, $fileType = 'document') { // ===== CONFIGURATION: HARDCODED TYPE WHITELIST ===== $fileTypeConfig = [ 'profile_picture' => [ 'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'], 'mimeTypes' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], 'maxSize' => 5242880, // 5MB 'description' => 'Profile Picture' ], 'proof_of_payment' => [ 'extensions' => ['pdf'], 'mimeTypes' => ['application/pdf'], 'maxSize' => 10485760, // 10MB 'description' => 'Proof of Payment' ], 'document' => [ 'extensions' => ['pdf', 'doc', 'docx'], 'mimeTypes' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], 'maxSize' => 10485760, // 10MB 'description' => 'Document' ] ]; // Validate fileType exists in config if (!isset($fileTypeConfig[$fileType])) { error_log("Invalid file type requested: $fileType"); return false; } $config = $fileTypeConfig[$fileType]; // ===== CHECK 1: Upload Error ===== if (!isset($file['error']) || !isset($file['tmp_name']) || !isset($file['size']) || !isset($file['name'])) { error_log("File upload validation: Missing required file array keys"); return false; } if ($file['error'] !== UPLOAD_ERR_OK) { error_log("File upload error code: {$file['error']} for type: $fileType"); return false; } // ===== CHECK 2: File Size Limit ===== if ($file['size'] > $config['maxSize']) { error_log("File size {$file['size']} exceeds limit {$config['maxSize']} for type: $fileType"); return false; } if ($file['size'] <= 0) { error_log("File size is zero or negative for type: $fileType"); return false; } // ===== CHECK 3: Extension Validation (Case-Insensitive) ===== $pathinfo = pathinfo($file['name']); $extension = strtolower($pathinfo['extension'] ?? ''); if (empty($extension) || !in_array($extension, $config['extensions'], true)) { error_log("Invalid extension '$extension' for type: $fileType. Allowed: " . implode(', ', $config['extensions'])); return false; } // ===== CHECK 4: Prevent Double Extensions (e.g., shell.php.jpg) ===== if (isset($pathinfo['filename'])) { // Check if filename contains suspicious extensions $suspiciousPatterns = ['/\.php/', '/\.phtml/', '/\.phar/', '/\.sh/', '/\.bat/', '/\.exe/', '/\.com/']; foreach ($suspiciousPatterns as $pattern) { if (preg_match($pattern, $pathinfo['filename'], $matches)) { error_log("Suspicious pattern detected in filename: {$pathinfo['filename']} for type: $fileType"); return false; } } } // ===== CHECK 5: MIME Type Validation ===== // Skip MIME type validation if finfo_open is not available (shared hosting compatibility) // Extension validation in CHECK 4 provides sufficient security $mimeType = 'application/octet-stream'; // Default fallback if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); if ($finfo !== false) { $mimeType = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); if (!in_array($mimeType, $config['mimeTypes'], true)) { error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes'])); return false; } } } // ===== CHECK 6: Additional Image Validation (for images) ===== if (in_array($fileType, ['profile_picture'])) { $imageInfo = @getimagesize($file['tmp_name']); if ($imageInfo === false) { error_log("File is not a valid image for type: $fileType"); return false; } } // ===== CHECK 7: Verify File is Actually Uploaded (Not Executed) ===== if (!is_uploaded_file($file['tmp_name'])) { error_log("File is not a valid uploaded file for type: $fileType"); return false; } // ===== GENERATE RANDOM FILENAME ===== $randomName = bin2hex(random_bytes(16)) . '.' . $extension; return [ 'filename' => $randomName, 'extension' => $extension, 'mimeType' => $mimeType ]; } // ==================== RATE LIMITING & ACCOUNT LOCKOUT FUNCTIONS ==================== /** * Records a login attempt in the login_attempts table * @param string $email The email address attempting to login * @param bool $success Whether the login was successful * @return void */ function recordLoginAttempt($email, $success = false) { // Get client IP address $ip = getClientIPAddress(); $conn = openDatabaseConnection(); if (!$conn) { return; } $email = strtolower(trim($email)); $sql = "INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, ?)"; $stmt = $conn->prepare($sql); if ($stmt) { $success_int = $success ? 1 : 0; $stmt->bind_param('ssi', $email, $ip, $success_int); $stmt->execute(); $stmt->close(); } } /** * Gets the client's IP address safely * @return string The client's IP address */ function getClientIPAddress() { // Check for IP from share internet if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } // Check for IP passed from proxy elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } // Check for remote IP else { $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } // Validate IP format if (!filter_var($ip, FILTER_VALIDATE_IP)) { $ip = '0.0.0.0'; } return $ip; } /** * Checks if an account is locked and returns lockout status * @param string $email The email address to check * @return array ['is_locked' => bool, 'locked_until' => datetime string or null, 'minutes_remaining' => int] */ function checkAccountLockout($email) { $conn = openDatabaseConnection(); if (!$conn) { return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; } $email = strtolower(trim($email)); $now = date('Y-m-d H:i:s'); $sql = "SELECT locked_until FROM users WHERE email = ? LIMIT 1"; $stmt = $conn->prepare($sql); if (!$stmt) { return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; } $stmt->bind_param('s', $email); $stmt->execute(); $stmt->bind_result($locked_until); $stmt->fetch(); $stmt->close(); if ($locked_until === null) { return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; } if ($locked_until > $now) { // Account is still locked $lockTime = strtotime($locked_until); $nowTime = strtotime($now); $secondsRemaining = max(0, $lockTime - $nowTime); $minutesRemaining = ceil($secondsRemaining / 60); return [ 'is_locked' => true, 'locked_until' => $locked_until, 'minutes_remaining' => $minutesRemaining ]; } // Lockout has expired, clear it $sql = "UPDATE users SET locked_until = NULL WHERE email = ?"; $stmt = $conn->prepare($sql); if ($stmt) { $stmt->bind_param('s', $email); $stmt->execute(); $stmt->close(); } return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0]; } /** * Counts recent failed login attempts for an email + IP combination * @param string $email The email address * @param int $minutesBack How many minutes back to check (default 15) * @return int Number of failed attempts */ function countRecentFailedAttempts($email, $minutesBack = 15) { $conn = openDatabaseConnection(); if (!$conn) { return 0; } $email = strtolower(trim($email)); // Count failed attempts by email only (IP may vary due to proxies, mobile networks, etc) // Using DATE_SUB to ensure proper datetime comparison $sql = "SELECT COUNT(*) as count FROM login_attempts WHERE email = ? AND success = 0 AND attempted_at > DATE_SUB(NOW(), INTERVAL ? MINUTE)"; $stmt = $conn->prepare($sql); if (!$stmt) { return 0; } $stmt->bind_param('si', $email, $minutesBack); $stmt->execute(); $stmt->bind_result($count); $stmt->fetch(); $stmt->close(); return (int)$count; } /** * Locks an account for a specified duration * @param string $email The email address to lock * @param int $minutes Duration of lockout in minutes (default 15) * @return bool True if successful, false otherwise */ function lockAccount($email, $minutes = 15) { $conn = openDatabaseConnection(); if (!$conn) { return false; } $email = strtolower(trim($email)); $lockUntil = date('Y-m-d H:i:s', time() + ($minutes * 60)); $sql = "UPDATE users SET locked_until = ? WHERE email = ?"; $stmt = $conn->prepare($sql); if (!$stmt) { return false; } $stmt->bind_param('ss', $lockUntil, $email); $result = $stmt->execute(); $stmt->close(); return $result; } /** * Unlocks an account (admin function) * @param string $email The email address to unlock * @return bool True if successful, false otherwise */ function unlockAccount($email) { $conn = openDatabaseConnection(); if (!$conn) { return false; } $email = strtolower(trim($email)); $sql = "UPDATE users SET locked_until = NULL WHERE email = ?"; $stmt = $conn->prepare($sql); if (!$stmt) { return false; } $stmt->bind_param('s', $email); $result = $stmt->execute(); $stmt->close(); return $result; } /** * Logs an action to the audit log table * @param int $user_id User ID (null if not authenticated) * @param string $action Action name (e.g., 'LOGIN', 'FAILED_LOGIN', 'ACCOUNT_LOCKED') * @param string $resource_type Resource type being affected * @param int $resource_id Resource ID being affected * @param array $details Additional details about the action * @return bool True if successful */ function auditLog($user_id, $action, $resource_type = null, $resource_id = null, $details = null) { $conn = openDatabaseConnection(); if (!$conn) { return false; } $ip = getClientIPAddress(); $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $detailsJson = $details ? json_encode($details) : null; $sql = "INSERT INTO audit_log (user_id, action, resource_type, resource_id, ip_address, user_agent, details) VALUES (?, ?, ?, ?, ?, ?, ?)"; $stmt = $conn->prepare($sql); if (!$stmt) { return false; } $stmt->bind_param('issdsss', $user_id, $action, $resource_type, $resource_id, $ip, $userAgent, $detailsJson); $result = $stmt->execute(); $stmt->close(); return $result; } /** * URL Helper - Map page names to file paths * Centralizes all internal links for easy management * * Usage: url('login') → /src/pages/auth/login.php */ function url($page) { static $map = [ // Home & Main 'home' => '/index.php', 'index' => '/index.php', // Auth Pages 'login' => '/src/pages/auth/login.php', 'register' => '/src/pages/auth/register.php', 'forgot' => '/src/pages/auth/forgot_password.php', 'forgot_password' => '/src/pages/auth/forgot_password.php', 'reset_password' => '/src/pages/auth/reset_password.php', 'verify' => '/src/pages/auth/verify.php', 'resend_verification' => '/src/pages/auth/resend_verification.php', 'change_password' => '/src/pages/auth/change_password.php', 'update_password' => '/src/pages/auth/update_password.php', // Membership Pages 'membership' => '/src/pages/memberships/membership.php', 'membership_details' => '/src/pages/memberships/membership_details.php', 'membership_application' => '/src/pages/memberships/membership_application.php', 'membership_payment' => '/src/pages/memberships/membership_payment.php', 'renew_membership' => '/src/pages/memberships/renew_membership.php', 'member_info' => '/src/pages/memberships/member_info.php', // Booking Pages 'bookings' => '/src/pages/bookings/bookings.php', 'campsites' => '/src/pages/bookings/campsites.php', 'campsite_booking' => '/src/pages/bookings/campsite_booking.php', 'trips' => '/src/pages/bookings/trips.php', 'trip_details' => '/src/pages/bookings/trip-details.php', 'course_details' => '/src/pages/bookings/course_details.php', 'driver_training' => '/src/pages/bookings/driver_training.php', // Shop Pages 'view_cart' => '/src/pages/shop/view_cart.php', 'add_to_cart' => '/src/pages/shop/add_to_cart.php', 'bar_tabs' => '/src/pages/shop/bar_tabs.php', 'payment_confirmation' => '/src/pages/shop/payment_confirmation.php', 'confirm' => '/src/pages/shop/confirm.php', 'confirm2' => '/src/pages/shop/confirm2.php', // Events & Blog 'events' => '/src/pages/events/events.php', 'blog' => '/src/pages/events/blog.php', 'blog_details' => '/src/pages/events/blog_details.php', 'best_of_eastern_cape' => '/src/pages/events/best_of_the_eastern_cape_2024.php', 'agm_minutes' => '/src/pages/events/2025_agm_minutes.php', 'agm_content' => '/src/pages/events/agm_content.php', 'instapage' => '/src/pages/events/instapage.php', // Other Pages 'about' => '/src/pages/other/about.php', 'contact' => '/src/pages/other/contact.php', 'privacy' => '/src/pages/other/privacy_policy.php', 'privacy_policy' => '/src/pages/other/privacy_policy.php', '404' => '/src/pages/other/404.php', 'account_settings' => '/src/pages/other/account_settings.php', 'rescue_recovery' => '/src/pages/other/rescue_recovery.php', 'bush_mechanics' => '/src/pages/other/bush_mechanics.php', 'indemnity' => '/src/pages/other/indemnity.php', 'indemnity_waiver' => '/src/pages/other/indemnity_waiver.php', 'basic_indemnity' => '/src/pages/other/basic_indemnity.php', 'view_indemnity' => '/src/pages/other/view_indemnity.php', // Admin Pages (accessible only to admins) 'admin_members' => '/src/admin/admin_members.php', 'admin_payments' => '/src/admin/admin_payments.php', 'admin_web_users' => '/src/admin/admin_web_users.php', 'admin_course_bookings' => '/src/admin/admin_course_bookings.php', 'admin_camp_bookings' => '/src/admin/admin_camp_bookings.php', 'admin_trip_bookings' => '/src/admin/admin_trip_bookings.php', 'admin_visitors' => '/src/admin/admin_visitors.php', 'admin_efts' => '/src/admin/admin_efts.php', 'add_campsite' => '/src/admin/add_campsite.php', // API/AJAX Endpoints 'fetch_users' => '/src/api/fetch_users.php', 'fetch_drinks' => '/src/api/fetch_drinks.php', 'fetch_bar_tabs' => '/src/api/fetch_bar_tabs.php', 'get_campsites' => '/src/api/get_campsites.php', 'get_tab_total' => '/src/api/get_tab_total.php', 'google_validate_login' => '/src/api/google_validate_login.php', // Processors 'validate_login' => '/validate_login.php', 'register_user' => '/src/processors/register_user.php', 'process_application' => '/src/processors/process_application.php', 'process_booking' => '/src/processors/process_booking.php', 'process_camp_booking' => '/src/processors/process_camp_booking.php', 'process_course_booking' => '/src/processors/process_course_booking.php', 'process_trip_booking' => '/src/processors/process_trip_booking.php', 'process_membership_payment' => '/src/processors/process_membership_payment.php', 'process_payments' => '/src/processors/process_payments.php', 'process_eft' => '/src/processors/process_eft.php', 'submit_order' => '/src/processors/submit_order.php', 'submit_pop' => '/src/processors/submit_pop.php', 'process_signature' => '/src/processors/process_signature.php', 'create_bar_tab' => '/src/processors/create_bar_tab.php', 'update_application' => '/src/processors/update_application.php', 'update_user' => '/src/processors/update_user.php', 'upload_profile_picture' => '/src/processors/upload_profile_picture.php', 'send_reset_link' => '/src/processors/send_reset_link.php', 'logout' => '/src/processors/logout.php', ]; // Return mapped URL or fallback to simple filename if (isset($map[$page])) { return $map[$page]; } // Fallback: assume it's a root-level file return '/' . $page . '.php'; } /** * Optimize image by resizing if it exceeds max dimensions * * @param string $filePath Path to the image file * @param int $maxWidth Maximum width in pixels * @param int $maxHeight Maximum height in pixels * @return bool Success status */ function optimizeImage($filePath, $maxWidth = 1920, $maxHeight = 1080) { if (!file_exists($filePath)) { return false; } // Get image info $imageInfo = getimagesize($filePath); if (!$imageInfo) { return false; } $width = $imageInfo[0]; $height = $imageInfo[1]; $mime = $imageInfo['mime']; // Only resize if image is larger than max dimensions if ($width <= $maxWidth && $height <= $maxHeight) { return true; } // Calculate new dimensions maintaining aspect ratio $ratio = min($maxWidth / $width, $maxHeight / $height); $newWidth = (int)($width * $ratio); $newHeight = (int)($height * $ratio); // Load image based on type switch ($mime) { case 'image/jpeg': $source = imagecreatefromjpeg($filePath); break; case 'image/png': $source = imagecreatefrompng($filePath); break; case 'image/gif': $source = imagecreatefromgif($filePath); break; case 'image/webp': $source = imagecreatefromwebp($filePath); break; default: return false; } if (!$source) { return false; } // Create resized image $destination = imagecreatetruecolor($newWidth, $newHeight); // Preserve transparency for PNG and GIF if ($mime === 'image/png' || $mime === 'image/gif') { $transparent = imagecolorallocatealpha($destination, 0, 0, 0, 127); imagefill($destination, 0, 0, $transparent); imagesavealpha($destination, true); } // Resize imagecopyresampled($destination, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height); // Save image $success = false; switch ($mime) { case 'image/jpeg': $success = imagejpeg($destination, $filePath, 85); break; case 'image/png': $success = imagepng($destination, $filePath, 6); break; case 'image/gif': $success = imagegif($destination, $filePath); break; case 'image/webp': $success = imagewebp($destination, $filePath, 85); break; } // Free up memory imagedestroy($source); imagedestroy($destination); return $success; } /** * Link a secondary user to a primary user's membership * @param int $primary_user_id The main membership holder * @param int $secondary_user_id The user to link (spouse, family member, etc) * @param string $relationship The relationship type (spouse, family_member, etc) * @return array ['success' => bool, 'message' => string] */ function linkSecondaryUserToMembership($primary_user_id, $secondary_user_id, $relationship = 'spouse') { $conn = openDatabaseConnection(); if ($conn === null) { error_log("linkSecondaryUserToMembership: Database connection failed"); return ['success' => false, 'message' => 'Database connection failed']; } error_log("linkSecondaryUserToMembership: primary=$primary_user_id, secondary=$secondary_user_id, relationship=$relationship"); // Validation: primary and secondary user IDs must be different if ($primary_user_id === $secondary_user_id) { error_log("linkSecondaryUserToMembership: Cannot link user to themselves"); return ['success' => false, 'message' => 'Cannot link user to themselves']; } // Validation: primary user must have active membership $memberStatus = getUserMemberStatus($primary_user_id); error_log("linkSecondaryUserToMembership: Primary user member status = " . ($memberStatus ? 'true' : 'false')); if (!$memberStatus) { $conn->close(); error_log("linkSecondaryUserToMembership: Primary user does not have active membership"); return ['success' => false, 'message' => 'Primary user does not have active membership']; } // Validation: secondary user must exist $userCheck = $conn->prepare("SELECT user_id FROM users WHERE user_id = ?"); $userCheck->bind_param("i", $secondary_user_id); $userCheck->execute(); $userResult = $userCheck->get_result(); $userCheck->close(); if ($userResult->num_rows === 0) { $conn->close(); error_log("linkSecondaryUserToMembership: Secondary user does not exist"); return ['success' => false, 'message' => 'Secondary user does not exist']; } // Check if link already exists $existingLink = $conn->prepare("SELECT link_id FROM membership_links WHERE primary_user_id = ? AND secondary_user_id = ?"); $existingLink->bind_param("ii", $primary_user_id, $secondary_user_id); $existingLink->execute(); $existingResult = $existingLink->get_result(); $existingLink->close(); if ($existingResult->num_rows > 0) { $conn->close(); error_log("linkSecondaryUserToMembership: Users are already linked"); return ['success' => false, 'message' => 'Users are already linked']; } try { // Start transaction $conn->begin_transaction(); error_log("linkSecondaryUserToMembership: Starting transaction"); // Insert link $insertLink = $conn->prepare(" INSERT INTO membership_links (primary_user_id, secondary_user_id, relationship, linked_at, created_at) VALUES (?, ?, ?, NOW(), NOW()) "); $insertLink->bind_param("iis", $primary_user_id, $secondary_user_id, $relationship); if (!$insertLink->execute()) { throw new Exception("Failed to insert link: " . $insertLink->error); } $linkId = $conn->insert_id; error_log("linkSecondaryUserToMembership: Link created with ID = $linkId"); $insertLink->close(); // Grant default permissions to secondary user $permissions = [ 'access_member_areas', 'member_pricing', 'book_campsites', 'book_courses', 'book_trips' ]; foreach ($permissions as $permission) { $insertPerm = $conn->prepare(" INSERT INTO membership_permissions (link_id, permission_name, granted_at) VALUES (?, ?, NOW()) "); $insertPerm->bind_param("is", $linkId, $permission); if (!$insertPerm->execute()) { throw new Exception("Failed to insert permission: " . $insertPerm->error); } error_log("linkSecondaryUserToMembership: Permission '$permission' granted"); $insertPerm->close(); } // Commit transaction $conn->commit(); error_log("linkSecondaryUserToMembership: Transaction committed successfully"); $conn->close(); return ['success' => true, 'message' => 'User successfully linked to membership', 'link_id' => $linkId]; } catch (Exception $e) { error_log("linkSecondaryUserToMembership: Exception - " . $e->getMessage()); $conn->rollback(); $conn->close(); return ['success' => false, 'message' => 'Failed to create link: ' . $e->getMessage()]; } } /** * Check if a user has access through a membership link * @param int $user_id The user to check * @return array ['has_access' => bool, 'primary_user_id' => int|null, 'relationship' => string|null] */ function getUserMembershipLink($user_id) { $conn = openDatabaseConnection(); if ($conn === null) { return ['has_access' => false, 'primary_user_id' => null, 'relationship' => null]; } // Check if user is a secondary user with active link $query = " SELECT ml.primary_user_id, ml.relationship FROM membership_links ml JOIN membership_fees mf ON ml.primary_user_id = mf.user_id JOIN membership_application ma ON ml.primary_user_id = ma.user_id WHERE ml.secondary_user_id = ? AND ma.accept_indemnity = 1 AND mf.payment_status = 'PAID' AND mf.membership_end_date >= CURDATE() LIMIT 1 "; $stmt = $conn->prepare($query); $stmt->bind_param("i", $user_id); $stmt->execute(); $result = $stmt->get_result(); $stmt->close(); if ($result->num_rows > 0) { $link = $result->fetch_assoc(); $conn->close(); return [ 'has_access' => true, 'primary_user_id' => $link['primary_user_id'], 'relationship' => $link['relationship'] ]; } $conn->close(); return ['has_access' => false, 'primary_user_id' => null, 'relationship' => null]; } /** * Get all secondary users linked to a primary user * @param int $primary_user_id The primary membership holder * @return array Array of linked users with their info */ function getLinkedSecondaryUsers($primary_user_id) { $conn = openDatabaseConnection(); if ($conn === null) { return []; } $query = " SELECT ml.link_id, u.user_id, u.first_name, u.last_name, u.email, ml.relationship, ml.linked_at FROM membership_links ml JOIN users u ON ml.secondary_user_id = u.user_id WHERE ml.primary_user_id = ? ORDER BY ml.linked_at DESC "; $stmt = $conn->prepare($query); $stmt->bind_param("i", $primary_user_id); $stmt->execute(); $result = $stmt->get_result(); $stmt->close(); $linkedUsers = []; while ($row = $result->fetch_assoc()) { $linkedUsers[] = $row; } $conn->close(); return $linkedUsers; } /** * Unlink a secondary user from a primary user's membership * @param int $link_id The membership link ID to remove * @param int $primary_user_id The primary user (for verification) * @return array ['success' => bool, 'message' => string] */ function unlinkSecondaryUser($link_id, $primary_user_id) { $conn = openDatabaseConnection(); if ($conn === null) { return ['success' => false, 'message' => 'Database connection failed']; } // Verify that this link belongs to the primary user $linkCheck = $conn->prepare("SELECT primary_user_id FROM membership_links WHERE link_id = ?"); $linkCheck->bind_param("i", $link_id); $linkCheck->execute(); $linkResult = $linkCheck->get_result(); $linkCheck->close(); if ($linkResult->num_rows === 0) { $conn->close(); return ['success' => false, 'message' => 'Link not found']; } $linkData = $linkResult->fetch_assoc(); if ($linkData['primary_user_id'] !== $primary_user_id) { $conn->close(); return ['success' => false, 'message' => 'Unauthorized: you do not have permission to remove this link']; } try { // Start transaction $conn->begin_transaction(); // Delete permissions first (cascade should handle this but being explicit) $deletePerm = $conn->prepare("DELETE FROM membership_permissions WHERE link_id = ?"); $deletePerm->bind_param("i", $link_id); $deletePerm->execute(); $deletePerm->close(); // Delete the link $deleteLink = $conn->prepare("DELETE FROM membership_links WHERE link_id = ?"); $deleteLink->bind_param("i", $link_id); $deleteLink->execute(); $deleteLink->close(); // Commit transaction $conn->commit(); $conn->close(); return ['success' => true, 'message' => 'User successfully unlinked from membership']; } catch (Exception $e) { $conn->rollback(); $conn->close(); return ['success' => false, 'message' => 'Failed to remove link: ' . $e->getMessage()]; } } /** * Retrieve the payment_link for a given internal payment_id from the payments table. * Returns the payment_link string on success or null if not found / on error. * * @param string $payment_id * @return string|null */ function getPaymentLinkByPaymentId($payment_id) { $conn = openDatabaseConnection(); if ($conn === null) { return null; } $stmt = $conn->prepare("SELECT payment_link FROM payments WHERE payment_id = ? LIMIT 1"); if (!$stmt) { $conn->close(); return null; } $stmt->bind_param('s', $payment_id); $stmt->execute(); $stmt->bind_result($payment_link); $found = $stmt->fetch(); $stmt->close(); $conn->close(); if ($found) { return $payment_link; } return null; }