H-01: Egypt document uploads - added path traversal prevention (basename),
replaced HTTP_HOST with APP_DOMAIN env var
H-02: 7 remaining hardcoded /home/siro-api/ paths replaced with env vars
(ENV_FILE_PATH, INTERNAL_SOCKET_KEY_PATH, WEBHOOK_SECRET_KEY_PATH)
H-03: serviceapp/updateDriver.php - added ownership check (user_id must match
driverID or user must be admin); non-admins blocked from changing
password/status/email/phone
H-04: ggg.php - replaced weak client-supplied phone auth with proper admin
JWT authentication via JwtService
H-05: Static IV fallback in encrypt_decrypt.php already documented as legacy
H-06: Wallet shared password noted as design limitation (mitigated by
fingerprint verification + short token TTL)
- Also fixed functions.php log message (removed hardcoded path)
533 lines
18 KiB
PHP
533 lines
18 KiB
PHP
<?php
|
|
//functions.php
|
|
|
|
use Firebase\JWT\JWT;
|
|
use Firebase\JWT\Key;
|
|
use Firebase\JWT\ExpiredException;
|
|
use Firebase\JWT\SignatureInvalidException;
|
|
use Firebase\JWT\BeforeValidException;
|
|
|
|
|
|
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
|
|
|
|
|
/**
|
|
* دالة البحث الهجين (Redis + MySQL)
|
|
* @param $redis Client : اتصال الريدز
|
|
* @param $con PDO : اتصال قاعدة البيانات الرئيسية (Main DB)
|
|
* @param $lat float : إحداثيات الراكب
|
|
* @param $lng float : إحداثيات الراكب
|
|
* @param $carType string: نوع الطلب (comfort, speed, Lady...)
|
|
*/
|
|
|
|
function getAllowedSocketUrls(): array {
|
|
$env = getenv('ALLOWED_SOCKET_URLS');
|
|
if ($env) {
|
|
return array_map('trim', explode(',', $env));
|
|
}
|
|
// القيم الافتراضية لو لم تكن موجودة في .env
|
|
return [
|
|
'http://188.68.36.205:2021',
|
|
'http://188.68.36.205:3031',
|
|
'https://location.intaleq.xyz',
|
|
];
|
|
}
|
|
|
|
function isAllowedSocketUrl(string $url): bool {
|
|
$allowed = getAllowedSocketUrls();
|
|
foreach ($allowed as $allowedUrl) {
|
|
if (str_starts_with($url, $allowedUrl)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function sendToLocationServer($action, $data) {
|
|
$url = getenv('LOCATION_SERVER_URL') ?: 'http://188.68.36.205:2021';
|
|
if (!isAllowedSocketUrl($url)) {
|
|
error_log("[SSRF_BLOCKED] Attempted connection to: $url");
|
|
return;
|
|
}
|
|
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
|
|
|
$postData = [
|
|
'action' => $action,
|
|
...$data
|
|
];
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500); // سريع جداً
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
|
|
curl_exec($ch);
|
|
curl_close($ch);
|
|
}
|
|
|
|
function findBestDrivers($con, $lat, $lng, $carType) {
|
|
// 1. الاتصال بـ Redis لجلب الأقرب
|
|
$locationServerUrl = "https://location.intaleq.xyz/api_get_nearby.php";
|
|
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
|
|
|
$postData = ['lat' => $lat, 'lng' => $lng, 'radius' => 5, 'limit' => 100];
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $locationServerUrl);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
|
|
|
|
$response = curl_exec($ch);
|
|
$info = curl_getinfo($ch);
|
|
curl_close($ch);
|
|
|
|
error_log("[findBestDrivers] HTTP Code: " . $info['http_code'] . " Response: " . $response);
|
|
|
|
if ($info['http_code'] !== 200) return [];
|
|
|
|
$json = json_decode($response, true);
|
|
$nearbyDrivers = ($json['status'] ?? false) ? $json['data'] : [];
|
|
|
|
if (empty($nearbyDrivers)) return [];
|
|
|
|
// 2. تجهيز البيانات للفلترة
|
|
$driverIds = [];
|
|
$redisMap = [];
|
|
foreach ($nearbyDrivers as $d) {
|
|
$driverIds[] = $d['id'];
|
|
$redisMap[$d['id']] = $d;
|
|
}
|
|
|
|
$placeholders = implode(',', array_fill(0, count($driverIds), '?'));
|
|
|
|
// تعريف الثوابت
|
|
$CAT_CAR = 1; $CAT_BIKE = 2; $CAT_VAN = 3; $FUEL_ELECTRIC = 3;
|
|
|
|
// 3. الاستعلام (بدون platform)
|
|
$sql = "SELECT
|
|
d.id AS driver_id,
|
|
dt.token,
|
|
cr.year,
|
|
cr.vehicle_category_id,
|
|
d.gender
|
|
FROM driver d
|
|
JOIN CarRegistration cr ON cr.driverID = d.id
|
|
JOIN driverToken dt ON dt.captain_id = d.id
|
|
WHERE d.id IN ($placeholders) ";
|
|
|
|
// ✅ FIX C-01: استخدام allowlist للـ carType لمنع SQL Injection
|
|
$carType = trim($carType);
|
|
$allowedCarTypes = ['Comfort', 'Mishwar Vip', 'Scooter', 'Pink Bike', 'Electric', 'Lady', 'Van', 'Awfar Car', 'Fixed Price', 'Speed', 'Rayeh Gai'];
|
|
if (!in_array($carType, $allowedCarTypes, true)) {
|
|
$carType = 'Speed';
|
|
}
|
|
|
|
$sqlParams = [];
|
|
switch ($carType) {
|
|
case 'Comfort':
|
|
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
|
|
$sqlParams[] = $CAT_CAR;
|
|
$sqlParams[] = 2017;
|
|
break;
|
|
case 'Mishwar Vip':
|
|
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
|
|
$sqlParams[] = $CAT_CAR;
|
|
$sqlParams[] = 2020;
|
|
break;
|
|
case 'Scooter':
|
|
case 'Pink Bike':
|
|
$sql .= " AND cr.vehicle_category_id = ? ";
|
|
$sqlParams[] = $CAT_BIKE;
|
|
break;
|
|
case 'Electric':
|
|
$sql .= " AND cr.vehicle_category_id = ? AND cr.fuel_type_id = ? ";
|
|
$sqlParams[] = $CAT_CAR;
|
|
$sqlParams[] = $FUEL_ELECTRIC;
|
|
break;
|
|
case 'Lady':
|
|
$femaleHash = 'bQ6yWJ2EVXKZooHdGclvmFiDlZCM8UYeO+ILFjDUvpQ=';
|
|
$sql .= " AND cr.vehicle_category_id = ? AND d.gender = ? ";
|
|
$sqlParams[] = $CAT_CAR;
|
|
$sqlParams[] = $femaleHash;
|
|
break;
|
|
case 'Van':
|
|
$sql .= " AND cr.vehicle_category_id = ? ";
|
|
$sqlParams[] = $CAT_VAN;
|
|
break;
|
|
case 'Awfar Car':
|
|
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
|
|
$sqlParams[] = $CAT_CAR;
|
|
$sqlParams[] = 1995;
|
|
break;
|
|
case 'Fixed Price':
|
|
case 'Speed':
|
|
case 'Rayeh Gai':
|
|
default:
|
|
$sql .= " AND cr.vehicle_category_id = ? AND CAST(TRIM(cr.year) AS UNSIGNED) > ? ";
|
|
$sqlParams[] = $CAT_CAR;
|
|
$sqlParams[] = 2000;
|
|
break;
|
|
}
|
|
|
|
try {
|
|
$allParams = array_merge($driverIds, $sqlParams);
|
|
$stmt = $con->prepare($sql);
|
|
$stmt->execute($allParams);
|
|
$finalDrivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// دمج البيانات
|
|
foreach ($finalDrivers as &$driver) {
|
|
$did = $driver['driver_id'];
|
|
if (isset($redisMap[$did])) {
|
|
$driver['distance_km'] = $redisMap[$did]['distance'];
|
|
$driver['lat'] = $redisMap[$did]['lat'];
|
|
$driver['lng'] = $redisMap[$did]['lng'];
|
|
} else {
|
|
$driver['distance_km'] = 999;
|
|
}
|
|
}
|
|
|
|
// الترتيب
|
|
usort($finalDrivers, function($a, $b) {
|
|
return $a['distance_km'] <=> $b['distance_km'];
|
|
});
|
|
|
|
return array_slice($finalDrivers, 0, 30);
|
|
} catch (Exception $e) {
|
|
error_log("FindBestDrivers Error: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
// --- دالة مساعدة لمخاطبة سيرفر السائقين (Location Socket) ---
|
|
function notifyDriversRideTaken($rideId, $winnerDriverId) {
|
|
$url = "http://188.68.36.205:2021";
|
|
if (!isAllowedSocketUrl($url)) return;
|
|
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
|
|
|
$postData = [
|
|
'action' => 'ride_taken_event', // هذا الأكشن الجديد في السوكيت
|
|
'ride_id' => $rideId,
|
|
'taken_by_driver_id' => $winnerDriverId
|
|
];
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500); // نصف ثانية فقط، لا نريد تعطيل الـ API
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
|
|
|
|
$response = curl_exec($ch);
|
|
curl_close($ch);
|
|
}
|
|
function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = null) {
|
|
$url = "http://188.68.36.205:2021";
|
|
if (!isAllowedSocketUrl($url)) return null;
|
|
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
|
|
|
$postData = [
|
|
'action' => 'dispatch_order', // اسم الحدث المتفق عليه في socket_server.php هناك
|
|
'drivers_ids' => json_encode($drivers_ids_array), // نحول المصفوفة لنص JSON
|
|
'ride_id' => $rideId ?? '', // ✅ تصحيح اسم المتغير
|
|
'payload' => $payload
|
|
];
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1000); // لا تنتظر أكثر من ثانية
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
"x-internal-key: $INTERNAL_KEY"
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
if (curl_errno($ch)) {
|
|
error_log("Curl Error (Location Socket): " . curl_error($ch));
|
|
}
|
|
|
|
curl_close($ch);
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* 🚀 دالة إشعار الراكب (تعمل على سيرفر الرحلات)
|
|
* تخاطب السوكيت الموجود محلياً على نفس السيرفر
|
|
*/
|
|
function notifyPassengerOnRideServer($passenger_id, $payload) {
|
|
$url = "http://188.68.36.205:3031";
|
|
if (!isAllowedSocketUrl($url)) return null;
|
|
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
|
|
|
if (empty($INTERNAL_KEY)) {
|
|
error_log("[SOCKET_CRITICAL] Internal socket key missing");
|
|
}
|
|
|
|
$postData = [
|
|
'action' => 'update_ride_status',
|
|
'passenger_id' => $passenger_id,
|
|
'payload' => $payload
|
|
];
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 3000);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
"x-internal-key: $INTERNAL_KEY"
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
if (curl_errno($ch)) {
|
|
error_log("[SOCKET_DEBUG] Curl Error (Passenger Socket) to $url: " . curl_error($ch));
|
|
} else {
|
|
error_log("[SOCKET_DEBUG] Sent to Passenger Socket $url | HTTP: $httpCode | Response: $response | Passenger: $passenger_id");
|
|
}
|
|
|
|
curl_close($ch);
|
|
return $response;
|
|
}
|
|
|
|
// ============================================================================
|
|
// دالة توزيع الطلب (محدثة لاستخدام sendFCM_Internal)
|
|
// ============================================================================
|
|
// ============================================================================
|
|
// دالة توزيع الطلب (Dispatch Function) - النسخة المصححة
|
|
// ============================================================================
|
|
function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNameLoc, $encryptionHelper) {
|
|
$countDrivers = count($driversData);
|
|
error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers");
|
|
|
|
$socketUrl = 'http://188.68.36.205:2021';
|
|
if (!isAllowedSocketUrl($socketUrl)) return;
|
|
$internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
|
|
|
foreach ($driversData as $driver) {
|
|
$driverId = $driver['driver_id'];
|
|
$rawToken = $driver['token'] ?? '';
|
|
|
|
error_log("--------------------------------------------------");
|
|
error_log("👤 [DRIVER_PROCESS] Processing Driver ID: $driverId");
|
|
|
|
// 1. معالجة التوكن
|
|
$driverToken = processDriverToken($rawToken, $encryptionHelper);
|
|
|
|
// تجهيز البيانات الخاصة بالسائق
|
|
$payloadForDriver = $payloadTemplate;
|
|
$payloadForDriver[6] = (string)$driverId;
|
|
$payloadForDriver[18] = (string)$driverId;
|
|
|
|
// 2. إرسال السوكيت
|
|
sendSocketNotification($driverId, $rideId, $payloadForDriver, $socketUrl, $internalKey);
|
|
|
|
// 3. إرسال FCM
|
|
if (!empty($driverToken)) {
|
|
$fcmData = [
|
|
'DriverList' => $payloadForDriver,
|
|
'order_id' => (string)$rideId
|
|
];
|
|
$fcmResult = sendFcmNotification(
|
|
$driverToken,
|
|
"طلب جديد 🔔",
|
|
"هناك رحلة جديدة من " . $startNameLoc,
|
|
$fcmData,
|
|
"Order",
|
|
"ding"
|
|
);
|
|
error_log("📲 [FCM_RESULT] " . json_encode($fcmResult));
|
|
} else {
|
|
error_log("⚠️ [FCM_SKIP] No valid token for Driver $driverId");
|
|
}
|
|
}
|
|
error_log("🏁 [DISPATCH_END] RideID: $rideId");
|
|
}
|
|
|
|
/**
|
|
* معالجة توكن السائق وفك تشفيره
|
|
*/
|
|
function processDriverToken($rawToken, $encryptionHelper) {
|
|
if (empty($rawToken)) {
|
|
error_log("🚫 [TOKEN_MISSING] No token found.");
|
|
return '';
|
|
}
|
|
try {
|
|
$decrypted = $encryptionHelper->decryptData($rawToken);
|
|
if ($decrypted !== false && !empty($decrypted)) {
|
|
error_log("✅ [TOKEN_DECRYPT] Success.");
|
|
return trim($decrypted);
|
|
}
|
|
error_log("⚠️ [TOKEN_DECRYPT] Failed. Using Raw.");
|
|
return $rawToken;
|
|
} catch (Exception $e) {
|
|
error_log("❌ [TOKEN_EXCEPTION] Error. Using Raw.");
|
|
return $rawToken;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* إرسال إشعار السوكيت لسيرفر اللوكيشن
|
|
*/
|
|
function sendSocketNotification($driverId, $rideId, $payload, $url, $internalKey) {
|
|
$postData = [
|
|
'action' => 'dispatch_order',
|
|
'drivers_ids' => json_encode([$driverId]),
|
|
'ride_id' => $rideId,
|
|
'payload' => $payload
|
|
];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
|
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
|
|
|
|
$res = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
error_log("📡 [SOCKET_SEND] Driver $driverId | HTTP: $httpCode");
|
|
}
|
|
|
|
/**
|
|
* إرسال إشعار FCM الموحد
|
|
*/
|
|
function sendFcmNotification($token, $title, $body, $data, $category, $tone) {
|
|
if (class_exists('FcmService')) {
|
|
global $redis;
|
|
$fcmService = new FcmService($redis ?? null);
|
|
return $fcmService->send($token, $title, $body, $data, $category, $tone);
|
|
} elseif (function_exists('sendFCM_Internal')) {
|
|
return sendFCM_Internal($token, $title, $body, $data, $category, false, $tone);
|
|
}
|
|
return ['status' => 'error', 'message' => 'FCM service not loaded'];
|
|
}
|
|
|
|
|
|
|
|
|
|
function authenticateJWT(): object
|
|
{
|
|
global $redis;
|
|
if (!class_exists('JwtService')) {
|
|
require_once __DIR__ . '/core/Auth/JwtService.php';
|
|
}
|
|
$jwtService = new JwtService($redis ?? null);
|
|
return $jwtService->authenticate();
|
|
}
|
|
define("MB", 1048576);
|
|
|
|
/**
|
|
* Send WhatsApp message using your server's API
|
|
*
|
|
* @param string $to The recipient phone number (e.g., 96279xxxxxxx)
|
|
* @param string $message The message to send
|
|
* @return mixed API response object or false on failure
|
|
*/
|
|
|
|
function sendWhatsAppFromServer($to, $message)
|
|
{
|
|
// 1) قائمة السيرفرات المتاحة
|
|
$servers = [
|
|
//"https://botmasa.intaleq.xyz/send",//mayar
|
|
// "https://botmasa2.intaleq.xyz/send",//shad
|
|
"https://bot5.intaleq.xyz/send",//ramat bus
|
|
"https://bot3.intaleq.xyz/send",//shahd
|
|
//"https://whatsapp.tripz-egypt.com/send"//tripz
|
|
];
|
|
|
|
// 2) محاولة الإرسال (Primary -> Fallback)
|
|
$response = null;
|
|
$success = false;
|
|
|
|
foreach ($servers as $url) {
|
|
$curl = curl_init();
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 3, // مهلة قصيرة للمحاولة
|
|
CURLOPT_CUSTOMREQUEST => "POST",
|
|
CURLOPT_POSTFIELDS => json_encode([
|
|
"to" => $to,
|
|
"message" => $message
|
|
], JSON_UNESCAPED_UNICODE),
|
|
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
|
|
]);
|
|
|
|
$response = curl_exec($curl);
|
|
$err = curl_error($curl);
|
|
curl_close($curl);
|
|
|
|
if (!$err) {
|
|
$success = true;
|
|
break;
|
|
} else {
|
|
error_log("[sendWhatsAppFromServer] Server $url failed, trying next... Error: $err");
|
|
}
|
|
}
|
|
|
|
if (!$success) return false;
|
|
return json_decode($response, true);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sendFCM_Internal(
|
|
$target,
|
|
$title,
|
|
$body,
|
|
$customData = [],
|
|
$category = 'Order',
|
|
$isTopic = false,
|
|
$tone = 'order'
|
|
) {
|
|
global $redis;
|
|
if (!class_exists('FcmService')) {
|
|
require_once __DIR__ . '/core/Services/FcmService.php';
|
|
}
|
|
$fcm = new FcmService($redis ?? null);
|
|
|
|
return $fcm->send($target, $title, $body, is_array($customData) ? $customData : [], $category, $tone);
|
|
}
|
|
|
|
|
|
|
|
function logAudit($con, $adminId, $action, $tableName = null, $recordId = null, $details = null) {
|
|
try {
|
|
if (empty($adminId)) {
|
|
$adminId = 'unknown_admin';
|
|
}
|
|
$stmt = $con->prepare("
|
|
INSERT INTO `admin_audit_log` (`admin_id`, `action`, `table_name`, `record_id`, `details`)
|
|
VALUES (:admin_id, :action, :table_name, :record_id, :details)
|
|
");
|
|
$stmt->execute([
|
|
':admin_id' => $adminId,
|
|
':action' => $action,
|
|
':table_name' => $tableName,
|
|
':record_id' => $recordId,
|
|
':details' => is_array($details) ? json_encode($details, JSON_UNESCAPED_UNICODE) : $details
|
|
]);
|
|
return true;
|
|
} catch (Exception $e) {
|
|
error_log("Audit Log Error: " . $e->getMessage());
|
|
return $e->getMessage();
|
|
}
|
|
}
|