# Function Reference Developer reference for the helpers in `src/config/functions.php` (~99 functions). This covers the ones you will actually reach for. For how the site fits together, see [HANDOVER.md](HANDOVER.md). Everything here is available after: ```php require_once($rootPath . "/src/config/functions.php"); ``` --- ## Database ### `openDatabaseConnection()` Returns a **shared** `mysqli` connection for the current request, or `null` if the database is unreachable. ```php $conn = openDatabaseConnection(); if ($conn === null) { /* handle it */ } ``` > **Do not call `$conn->close()` on the result.** The handle is reused across > every caller in the request; closing it breaks everything that runs after. > PHP releases it automatically when the request ends. > > This used to open a fresh connection per call, across 73 call sites, which > was exhausting the server's connection limit in production. Most helpers below call this internally, so you rarely need it directly. ### `classes/DatabaseService.php` A prepared-statement wrapper available as `$db` after including `connection.php`. Cleaner than raw mysqli, but only lightly used. Prefer it for new code. Methods: `select()`, `selectOne()`, `insert()`, `update()`, `delete()`, `execute()`, `count()`, `exists()`, `beginTransaction()`, `commit()`, `rollback()`, `getConnection()`, plus `getLastError()` / `getLastQuery()` for debugging. ```php $rows = $db->select("SELECT * FROM trips WHERE published = ?", [1], "i"); ``` --- ## Authentication and access control | Function | Returns | Notes | |---|---|---| | `getUserRole()` | `'user'` / `'admin'` / `'superadmin'` / `null` | `null` when logged out | | `checkAdmin()` | — | **Redirects** to `index.php` unless admin/superadmin | | `checkSuperAdmin()` | — | **Redirects** unless superadmin | | `checkUserSession()` | — | **Redirects** to login if not logged in | | `get_user_info($field)` | mixed | Reads one column from `users` for the logged-in user | `checkAdmin()` and friends `exit()` after redirecting — call them at the very top of a page, before any output. ```php require_once(...functions.php); checkAdmin(); // page below is admin-only ``` > `get_user_info()` interpolates `$field` straight into the SQL. Only ever pass > a hardcoded column name — never user input. ### Login throttling | Function | Purpose | |---|---| | `recordLoginAttempt($email, $success = false)` | Log an attempt | | `countRecentFailedAttempts($email, $minutesBack = 15)` | Recent failures | | `checkAccountLockout($email)` | `['is_locked' => bool, 'minutes_remaining' => int, ...]` | | `lockAccount($email, $minutes = 15)` | Lock after repeated failures | | `unlockAccount($email)` | Manual unlock — use this when a member phones up | Five failures in 15 minutes triggers a 15-minute lockout. --- ## CSRF protection ```php // In the form // In the processor, before doing anything else if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { http_response_code(403); exit(json_encode(['status' => 'error', 'message' => 'Security token validation failed.'])); } ``` - `generateCSRFToken($duration = 3600)` — random, expiring token - `validateCSRFToken($token)` — **single-use**; validating consumes it Tokens are single-use, so a page with several independent forms needs a token per form. If a form is submitted twice (double-click, back button) the second attempt fails validation — that is intended. **Apply this to every state-changing endpoint.** Roughly 40% currently do. --- ## Input validation All return the cleaned value, or `false` if invalid. | Function | Checks | |---|---| | `validateEmail($email, $maxLength = 254)` | Format and length | | `validatePhoneNumber($phone)` | 7–20 digits | | `validateName($name, $min = 2, $max = 100)` | Length and characters | | `validateDate($date, $format = 'Y-m-d')` | Real date in the given format | | `validateAmount($amount, $min = 0, $max = 999999.99)` | Money | | `validateInteger($int, $min = 0, $max = 2147483647)` | Bounded integer | | `validateSAIDNumber($idNumber)` | SA ID number incl. checksum | | `sanitizeTextInput($text, $maxLength = 1000)` | Strips dangerous content | ### `validateFileUpload($file, $fileType = 'document')` The **only** correct way to accept an upload. Returns `['filename' => ..., 'extension' => ..., 'mimeType' => ...]` or `false`. ```php $validated = validateFileUpload($_FILES['file'], 'blog_image'); if ($validated === false) { /* reject */ } move_uploaded_file($_FILES['file']['tmp_name'], $dir . $validated['filename']); ``` Types: `profile_picture`, `blog_image`, `proof_of_payment`, `document`. It checks the upload error code, size, an extension **whitelist**, double extensions (`shell.php.jpg`), MIME type, that images really are images, and `is_uploaded_file()`. It returns a **random** filename — always use that rather than the user's original name. > An endpoint that skipped this was the most serious vulnerability found in the > August 2026 audit: it allowed anyone to upload and execute a `.php` file. > Do not hand-roll upload handling. --- ## Membership | Function | Returns | |---|---| | `getUserMemberStatus($user_id)` | `true` if member benefits apply — the one to use for gating | | `getUserMemberStatusPending($user_id)` | `true` if awaiting payment | | `hasAcceptedIndemnity($user_id)` | Indemnity signed? | | `getMembershipType($user_id)` | Type from the latest application | | `getMembershipEndDate($user_id)` | Expiry date | | `isMembershipExpiringSoon($user_id)` | `true` within 90 days of expiry | | `validateHonoraryMemberName($first, $last)` | Fuzzy match against a hardcoded list | `getUserMemberStatus()` also returns `true` for people **linked** to somebody else's active membership — that is usually what you want. See HANDOVER.md §6 for the full rules. ### Linked memberships (spouse / family) | Function | Purpose | |---|---| | `linkSecondaryUserToMembership($primary_id, $secondary_id, $relationship = 'spouse')` | Create a link + default permissions. Transactional | | `getUserMembershipLink($user_id)` | `['has_access' => bool, ...]` for a secondary user | | `getLinkedSecondaryUsers($primary_id)` | Everyone linked to a primary | | `unlinkSecondaryUser($link_id, $primary_id)` | Remove a link. Verifies ownership | --- ## Pricing and payments | Function | Purpose | |---|---| | `calculateProrata()` | Subscription fee for right now (full Mar–Oct, pro-rata Nov–Feb) | | `getPriceByDescription($description)` | One row from the `prices` table | | `getAvailableSpaces($trip_id)` | Vehicle capacity minus bookings | | `generatePaymentRef(string $type, ?int $course_trip_id, int $user_id): string` | Unique reference. `$type` is `SUBS`, `COURSE`, `TRIP`, … | | `getPaymentAmount($payment_id)` | Amount for a payment | | `getPaymentLinkByPaymentId($payment_id)` | Stored gateway payment link | ### iKhokha | Function | Purpose | |---|---| | `createIkhokhaPayment($payment_id, $amount, $description, $publicRef)` | Creates a payment link and records provider metadata | | `getIkhokhaTransactionHistory($startDate, $endDate)` | Raw history, used by Admin → Transactions | | `createPayloadToSign($urlPath, $body)` / `generateSignature($payload, $secret)` | HMAC signing. Used by the webhook — **do not change without testing against live iKhokha callbacks** | `$amount` is in **rands**; the function converts to cents internally. ### EFT - `addEFT($eft_id, $booking_id, $user_id, $status, $amount, $description)` - `addSubsEFT($eft_id, $user_id, $status, $amount, $description)` — subscriptions - `getEFTDetails($eft_id)`, `getUserIdFromEFT($eft_id)`, `getEftDescription($eft_id)` ### Dead code `processPayment()`, `processMembershipPayment()`, `processPaymentTest()` are PayFast and are **not called from anywhere live**. `processZeroPayment()` *is* live — it handles free bookings. --- ## Email All go through Mailjet and mostly use stored templates. | Function | Goes to | |---|---| | `sendVerificationEmail($email, $name, $token)` | New registrant | | `sendPaymentConfirmation($email, $name, $description)` | Paying member | | `sendInvoice($email, $name, $eft_id, $amount, $description)` | Member, after booking | | `sendPOP($fullname, $eft_id, $amount, $description)` | `POP_NOTIFICATION_EMAILS` | | `sendAdminNotification($subject, $message)` | `NOTIFICATION_ADDR` | | `sendFinanceNotification($subject, $message)` | `FINANCE_EMAIL` | | `sendEmail($email, $subject, $message)` | Plain text, no template | Template IDs are hardcoded. If a template is edited in Mailjet the ID stays the same; if it is recreated, the ID changes and the code must be updated. --- ## Tokens and URLs ### `encryptData($input, $salt)` / `decryptData($encrypted, $salt)` Used to put record IDs in URLs without exposing raw database IDs: ```php $token = encryptData($blog_id, $salt); // link out $blog_id = intval(decryptData($token, $salt)); // read back ``` `$salt` comes from `.env` via `connection.php`. **Changing `SALT` invalidates every existing link.** > This obscures IDs; it is not access control. Always still check ownership. ### `url($page)` Maps a page name to a path. See HANDOVER.md §4 for why this and `.htaccess` must be kept in step. --- ## Audit and logging ### `auditLog($user_id, $action, $resource_type = null, $resource_id = null, $details = null)` Writes to the audit table. Use for security-relevant events: ```php auditLog($user_id, 'BLOG_AUTOSAVE_DENIED', 'blogs', $blog_id); ``` `$user_id` may be `null` for pre-authentication events. Do not put secrets, passwords or signature values in `$details` — admins can read this table. ### `progress_log($message, $context = null)` Appends to `progress.log` in the site root. Used heavily by the payment webhook. Never throws, so it is safe in error paths. Blocked from web access by `.htaccess` — keep it that way, it contains payment data. ### Visitors `logVisitor()`, `getUserIP()`, `getClientIPAddress()`, `guessCountry()`, `blockBlacklistedIP()`. `visitor_logs` holds IP addresses tied to user IDs — personal data under POPIA (HANDOVER.md §9). --- ## Formatting helpers | Function | Does | |---|---| | `convertDate($dateString)` | `2026-03-15` → `Sun, 15 Mar 2026` | | `calculateDaysAndNights($start, $end)` | `"3 days 2 nights"` | | `formatCurrency($amount)` | Rand formatting | | `getFullName($user_id)` | `"First Last"` | | `getInitialSurname($user_id)` | `"F.Last"` | | `getEmail($user_id)` / `getProfilePic($user_id)` | Single lookups | | `normalizeName($name)` | Lowercase, punctuation stripped — for fuzzy matching | | `optimizeImage($filePath, $maxWidth = 1920, $maxHeight = 1080)` | Resize in place | --- ## Conventions for new code 1. **Always use prepared statements.** No string interpolation into SQL. 2. **Check permissions on every endpoint** — logged in *and* allowed to touch *this* record. Several bugs have come from checking only the first. 3. **CSRF-validate every state-changing POST.** 4. **Route uploads through `validateFileUpload()`.** 5. **Do not close the shared database connection.** 6. **Add new pages to both routing tables** (`.htaccess` and `url()`). 7. **Never echo raw user input** — `htmlspecialchars()` on output. 8. **Keep secrets in `.env`**, never in code.