HASH_COST]); } /** * Verify password */ function verifyPassword($password, $hash) { return password_verify($password, $hash); } /** * Sanitize input */ function sanitize($input) { if (is_array($input)) { return array_map('sanitize', $input); } return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8'); } /** * Format currency */ function formatCurrency($amount) { return TJJ_CURRENCY_SYMBOL . number_format((float)$amount, 2); } /** * Format date */ function formatDate($date, $format = 'M j, Y') { return date($format, strtotime($date)); } /** * Format datetime */ function formatDateTime($date, $format = 'M j, Y g:i A') { return date($format, strtotime($date)); } /** * JSON response helper */ function jsonResponse($data, $statusCode = 200) { http_response_code($statusCode); header('Content-Type: application/json'); echo json_encode($data); exit; } /** * Redirect helper */ function redirect($url) { header("Location: $url"); exit; } /** * Get current URL */ function currentUrl() { $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; return $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } /** * Check if request is AJAX */ function isAjax() { return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; } /** * Get client IP address */ function getClientIp() { $ip = $_SERVER['REMOTE_ADDR']; if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } return trim($ip); } /** * Generate CSRF token */ function generateCsrfToken() { if (empty($_SESSION[CSRF_TOKEN_NAME])) { $_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32)); } return $_SESSION[CSRF_TOKEN_NAME]; } /** * Verify CSRF token */ function verifyCsrfToken($token) { return isset($_SESSION[CSRF_TOKEN_NAME]) && hash_equals($_SESSION[CSRF_TOKEN_NAME], $token); } /** * Get setting value */ function getSetting($key, $default = null) { $result = db()->fetch( "SELECT setting_value FROM settings WHERE setting_key = :key", ['key' => $key] ); if ($result) { return json_decode($result['setting_value'], true) ?? $result['setting_value']; } return $default; } /** * Update setting value */ function setSetting($key, $value) { $jsonValue = json_encode($value); $existing = db()->fetch( "SELECT id FROM settings WHERE setting_key = :key", ['key' => $key] ); if ($existing) { db()->update('settings', ['setting_value' => $jsonValue], 'setting_key = :key', ['key' => $key]); } else { db()->insert('settings', ['setting_key' => $key, 'setting_value' => $jsonValue]); } } /** * Flash message helpers */ function setFlash($type, $message) { $_SESSION['flash'][$type] = $message; } function getFlash($type) { if (isset($_SESSION['flash'][$type])) { $message = $_SESSION['flash'][$type]; unset($_SESSION['flash'][$type]); return $message; } return null; } function hasFlash($type) { return isset($_SESSION['flash'][$type]); } /** * Pagination helper */ function paginate($totalItems, $currentPage, $perPage = ITEMS_PER_PAGE) { $totalPages = ceil($totalItems / $perPage); $currentPage = max(1, min($currentPage, $totalPages)); $offset = ($currentPage - 1) * $perPage; return [ 'total_items' => $totalItems, 'total_pages' => $totalPages, 'current_page' => $currentPage, 'per_page' => $perPage, 'offset' => $offset, 'has_prev' => $currentPage > 1, 'has_next' => $currentPage < $totalPages ]; } /** * Render pagination HTML */ function renderPagination($pagination, $baseUrl) { if ($pagination['total_pages'] <= 1) return ''; $cur = $pagination['current_page']; $total = $pagination['total_pages']; // baseUrl may already contain query params; append page with & $sep = strpos($baseUrl, '?') !== false ? '&' : '?'; $url = fn($p) => $baseUrl . $sep . 'page=' . $p; $btn = fn($href, $label, $active = false, $disabled = false) => '' . $label . ''; $html = '
'; // Prev $html .= $btn($url($cur - 1), '← Prev', false, !$pagination['has_prev']); // Page numbers with ellipsis $pages = []; for ($i = 1; $i <= $total; $i++) { if ($i === 1 || $i === $total || abs($i - $cur) <= 2) { $pages[] = $i; } } $prev = null; foreach ($pages as $p) { if ($prev !== null && $p - $prev > 1) { $html .= ''; } $html .= $btn($url($p), $p, $p === $cur); $prev = $p; } // Next $html .= $btn($url($cur + 1), 'Next →', false, !$pagination['has_next']); $html .= '
'; return $html; } /** * Truncate text */ function truncate($text, $length = 100, $suffix = '...') { if (strlen($text) <= $length) return $text; return substr($text, 0, $length) . $suffix; } /** * Slugify text */ function slugify($text) { $text = preg_replace('~[^\pL\d]+~u', '-', $text); $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); $text = preg_replace('~[^-\w]+~', '', $text); $text = trim($text, '-'); $text = preg_replace('~-+~', '-', $text); return strtolower($text); } /** * Get cart from session */ function getCart() { return $_SESSION['cart'] ?? []; } /** * Add item to cart */ function addToCart($productId, $quantity = 1) { if (!isset($_SESSION['cart'])) { $_SESSION['cart'] = []; } if (isset($_SESSION['cart'][$productId])) { $_SESSION['cart'][$productId] += $quantity; } else { $_SESSION['cart'][$productId] = $quantity; } } /** * Update cart item quantity */ function updateCartItem($productId, $quantity) { if ($quantity <= 0) { removeFromCart($productId); } else { $_SESSION['cart'][$productId] = $quantity; } } /** * Remove item from cart */ function removeFromCart($productId) { unset($_SESSION['cart'][$productId]); } /** * Clear cart */ function clearCart() { $_SESSION['cart'] = []; } /** * Get cart count */ function getCartCount() { return array_sum($_SESSION['cart'] ?? []); } /** * Get cart total */ function getCartTotal() { $total = 0; $cart = getCart(); foreach ($cart as $productId => $quantity) { $product = db()->fetch( "SELECT price, sale_price FROM products WHERE product_id = :id AND is_active = 1", ['id' => $productId] ); if ($product) { $price = $product['sale_price'] ?? $product['price']; $total += $price * $quantity; } } return $total; } /** * Send email via CyberMail API. * Thin wrapper around Email::send() (includes/email.php) so there is a * single implementation of the CyberMail HTTP call, TLS settings, and * error handling instead of two copies that can drift out of sync. */ function sendEmail($to, $subject, $htmlContent, $textContent = '') { require_once __DIR__ . '/email.php'; $result = emailService()->send($to, $subject, $htmlContent, $textContent ?: null); return $result['success'] ?? false; } /** * Log activity */ function logActivity($action, $details = [], $userId = null) { // Implement activity logging if needed } /** * Validate and save an uploaded image, verifying the ACTUAL file content * (not the client-supplied MIME type or filename extension, both of which * are attacker-controlled) before writing it to disk. Used by every admin * image-upload endpoint so the checks live in exactly one place. * * @param string $fieldName Key in $_FILES to read * @param string $destDir Absolute directory to save into (created if missing) * @param string $prefix Filename prefix, e.g. "product_" or "splash_" * @return array ['success' => bool, 'url' => string, 'path' => string] or ['error' => string] */ function handleImageUpload(string $fieldName, string $destDir, string $prefix = 'img_'): array { if (empty($_FILES[$fieldName]) || $_FILES[$fieldName]['error'] !== UPLOAD_ERR_OK) { return ['error' => 'No file received']; } $file = $_FILES[$fieldName]; if ($file['size'] > MAX_UPLOAD_SIZE) { return ['error' => 'File too large. Maximum ' . (MAX_UPLOAD_SIZE / (1024 * 1024)) . 'MB.']; } // Never trust $file['type'] (client-supplied) or the extension in // $file['name'] (attacker-supplied) — inspect the real file bytes. $imageInfo = @getimagesize($file['tmp_name']); if ($imageInfo === false) { return ['error' => 'File is not a valid image.']; } $extensionByMime = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', ]; $mime = $imageInfo['mime']; if (!in_array($mime, ALLOWED_IMAGE_TYPES, true) || !isset($extensionByMime[$mime])) { return ['error' => 'Invalid file type. Use JPG, PNG, WebP, or GIF.']; } if (!is_dir($destDir)) { mkdir($destDir, 0755, true); } $filename = $prefix . time() . '_' . bin2hex(random_bytes(4)) . '.' . $extensionByMime[$mime]; $filepath = rtrim($destDir, '/\\') . '/' . $filename; if (!move_uploaded_file($file['tmp_name'], $filepath)) { return ['error' => 'Failed to save file. Check directory permissions.']; } return ['success' => true, 'filename' => $filename, 'path' => $filepath]; }