3228 lines
99 KiB
PHP
3228 lines
99 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/../../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'];
|
|
|
|
// Disable mysqli exceptions
|
|
mysqli_report(MYSQLI_REPORT_OFF);
|
|
|
|
// Create connection
|
|
$conn = @new mysqli($dbhost, $dbuser, $dbpass, $dbname);
|
|
|
|
// Check connection
|
|
if ($conn->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 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 = '<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
|
|
$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()];
|
|
}
|
|
}
|
|
|