The countRecentFailedAttempts() function was requiring BOTH email AND ip_address to match, which caused failed attempts from different IPs to not count together. This prevented account lockout from working properly. Changed to count failed attempts by email only. IP address is still recorded for audit purposes but doesn't affect the failed attempt count. This ensures: - Failed attempts accumulate correctly regardless of IP changes - Accounts lock after 5 failed attempts within 15 minutes - Prevents attackers from bypassing by changing IP
2643 lines
80 KiB
PHP
2643 lines
80 KiB
PHP
<?php
|
|
|
|
require_once "vendor/autoload.php";
|
|
|
|
use GuzzleHttp\Client;
|
|
|
|
function openDatabaseConnection()
|
|
{
|
|
// Database connection parameters
|
|
|
|
$dbhost = $_ENV['DB_HOST'];
|
|
$dbuser = $_ENV['DB_USER'];
|
|
$dbpass = $_ENV['DB_PASS'];
|
|
$dbname = $_ENV['DB_NAME'];
|
|
$salt = $_ENV['SALT'];
|
|
|
|
|
|
// Create connection
|
|
$conn = new mysqli($dbhost, $dbuser, $dbpass, $dbname);
|
|
|
|
// Check connection
|
|
if ($conn->connect_error) {
|
|
die("Connection failed: " . $conn->connect_error);
|
|
}
|
|
|
|
return $conn;
|
|
}
|
|
|
|
function getTripCount()
|
|
{
|
|
// Database connection
|
|
$conn = openDatabaseConnection();
|
|
|
|
// SQL query to count the number of rows
|
|
$sql = "SELECT COUNT(*) AS total FROM trips WHERE published = 1 AND start_date > CURDATE()";
|
|
$result = $conn->query($sql);
|
|
|
|
// 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)
|
|
{
|
|
$message = [
|
|
'Messages' => [
|
|
[
|
|
'From' => [
|
|
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
|
'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin'
|
|
],
|
|
'To' => [
|
|
[
|
|
'Email' => 'chrispintoza@gmail.com',
|
|
'Name' => 'Chris Pinto'
|
|
],
|
|
[
|
|
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
|
'Name' => 'Jacqui Boshoff'
|
|
],
|
|
[
|
|
'Email' => 'louiseb@global.co.za',
|
|
'Name' => 'Louise Blignault'
|
|
]
|
|
],
|
|
'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();
|
|
|
|
// 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 === "PAID" && $current_date <= $membership_end_date_obj) {
|
|
return true; // Membership is active
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return false; // Membership is not active
|
|
}
|
|
|
|
function getUserMemberStatusPending($user_id)
|
|
{
|
|
|
|
$conn = openDatabaseConnection();
|
|
|
|
// 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 = '<form id="payfastForm" action="https://' . $pfHost . '/eng/process" method="post">';
|
|
foreach ($data as $name => $value) {
|
|
$htmlForm .= '<input name="' . $name . '" type="hidden" value="' . $value . '" />';
|
|
}
|
|
// $htmlForm .= '<input class="btn mr-0" style="width: 100%;" type="submit" name="submit" value="PAY NOW">';
|
|
$htmlForm .= '</form>';
|
|
|
|
// JavaScript to automatically submit the form
|
|
$htmlForm .= '<script type="text/javascript">
|
|
document.getElementById("payfastForm").submit();
|
|
</script>';
|
|
|
|
// Output the form and script to the browser
|
|
echo $htmlForm;
|
|
|
|
ob_end_flush(); // Ensure any buffered output is sent to the browser
|
|
|
|
}
|
|
|
|
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 = '<form id="payfastForm" action="https://' . $pfHost . '/eng/process" method="post">';
|
|
foreach ($data as $name => $value) {
|
|
$htmlForm .= '<input name="' . $name . '" type="hidden" value="' . $value . '" />';
|
|
}
|
|
// $htmlForm .= '<input class="btn mr-0" style="width: 100%;" type="submit" name="submit" value="PAY NOW">';
|
|
$htmlForm .= '</form>';
|
|
|
|
// JavaScript to automatically submit the form
|
|
$htmlForm .= '<script type="text/javascript">
|
|
document.getElementById("payfastForm").submit();
|
|
</script>';
|
|
|
|
// 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
|
|
$query = "SELECT vehicle_capacity FROM trips WHERE trip_id = $trip_id";
|
|
$result = $conn->query($query);
|
|
|
|
// 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
|
|
$query = "SELECT SUM(num_vehicles) as total_booked FROM bookings WHERE trip_id = $trip_id";
|
|
$result = $conn->query($query);
|
|
|
|
// 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();
|
|
}
|
|
|
|
// 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) {
|
|
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();
|
|
|
|
$query = "SELECT COUNT(*) AS trip_count FROM trips WHERE published = 1 AND start_date > CURDATE()";
|
|
|
|
|
|
if ($result = $conn->query($query)) {
|
|
$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();
|
|
|
|
// 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();
|
|
$sql = "
|
|
SELECT date
|
|
FROM events
|
|
WHERE name = '4WDCSA Open Day'
|
|
AND date >= NOW()
|
|
ORDER BY date ASC
|
|
LIMIT 1
|
|
";
|
|
|
|
$result = $conn->query($sql);
|
|
|
|
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)
|
|
{
|
|
$response = file_get_contents("http://ip-api.com/json/$ip");
|
|
$data = json_decode($response, true);
|
|
|
|
if ($data['status'] == 'success') {
|
|
return $data['country']; // e.g., South Africa
|
|
}
|
|
}
|
|
|
|
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("i", $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;
|
|
}
|
|
|
|
// Only allow letters, spaces, hyphens, and apostrophes
|
|
if (!preg_match('/^[a-zA-Z\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 =====
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
if ($finfo === false) {
|
|
error_log("Failed to open fileinfo resource");
|
|
return 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));
|
|
$cutoffTime = date('Y-m-d H:i:s', time() - ($minutesBack * 60));
|
|
|
|
// Count failed attempts by email only (IP may vary due to proxies, mobile networks, etc)
|
|
$sql = "SELECT COUNT(*) as count FROM login_attempts
|
|
WHERE email = ? AND success = 0
|
|
AND attempted_at > ?";
|
|
$stmt = $conn->prepare($sql);
|
|
if (!$stmt) {
|
|
return 0;
|
|
}
|
|
|
|
$stmt->bind_param('ss', $email, $cutoffTime);
|
|
$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;
|
|
}
|