إضافة getRideOrderID.php المفقود — المسار الاحتياطي لبيانات السائق

تطبيق الراكب يطلب /ride/rides/getRideOrderID.php منذ البداية، والموجود على
السيرفر getRideOrderIDNew.php فقط ⇒ 404 صامت في getUpdatedRideForDriverApply.

هذا يفسّر عرضين ظنّاهما منفصلين:
- الـ polling يسبق الـ FCM فيضبط _isAcceptanceProcessed=true ثم يأخذ 404،
  فيُرفض بعدها payload الـ FCM الذي يحمل driver_info كاملاً
  ("Already processed") ⇒ الرحلة مقبولة بلا معلومات سائق.
- ولأن driverToken يبقى فارغاً، ترد send_fcm.php بـ 400 Missing: target
  ⇒ رسائل الراكب للسائق لا تُرسَل إطلاقاً. (الاتجاه المعاكس كان يعمل بعد
  إصلاح getRideWaiting.php — نفس خطأ التشفير معكوساً.)

getRideOrderIDNew.php لا يصلح بديلاً: داخلي عبر get_connect.php →
validateInternalKey فلا يستطيع التطبيق مناداته، ولا يرجع
ratingCount/completedRides/driverTier.

النقطة الجديدة:
- connect.php (JWT)، والراكب من الـ JWT فقط لا من الطلب (حماية IDOR).
- نفس استعلام acceptRide.php وشكل رده حرفياً حتى يقرأه
  _fillDriverDataLocally بنفس المفاتيح.
- فك تشفير driverToken.token — بلا ذلك يصل التطبيق blob يستخدمه كـ FCM
  target فيرفضه FCM بـ 400.
- تفادي تصادم المفاتيح: getUpdatedRideForDriverApply يقرأ
  passengerName + last_name كاسم الراكب، فلقب السائق نُقل إلى
  driver_last_name واسم الراكب الحقيقي يُرجَّع في مكانه.
- ride DB هو المرجع مع fallback على primary، ورحلة بلا سائق ترد success
  لا failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-26 16:18:02 +03:00
co-authored by Claude Opus 5
parent 143146c1b4
commit f66db7db42
+202
View File
@@ -0,0 +1,202 @@
<?php
// ═══════════════════════════════════════════════════════════════
// ride/rides/getRideOrderID.php
// PURPOSE : بيانات السائق/السيارة للرحلة الحالية — المسار الاحتياطي للراكب
// حين لا يوصل السوكيت driver_info (ride_lifecycle_controller.dart
// → getUpdatedRideForDriverApply).
//
// لماذا هذا الملف: التطبيق يطلب getRideOrderID.php منذ البداية، والموجود
// كان getRideOrderIDNew.php فقط ⇒ 404 صامت. ونتيجته أن الـ polling يسبق
// الـ FCM فيضبط _isAcceptanceProcessed=true بلا بيانات، فيُرفض لاحقاً
// payload الـ FCM الذي يحمل driver_info كاملاً ("Already processed")،
// فتظهر الرحلة مقبولة بلا معلومات سائق، ويبقى driverToken فارغاً فلا
// تُرسَل رسائل الراكب للسائق.
//
// getRideOrderIDNew.php لا يصلح بديلاً مباشراً: هو داخلي (get_connect.php →
// validateInternalKey) ولا يستطيع التطبيق مناداته، ولا يرجع
// ratingCount/completedRides/driverTier.
//
// الرد يطابق شكل driverInfo في acceptRide.php حرفياً حتى يقرأه
// _fillDriverDataLocally و getUpdatedRideForDriverApply بنفس المفاتيح.
// ═══════════════════════════════════════════════════════════════
include "../../connect.php";
// الراكب من الـ JWT فقط — لا نثق بأي passengerID قادم من الطلب (حماية IDOR)
$passengerId = $user_id;
$rideIdParam = filterRequest("id");
if (empty($passengerId)) {
printFailure("Unauthorized");
exit;
}
try {
// ── 1. الرحلة: ride DB هو المرجع، مع fallback على primary ──────
$rideRow = null;
$sqlRide = "SELECT id, driver_id, passenger_id, status
FROM `ride`
WHERE passenger_id = ? ";
$params = [$passengerId];
if (!empty($rideIdParam)) {
$sqlRide .= "AND id = ? ";
$params[] = $rideIdParam;
}
$sqlRide .= "ORDER BY id DESC LIMIT 1";
try {
$con_ride = Database::get('ride');
$stmtRide = $con_ride->prepare($sqlRide);
$stmtRide->execute($params);
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
} catch (Exception $eRideDb) {
error_log("[getRideOrderID] ride DB unavailable: " . $eRideDb->getMessage());
}
if (!$rideRow) {
$stmtRide = $con->prepare($sqlRide);
$stmtRide->execute($params);
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
}
if (!$rideRow) {
printFailure("No ride found");
exit;
}
$driverId = $rideRow['driver_id'] ?? '';
if (empty($driverId)) {
// رحلة لم يقبلها سائق بعد — ليست حالة خطأ
echo json_encode([
"status" => "success",
"message" => "No driver assigned yet",
"data" => ["ride_id" => (string)$rideRow['id'], "status" => $rideRow['status']],
]);
exit;
}
// ── 2. بيانات السائق — نفس استعلام acceptRide.php ──────────────
$stmtDriver = $con->prepare("
SELECT
d.id AS driver_id,
d.first_name,
d.last_name,
d.gender,
d.phone,
c.make,
c.model,
c.car_plate,
c.year,
c.color,
c.color_hex,
(SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver,
(SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount,
(SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides,
dt.token
FROM driver d
LEFT JOIN CarRegistration c ON c.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
WHERE d.id = ?
LIMIT 1
");
$stmtDriver->execute([$driverId]);
$driverRaw = $stmtDriver->fetch(PDO::FETCH_ASSOC);
if (!$driverRaw) {
printFailure("Driver not found");
exit;
}
// جدول driverToken يخزّن التوكن مشفّراً — بلا فك تشفير يصل التطبيق
// blob يستخدمه كـ FCM target فيرفضه FCM بـ 400 ولا تصل رسائل الراكب.
$driverInfo = [];
$encryptedFields = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
foreach ($driverRaw as $key => $value) {
if (in_array($key, $encryptedFields, true) && !empty($value)) {
$decrypted = false;
try {
$decrypted = $encryptionHelper->decryptData($value);
} catch (\Throwable $e) {
$decrypted = false;
}
$driverInfo[$key] = ($decrypted !== false && $decrypted !== null && $decrypted !== '')
? $decrypted
: $value;
} else {
$driverInfo[$key] = $value;
}
}
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
if (empty($driverInfo['driverName']) && !empty($driverInfo['phone'])) {
$driverInfo['driverName'] = $driverInfo['phone'];
}
$driverInfo['ratingDriver'] = !empty($driverInfo['ratingDriver']) ? (string)$driverInfo['ratingDriver'] : "5.0";
$ratingValue = (float) $driverInfo['ratingDriver'];
$ratingCount = (int) ($driverInfo['ratingCount'] ?? 0);
$completedRides = (int) ($driverInfo['completedRides'] ?? 0);
if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) {
$driverInfo['driverTier'] = 'Professional driver';
} elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) {
$driverInfo['driverTier'] = 'Trusted driver';
} else {
$driverInfo['driverTier'] = 'Verified driver';
}
// ── 3. آخر موقع للسائق من Redis (اختياري) ──────────────────────
if (isset($redisLocation) && $redisLocation) {
try {
$driverLoc = $redisLocation->hGetAll("driver:location:$driverId");
if (!empty($driverLoc)) {
$driverInfo['lat'] = (float)($driverLoc['lat'] ?? 0);
$driverInfo['lng'] = (float)($driverLoc['lng'] ?? 0);
$driverInfo['heading'] = (float)($driverLoc['heading'] ?? 0);
}
} catch (Exception $eLoc) {
// تجاهل — الموقع يصل بالسوكيت أصلاً
}
}
// ⚠️ تصادم مفاتيح: getUpdatedRideForDriverApply يقرأ passengerName + last_name
// كاسم **الراكب**. لو تركنا last_name للسائق يظهر لقب السائق كاسم الراكب.
// ننقل لقب السائق إلى driver_last_name (وهو مفتاح يفهمه
// _fillDriverDataLocally أصلاً) ونرجّع اسم الراكب الحقيقي في مكانه.
$driverInfo['driver_last_name'] = $driverInfo['last_name'] ?? '';
unset($driverInfo['last_name']);
try {
$stmtPas = $con->prepare("SELECT first_name, last_name FROM passengers WHERE id = ? LIMIT 1");
$stmtPas->execute([$passengerId]);
$pasRow = $stmtPas->fetch(PDO::FETCH_ASSOC);
if ($pasRow) {
foreach (['first_name' => 'passengerName', 'last_name' => 'last_name'] as $src => $dst) {
$val = $pasRow[$src] ?? '';
if ($val === '' || $val === null) { $driverInfo[$dst] = ''; continue; }
$dec = false;
try {
$dec = $encryptionHelper->decryptData($val);
} catch (\Throwable $e) {
$dec = false;
}
$driverInfo[$dst] = ($dec !== false && $dec !== null && $dec !== '') ? $dec : $val;
}
}
} catch (PDOException $ePas) {
error_log("[getRideOrderID] passenger name lookup WARNING: " . $ePas->getMessage());
}
$driverInfo['ride_id'] = (string)$rideRow['id'];
$driverInfo['status'] = $rideRow['status'];
echo json_encode([
"status" => "success",
"message" => "Driver info",
"data" => $driverInfo,
]);
} catch (PDOException $e) {
error_log("[getRideOrderID] CRITICAL: " . $e->getMessage());
printFailure("Server error");
}