feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/../../connect.php';
// Sanitize and validate input
$driverId = filterRequest("driverId");
$issueDate = filterRequest("IssueDate");
$inspectionResult = filterRequest("InspectionResult");
// Prepare SQL statement
$sql = "INSERT INTO criminalDocuments (driverId, IssueDate, InspectionResult)
VALUES (:driverId, :issueDate, :inspectionResult)";
try {
$stmt = $con->prepare($sql);
// Bind parameters
$stmt->bindParam(':driverId', $driverId, PDO::PARAM_INT);
$stmt->bindParam(':issueDate', $issueDate, PDO::PARAM_STR);
$stmt->bindParam(':inspectionResult', $inspectionResult, PDO::PARAM_STR);
// Execute the statement
$stmt->execute();
// Check if the insertion was successful
if ($stmt->rowCount() > 0) {
jsonSuccess(null, "Criminal document data saved successfully");
} else {
jsonError("Failed to save criminal document data");
}
} catch (PDOException $e) {
// Log the error and print a generic failure message
error_log("Database Error: " . $e->getMessage());
jsonError("An error occurred while saving the data");
}
?>
@@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/../../connect.php';
// استقبال وتشفير رقم الهاتف
$phoneNumber = filterRequest("phone_number");
$phoneNumber = otpPhoneKey($phoneNumber);
// تجهيز الاستعلام باستخدام bindParam للحماية
$sql = "SELECT * FROM `phone_verification` WHERE `phone_number` = :phone_number";
$stmt = $con->prepare($sql);
$stmt->bindParam(":phone_number", $phoneNumber);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess($rows);
} else {
jsonError("No phone verified yet found");
}
?>
+61
View File
@@ -0,0 +1,61 @@
<?php
require_once __DIR__ . '/../../connect.php';
$id = filterRequest("id");
// يمكن استقبال سبب الحظر من التطبيق أو وضعه كقيمة افتراضية
$reason = "Driver requested deletion (deleteFromHimself)";
// تأكد أن المعرف رقم صحيح
if (!is_numeric($id)) {
jsonError("Invalid ID");
exit();
}
try {
// 1. جلب رقم الهاتف الخاص بالسائق قبل التحديث
// نحتاج الهاتف لإضافته في القائمة السوداء
$stmtPhone = $con->prepare("SELECT phone FROM `driver` WHERE `id` = :id");
$stmtPhone->bindParam(':id', $id, PDO::PARAM_INT);
$stmtPhone->execute();
$driverData = $stmtPhone->fetch(PDO::FETCH_ASSOC);
// التحقق من وجود السائق
if (!$driverData) {
jsonError("Driver not found");
exit();
}
$phone = $driverData['phone'];
// 2. تحديث حالة السائق
$sql = "UPDATE `driver` SET `status` = 'deleteFromHimself' WHERE `id` = :id";
$stmt = $con->prepare($sql);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
if ($stmt->rowCount() > 0) {
// 3. الإضافة إلى القائمة السوداء (blacklist_driver)
// نستخدم NOW() لتسجيل الوقت الحالي تلقائياً
// لا نمرر id العمود الأول لأنه غالباً Auto Increment في قاعدة البيانات
$insertSql = "INSERT INTO `blacklist_driver` (`driver_id`, `phone`, `reason`, `created_at`)
VALUES (:driver_id, :phone, :reason, NOW())";
$insertStmt = $con->prepare($insertSql);
$insertStmt->execute([
':driver_id' => $id,
':phone' => $phone,
':reason' => $reason
]);
jsonSuccess(null, "Record marked as deleted and added to blacklist successfully");
} else {
jsonError("Failed to update record or no change made");
}
} catch (PDOException $e) {
// في حال حدوث خطأ في قاعدة البيانات (مثلاً تكرار الإضافة)
error_log("[deletecaptainAccounr] " . $e->getMessage());
jsonError("Database Error");
}
?>
+59
View File
@@ -0,0 +1,59 @@
<?php
require_once __DIR__ . '/../../../connect.php';
// 🔥 [Fix Broken Access Control] كان يتحقق من صلاحية التوكن فقط — أي مستخدم
// مسجّل دخول (راكب/سائق آخر) كان يقدر يجلب بيانات أي سائق مفكوكة التشفير
// (هوية وطنية، هاتف، عنوان...) بالإضافة لروابط وثائقه الشخصية.
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
exit;
}
$driverId = filterRequest("id");
if (empty($driverId)) {
jsonError("driver_id is required.");
exit;
}
try {
// تفاصيل السائق
$sql = "SELECT * FROM driver WHERE id = :id LIMIT 1";
$stmt = $con->prepare($sql);
$stmt->execute([':id' => $driverId]);
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$driver) {
jsonError("Driver not found.");
exit;
}
// فك التشفير للحقول الحساسة
foreach ($driver as $k => $v) {
if (in_array($k, ['phone',
'email',
'first_name',
'last_name',
'national_number',
'address','gender','site',
'birthdate',
'name_arabic'])) {
$driver[$k] = $encryptionHelper->decryptData($v);
}
}
// الوثائق
$sql2 = "SELECT doc_type, image_name, link FROM driver_documents WHERE driverID = :id";
$stmt2 = $con->prepare($sql2);
$stmt2->execute([':id' => $driverId]);
$docs = $stmt2->fetchAll(PDO::FETCH_ASSOC);
printSuccess([
"driver" => $driver,
"documents" => $docs
]);
} catch (PDOException $e) {
error_log("[driver_details] " . $e->getMessage());
jsonError("Error fetching details");
}
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/../../../connect.php';
// 🔥 [Fix Broken Access Control] كان يتحقق من صلاحية التوكن فقط بدون التحقق
// من الدور — أي توكن صالح (حتى راكب) كان يقدر يسحب قائمة السائقين المعلّقين
// وبياناتهم الشخصية المفكوكة التشفير.
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
exit;
}
$limit = isset($_POST['limit']) ? (int)$_POST['limit'] : (isset($_GET['limit']) ? (int)$_GET['limit'] : 10);
$offset = isset($_POST['offset']) ? (int)$_POST['offset'] : (isset($_GET['offset']) ? (int)$_GET['offset'] : 0);
try {
$sql = "SELECT id, first_name, last_name, phone FROM driver WHERE status <> 'active' ORDER BY id DESC LIMIT :limit OFFSET :offset";
$stmt = $con->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// فك التشفير
foreach ($rows as &$r) {
$r['phone'] = $encryptionHelper->decryptData($r['phone']);
$r['first_name'] = $encryptionHelper->decryptData($r['first_name']);
$r['last_name'] = $encryptionHelper->decryptData($r['last_name']);
}
jsonSuccess($rows); // يرجع كـ message: [...]
} catch (PDOException $e) {
error_log("[drivers_pending_list] " . $e->getMessage());
jsonError("Error fetching data");
}
+24
View File
@@ -0,0 +1,24 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driverID = filterRequest("id");
// تحقق أن المعرف رقم صحيح
if (!is_numeric($driverID)) {
jsonError("Invalid driver ID");
exit();
}
// استخدم bindParam لتفادي حقن SQL
$sql = "SELECT `accountBank` FROM `driver` WHERE `id` = :id";
$stmt = $con->prepare($sql);
$stmt->bindParam(':id', $driverID, PDO::PARAM_INT);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess($row);
} else {
jsonError("No account bank record found");
}
?>
@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../../connect.php';
// $driverID = filterRequest("id");
$sql = "
SELECT * FROM `promptDriverIDEgypt`";
$stmt = $con->prepare($sql);
$stmt->execute();
if ($stmt->rowCount() > 0) {
// Fetch the record
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess($row);
}
else{
// Print a failure message
jsonError($message = "No wallet record found");
}
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/../../../connect.php';
$phoneNumber = filterRequest("phone_number");
// تشفير الرقم قبل البحث
$phoneNumber_encrypted = otpPhoneKey($phoneNumber);
try {
// الاستعلام عن السائق حسب رقم الهاتف وحالة التحقق
$stmt = $con->prepare("
SELECT * FROM phone_verification
WHERE phone_number = ? AND is_verified = 1
");
$stmt->execute([$phoneNumber_encrypted]);
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
if ($driver) {
jsonSuccess(null, "Phone number is verified.");
} else {
jsonError("Phone number is not verified or does not exist.");
}
} catch (PDOException $e) {
error_log("[isPhoneVerified] " . $e->getMessage());
jsonError("Database error");
}
+123
View File
@@ -0,0 +1,123 @@
<?php
require_once __DIR__ . '/../../connect.php';
global $blindIndex;
$email = filterRequest('email');
$phone = filterRequest('phone');
$password = filterRequest('password');
if (empty($phone) && empty($email)) {
jsonError("Phone or email is required.");
exit;
}
$conditions = [];
$params = [];
if (!empty($phone)) {
$phoneEnc = $encryptionHelper->encryptData($phone);
$conditions[] = "driver.phone = :phone";
$params[':phone'] = $phoneEnc;
$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null;
if ($phoneBidx) {
$conditions[] = "driver.phone_bidx = :phone_bidx";
$params[':phone_bidx'] = $phoneBidx;
}
}
if (!empty($email)) {
$emailEnc = $encryptionHelper->encryptData($email);
$conditions[] = "driver.email = :email";
$params[':email'] = $emailEnc;
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null;
if ($emailBidx) {
$conditions[] = "driver.email_bidx = :email_bidx";
$params[':email_bidx'] = $emailBidx;
}
}
$whereClause = implode(' OR ', $conditions);
$sql = "SELECT
driver.id,
driver.phone,
driver.email,
driver.password,
driver.gender,
driver.birthdate,
driver.site,
driver.first_name,
driver.last_name,
driver.education,
driver.employmentType,
driver.maritalStatus,
driver.created_at,
driver.updated_at,
driver.email AS _email_enc
FROM
driver
WHERE
$whereClause";
/**
* حالة توثيق البريد.
*
* كان الاستعلام يربط email_verifications.email بعمود البريد في الحساب، لكن
* الأول يُخزَّن نصاً صريحاً والثاني مشفّراً — فالربط لم يكن يطابق شيئاً أصلاً
* وكانت verified تعود NULL دائماً. نجلبها هنا بالبريد الأصلي.
*/
function fetchEmailVerified(PDO $con, ?string $plainEmail): ?int
{
if (!$plainEmail) return null;
try {
$st = $con->prepare("SELECT verified FROM email_verifications WHERE email = ? LIMIT 1");
$st->execute([$plainEmail]);
$v = $st->fetchColumn();
return $v === false ? null : (int) $v;
} catch (PDOException $e) {
error_log('[email_verifications] ' . $e->getMessage());
return null;
}
}
$stmt = $con->prepare($sql);
$stmt->execute($params);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = count($data);
if ($count > 0) {
$plainEmail = $encryptionHelper->decryptData($data[0]['_email_enc'] ?? null) ?: null;
$data[0]['verified'] = fetchEmailVerified($con, $plainEmail);
unset($data[0]['_email_enc']);
}
if ($count > 0) {
$stored_password = $data[0]['password'];
if (password_verify($password, $stored_password)) {
// فك التشفير للحقول الحساسة
$data[0]['phone'] = $encryptionHelper->decryptData($data[0]['phone']);
$data[0]['email'] = $encryptionHelper->decryptData($data[0]['email']);
$data[0]['gender'] = $encryptionHelper->decryptData($data[0]['gender']);
$data[0]['birthdate'] = $encryptionHelper->decryptData($data[0]['birthdate']);
$data[0]['site'] = $encryptionHelper->decryptData($data[0]['site']);
$data[0]['first_name'] = $encryptionHelper->decryptData($data[0]['first_name']);
$data[0]['last_name'] = $encryptionHelper->decryptData($data[0]['last_name']);
$data[0]['education'] = $encryptionHelper->decryptData($data[0]['education']);
$data[0]['employmentType'] = $encryptionHelper->decryptData($data[0]['employmentType']);
$data[0]['maritalStatus'] = $encryptionHelper->decryptData($data[0]['maritalStatus']);
unset($data[0]['password']); // لا نرجّع الباسورد
jsonSuccess($data);
} else {
jsonError("Incorrect password.");
}
} else {
jsonError("User does not exist.");
}
?>
+101
View File
@@ -0,0 +1,101 @@
<?php
// loginFromGoogle.php
require_once __DIR__ . '/../../connect.php';
try {
/* ────────────────────────────────
1) استخدام ID: من الطلب أولاً، ثم من JWT
───────────────────────────────── */
$driverID = filterRequest('driver_id') ?: $user_id;
error_log("[Debug] DriverID from JWT: $driverID");
/* ────────────────────────────────
3) إعداد الاستعلام الموحَّد
───────────────────────────────── */
$sql = "
SELECT
driver.id, driver.phone, driver.email, driver.gender, driver.birthdate,
driver.site, driver.first_name, driver.last_name, driver.bankCode,
driver.accountBank, driver.employmentType,driver.status, driver.maritalStatus,
driver.created_at, driver.updated_at,
phone_verification.is_verified,
CarRegistration.make, CarRegistration.model, CarRegistration.year,
df.is_claimed, inv.isInstall, inv.isGiftToken
FROM driver
LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone_key
LEFT JOIN driver_gifts df ON df.driver_id = driver.id
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
LEFT JOIN invites inv ON inv.driverId = driver.id
WHERE
driver.id = :id
-- AND phone_verification.is_verified = '1'
LIMIT 1
";
// error_log("[Debug] queryString:\n$sql");
$stmt = $con->prepare($sql);
// باراميترات الربط
$params = [
':id' => $driverID,
];
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v);
}
/* ───────── dumpParams (اختياري) ───────── */
ob_start();
$stmt->debugDumpParams();
error_log("[Debug] dumpParams:\n" . ob_get_clean());
/* ────────────────────────────────
4) تنفيذ الاستعلام
───────────────────────────────── */
$stmt->execute();
error_log("[Debug] stmt->rowCount(): " . $stmt->rowCount());
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// error_log("[Debug] Raw fetched JSON: " . json_encode($rows, JSON_UNESCAPED_UNICODE));
if (!$rows) {
jsonError("User does not exist or phone not verified.");
exit;
}
/* ────────────────────────────────
5) فك التشفير للحقول الحسّاسة
───────────────────────────────── */
$data = &$rows[0]; // مرجع لتوفير الذاكرة
$decryptIfNotNull = function($field) use (&$data, $encryptionHelper) {
if (isset($data[$field]) && $data[$field] !== null) {
$data[$field] = $encryptionHelper->decryptData($data[$field]);
}
};
foreach ([
'phone', 'email', 'gender', 'birthdate', 'site',
'first_name', 'last_name'
] as $field) {
$decryptIfNotNull($field);
}
error_log("[Debug] Raw fetched JSON: " . json_encode($rows, JSON_UNESCAPED_UNICODE));
echo json_encode([
"status" => "success",
"count" => 1,
"data" => $rows // نتيجة واحدة فقط
], JSON_UNESCAPED_UNICODE);
} catch (PDOException $e) {
error_log("[PDO ERROR] " . $e->getMessage());
jsonError("Database error: ".$e->getCode());
} catch (Exception $e) {
error_log("[GENERAL ERROR] " . $e->getMessage());
jsonError("Error occurred.");
} finally {
$stmt = null;
$con = null;
}
?>
@@ -0,0 +1,115 @@
<?php
// ============================================================
// loginUsingCredentialsWithoutGoogle.php
// مخصص لدخول الفاحصين (Testers) بالإيميل والباسورد
// ============================================================
require_once __DIR__ . '/../../core/bootstrap.php';
$email = filterRequest('email');
$password = filterRequest('password');
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
if (!$email || !$password) {
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
exit();
}
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
if (empty($allowedEmails)) {
$allowedEmails = [
'driver_tester@siromove.com',
'passenger_tester@siromove.com',
];
}
$cleanEmail = strtolower(trim($email));
$isTester = in_array($cleanEmail, $allowedEmails) ||
substr($cleanEmail, -13) === '@siromove.com' ||
str_contains($cleanEmail, 'tester') ||
str_contains($cleanEmail, 'reviewer');
// تشفير الإيميل لاستخدامه في الاستعلام
$encryptedEmail = $encryptionHelper->encryptData($email);
try {
$con = Database::get('main');
// Auto-seed/create tester driver logic removed for security
global $blindIndex;
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null;
// SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر أو الفهرس الأعمى
$sql = "SELECT
driver.*,
phone_verification.is_verified,
CarRegistration.make,
CarRegistration.model,
CarRegistration.year
FROM driver
LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone_key
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
WHERE
driver.email = :email OR (:email_bidx IS NOT NULL AND driver.email_bidx = :email_bidx)
LIMIT 1";
$stmt = $con->prepare($sql);
$stmt->execute([':email' => $encryptedEmail, ':email_bidx' => $emailBidx]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
// التحقق من أن الحساب معلم كحساب فحص في قاعدة البيانات أو البيئة
$isTestInDb = (isset($data['is_test']) && $data['is_test'] == 1) || (isset($data['isTest']) && $data['isTest'] == 1);
if (!$isTestInDb && !$isTester) {
jsonError("Access denied. Not a tester account.");
exit();
}
// فحص الباسورد (في نظامنا، يمكن أن يكون الباسورد هو HMAC أو نص عادي للفاحصين)
// لنفترض أن الفاحص له باسورد عادي أو مشفر بـ bcrypt
if (password_verify($password, $data['password'])) {
unset($data['password']);
// فك تشفير الحقول الحساسة
$data['phone'] = $encryptionHelper->decryptData($data['phone']);
$data['email'] = $encryptionHelper->decryptData($data['email']);
$data['gender'] = $encryptionHelper->decryptData($data['gender']);
$data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
$data['site'] = $encryptionHelper->decryptData($data['site']);
$data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
$data['last_name'] = $encryptionHelper->decryptData($data['last_name']);
if(isset($data['employmentType'])) $data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']);
if(isset($data['maritalStatus'])) $data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
// توليد الـ JWT بصلاحية (tester) لتميزهم عن السائقين الفعليين
$jwtService = new JwtService($redis);
$jwt = $jwtService->generateAccessToken($data['id'], 'tester', $audience, $fingerprint);
echo json_encode([
"status" => "success",
"jwt" => $jwt,
"data" => [$data] // مطابق لنسق التطبيق الذي يتوقع مصفوفة
], JSON_UNESCAPED_UNICODE);
} else {
jsonError("Incorrect password.");
}
} else {
jsonError("User does not exist.");
}
} catch (Exception $e) {
error_log("[Tester Login Error] " . $e->getMessage());
jsonError("Server error occurred.");
} finally {
$stmt = null;
$con = null;
}
exit();
?>
+635
View File
@@ -0,0 +1,635 @@
<?php
/**
* Endpoint: register_driver_and_car.php
* [MODIFIED] Added vehicle_category_id and fuel_type_id support.
* [MODIFIED] Fixed birthdate logic: Append -01-01 BEFORE encryption.
* [MODIFIED] Added Syrian phone number formatting logic.
*/
//register_driver_and_car.php
$allowRegistration = true;
require_once __DIR__ . '/../../../connect.php';
header('Content-Type: application/json; charset=utf-8');
// Rate Limiting: الحماية من التسجيل العشوائي وهجمات الـ Bots
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'register_driver');
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonError("Invalid method.");
exit;
}
$host = getenv('APP_DOMAIN') ?: 'api-syria.siromove.com';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
$PUBLIC_BASE = "$protocol://$host/siro/auth/uploads/documents";
/* ================== 1) Input Fields ================== */
$raw_first_name = null;
$raw_last_name = null;
$required = ["phone", "password", "first_name", "last_name"];
$optional = [
"id","email","gender","license_type","national_number",
"name_arabic","issue_date","expiry_date","license_categories",
"address","licenseIssueDate","status","birthdate","site",
"employmentType","maritalStatus","fullNameMaritial","expirationDate"
];
$carRequired = [
"vin","car_plate","make","model","year","expiration_date",
"color","owner","color_hex","fuel"
];
// حقول اختيارية للسيارة (التصنيف والوقود الرقمي)
// vehicle_category_id, fuel_type_id
$docKeys = [
'id_front',
'id_back',
'driver_license',
'driver_license_back',
'profile_picture',
'criminal_record',
'car_license_front',
'car_license_back'
];
// Read driver fields
$data = [];
foreach ($required as $f) {
$v = filterRequest($f);
if ($v === null || $v === '') {
jsonError("Missing required field: $f");
exit;
}
$data[$f] = $v;
if ($f === 'first_name') $raw_first_name = $v;
if ($f === 'last_name') $raw_last_name = $v;
}
foreach ($optional as $f) {
$v = filterRequest($f);
$data[$f] = ($v === null || $v === '' || $v === 'Not specified') ? null : $v;
}
/* ================== 🟢 START PHONE FORMATTING LOGIC 🟢 ================== */
$country = 'Syria'; // Default
if (!empty($data['phone'])) {
$phone = $data['phone'];
// 1. إزالة المسافات والرموز
$phone = preg_replace('/[ \-\(\)\+]/', '', $phone);
$phone = trim($phone);
if (strpos($phone, '962') === 0 || strpos($phone, '00962') === 0) {
if (strpos($phone, '00962') === 0) $phone = substr($phone, 2);
$country = 'Jordan';
} elseif (strpos($phone, '20') === 0 || strpos($phone, '0020') === 0) {
if (strpos($phone, '0020') === 0) $phone = substr($phone, 2);
$country = 'Egypt';
} else {
// 2. توحيد البادئات الدولية (سوريا)
if (strpos($phone, '00963') === 0) {
$phone = substr($phone, 2);
} elseif (strpos($phone, '0963') === 0) {
$phone = substr($phone, 1);
}
// 3. معالجة الحالات الخاصة بالصفر الزائد بعد الرمز الدولي
if (strpos($phone, '96309') === 0) {
$phone = '9639' . substr($phone, 5);
}
elseif (strpos($phone, '9630') === 0) {
$phone = '9639' . substr($phone, 4);
}
// 4. معالجة الأرقام المحلية
elseif (strpos($phone, '09') === 0) {
$phone = '963' . substr($phone, 1);
}
elseif (strpos($phone, '9') === 0 && strlen($phone) == 9) {
$phone = '963' . $phone;
}
elseif (strpos($phone, '0') === 0 && strlen($phone) == 10) {
$phone = '963' . substr($phone, 1);
}
// 5. التأكد من وجود 9 بعد الرمز الدولي
if (strpos($phone, '963') === 0 && strlen($phone) > 3) {
if (strpos($phone, '9639') !== 0) {
$phone = '9639' . substr($phone, 3);
}
}
}
$data['phone'] = $phone;
}
/* ================== 🔴 END PHONE FORMATTING LOGIC 🔴 ================== */
// ======================================================
// Step 1.5: التحقق الفعلي من ملكية رقم الهاتف قبل إكمال المعالجة (سد الثغرة)
// ======================================================
require_once __DIR__ . '/../../../core/Auth/EncryptionHelper.php';
$tempEncryptionHelper = new EncryptionHelper($redis);
$phoneNumber_encrypted_check = $tempEncryptionHelper->encryptData($data['phone']);
$verifyCheckStmt = $con->prepare(
"SELECT id FROM phone_verification_driver
WHERE phone_number = ? AND verified = 1 AND created_at > DATE_SUB(NOW(), INTERVAL 30 MINUTE)
LIMIT 1"
);
$verifyCheckStmt->execute([$phoneNumber_encrypted_check]);
if ($verifyCheckStmt->rowCount() === 0) {
error_log("[Register_Debug_driver] Error: Phone number not verified via OTP.");
jsonError("Phone number must be verified before registration.");
exit();
}
// ======================================================
// تجهيز تاريخ الميلاد قبل التشفير
if (!empty($data['birthdate'])) {
$data['birthdate'] = trim($data['birthdate']);
$data['birthdate'] = $data['birthdate'] . '-01-01';
} else {
$data['birthdate'] = '1970-01-01';
}
// Read car fields
$car = [];
foreach ($carRequired as $f) {
$v = filterRequest($f);
if ($v === null || $v === '') {
jsonError("Missing required field: $f");
exit;
}
$car[$f] = $v;
}
// Read document links
$docUrls = [];
foreach ($docKeys as $k) {
$u = filterRequest($k);
if (($k === 'driver_license_back' || $k === 'criminal_record') && ($u === null || $u === '')) continue;
if ($u === null || $u === '') {
jsonError("Missing document URL: $k");
exit;
}
if (!filter_var($u, FILTER_VALIDATE_URL)) {
jsonError("Invalid document URL: $k");
exit;
}
$docUrls[$k] = $u;
}
/* ================== حفظ المدخلات الخام قبل AI ================== */
$userInputJson = json_encode([
'driver' => $data,
'car' => $car,
]);
/* ================== AI PROCESSING START ================== */
$apiKey = getenv("GEMINI_API_KEY");
$aiRawText = null;
if ($apiKey) {
$promptBase = '
You are a highly secure AI Assistant specialized in analyzing identification and driver documents.
Country Context: ' . ($country ?? 'Syria') . '
### TASK
We are providing you with multiple images representing a driver\'s documents (National ID, Driver License, Profile Picture, Criminal Record, and Car Registration).
Extract all the required data accurately according to the exact schema provided, and perform a FACE MATCHING analysis.
Since the driver may be from Jordan, Egypt, or Syria, the layout, fields, and distribution of information between the front and back of each card varies widely by country.
Therefore, do NOT assume a specific field is on the front or the back of a card. You must scan all provided document images (e.g., both ID images, both license images) and intelligently locate the requested fields wherever they appear on the cards.
### RULES
1. Convert any Eastern-Arabic digits (٠١٢٣٤٥٦٧٨٩) to Western digits (0-9).
2. Dates must be formatted as ISO `YYYY-MM-DD`.
3. Smart Extraction: Scan all provided document images without restriction. Do not fail the overall request if some optional fields (like governorate or address or issue dates) are missing or unreadable on the card. Simply set those specific fields to `null` in the JSON, but do NOT set the overall status to failure. Overall status should only be \'failure\' if there is a critical security/authenticity issue (e.g., face mismatch, fake/forged documents, or missing primary driver identity).
4. FACE MATCHING (CRITICAL): Compare the face in the "Profile Picture" with the photos on the "National ID" and "Driver License".
5. Ensure the Criminal/Non-Conviction record is valid and matches the driver\'s name.
6. The `national_number` and `vin` (chassis) must contain Latin digits/characters only.
7. Normalize color names (e.g. "أبيض" -> "White") and provide a matching Hex code (e.g. "#FFFFFF").
8. Do NOT add markdown formatting around the output. Return ONLY raw JSON.
### REQUIRED JSON OUTPUT FORMAT
{
"status": "success|failure",
"reason": "If failure, state the reason (e.g., Face mismatch, blurry, invalid record)",
"face_match_confidence": "high|low",
"driver": {
"full_name": "", // Full name in Arabic
"national_number": "", // National ID/National number (Latin digits)
"dob": "YYYY-MM-DD", // Date of birth
"address": "", // Full address
"governorate": "", // Governorate/Site/City
"gender": "Male|Female", // Gender
"id_issue_date": "YYYY-MM-DD", // National ID issue date
"license_issue_date": "YYYY-MM-DD", // Driver license issue date
"license_expiry_date": "YYYY-MM-DD", // Driver license expiry date
"license_number": "", // Driver license number
"license_category": "", // License category (e.g., D1, B, Private, Public)
"blood_type": "", // Blood type (e.g., A+, O-)
"civil_registry": "", // Civil registry/Place of registration
"birth_place": "" // Place of birth
},
"car": {
"car_plate": "", // Full car plate (e.g., 155186 درعا)
"owner": "", // Owner full name
"vin": "", // Chassis/VIN number
"color": "", // Color name
"color_hex": "", // Color hex code (e.g., #FFFFFF)
"car_issue_date": "YYYY-MM-DD", // Car registration issue date
"inspection_date": "YYYY-MM-DD", // Car next inspection date
"make": "", // Car Make (e.g., Hyundai)
"model": "", // Car Model (e.g., H1)
"year": "", // Manufacturing year (e.g., 2019)
"fuel": "" // Fuel type (e.g., Petrol, Diesel, Electric)
}
}';
$contents = [
["role" => "user", "parts" => [["text" => $promptBase]]]
];
// ✅ SSRF Protection: Allowlist for document URLs
$allowedHosts = array_filter([
parse_url($PUBLIC_BASE, PHP_URL_HOST),
getenv('ALLOWED_UPLOAD_HOST'),
]);
$maxFileSize = 10 * 1024 * 1024; // 10MB max per image
foreach ($docUrls as $key => $url) {
$urlHost = parse_url($url, PHP_URL_HOST);
$allowed = false;
foreach ($allowedHosts as $host) {
if ($host && $urlHost === $host) {
$allowed = true;
break;
}
}
if (!$allowed) {
error_log("[SSRF_BLOCKED] Doc URL not in allowlist: $urlHost ($key)");
continue;
}
$ctx = stream_context_create(['http' => [
'timeout' => 10,
'ignore_errors' => true,
]]);
$imgData = @file_get_contents($url, false, $ctx, 0, $maxFileSize);
if ($imgData !== false) {
$base64 = base64_encode($imgData);
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
$mime = ($ext === 'png') ? 'image/png' : 'image/jpeg';
$contents[0]["parts"][] = ["text" => "Image type: " . $key];
$contents[0]["parts"][] = ["inlineData" => ["mimeType" => $mime, "data" => $base64]];
}
}
$apiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=$apiKey";
$payload = ["contents" => $contents];
$ch = curl_init($apiURL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$aiData = json_decode($response, true);
$textRaw = $aiData['candidates'][0]['content']['parts'][0]['text'] ?? '';
$textRaw = trim(preg_replace('/```json|```/', '', $textRaw));
$json = json_decode($textRaw, true);
$aiRawText = ($json !== null && $json !== false) ? $textRaw : null; // فقط إذا كان JSON صحيح
if ($json && isset($json['status']) && strtolower($json['status']) === 'failure') {
jsonError("AI Verification Failed: " . ($json['reason'] ?? 'Unknown reason'));
exit;
}
if ($json && isset($json['driver'])) {
$ex = $json['driver'];
if (!empty($ex['full_name'])) {
$data['name_arabic'] = $ex['full_name'];
$parts = explode(' ', trim($ex['full_name']));
if (count($parts) >= 2) {
$data['first_name'] = $parts[0];
$data['last_name'] = implode(' ', array_slice($parts, 1));
} else {
$data['first_name'] = $ex['full_name'];
$data['last_name'] = '';
}
}
if (!empty($ex['national_number'])) $data['national_number'] = $ex['national_number'];
if (!empty($ex['dob'])) {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $ex['dob'])) {
$data['birthdate'] = $ex['dob'];
} elseif (preg_match('/^\d{4}$/', $ex['dob'])) {
$data['birthdate'] = $ex['dob'] . '-01-01';
}
}
if (!empty($ex['address'])) $data['address'] = $ex['address'];
if (!empty($ex['governorate'])) $data['site'] = $ex['governorate'];
if (!empty($ex['gender'])) $data['gender'] = $ex['gender'];
if (!empty($ex['id_issue_date'])) $data['issue_date'] = $ex['id_issue_date'];
if (!empty($ex['license_issue_date'])) $data['licenseIssueDate'] = $ex['license_issue_date'];
if (!empty($ex['license_expiry_date'])) $data['expiry_date'] = $ex['license_expiry_date'];
if (!empty($ex['license_category'])) $data['license_categories'] = $ex['license_category'];
// Not mapped directly in basic DB schema but extracted: blood_type, civil_registry, birth_place
}
if ($json && isset($json['car'])) {
$ex = $json['car'];
if (!empty($ex['car_plate'])) $car['car_plate'] = $ex['car_plate'];
if (!empty($ex['make'])) $car['make'] = $ex['make'];
if (!empty($ex['model'])) $car['model'] = $ex['model'];
if (!empty($ex['year'])) $car['year'] = $ex['year'];
if (!empty($ex['color'])) $car['color'] = $ex['color'];
if (!empty($ex['color_hex'])) $car['color_hex'] = $ex['color_hex'];
if (!empty($ex['vin'])) $car['vin'] = $ex['vin'];
if (!empty($ex['owner'])) $car['owner'] = $ex['owner'];
if (!empty($ex['fuel'])) $car['fuel'] = $ex['fuel'];
}
}
}
/* ================== AI PROCESSING END ================== */
/* ================== 2) Generate default id/email ================== */
if (empty($data['id'])) {
$data['id'] = 'DRV' . date('YmdHis') . random_int(1000, 9999);
}
if ($data['email'] === null) {
$data['email'] = $data['phone'] . '@intaleqapp.com';
}
/* ================== 3) Hash password (HMAC + password_hash) ================== */
// 🔴 مهم: يجب أن يكون قبل التشفير - يستخدم القيم الخام
$pepper = getenv('SECRET_KEY_HMAC');
$baseParts = [
$data['id'],
$data['phone'],
];
if (!empty($data['national_number'])) {
$baseParts[] = $data['national_number'];
} elseif (!empty($data['birthdate'])) {
$year = substr($data['birthdate'], 0, 4);
if (preg_match('/^\d{4}$/', $year)) {
$baseParts[] = $year;
}
}
$baseString = implode('|', $baseParts);
$rawSecret = hash_hmac('sha256', $baseString, $pepper, true);
$pwdHashed = password_hash($rawSecret, PASSWORD_DEFAULT);
/* ================== 4) Encrypt sensitive fields ================== */
// فهارس البحث تُحسب من القيم الخام قبل التشفير — بعده تصبح القيمة الأصلية
// غير متاحة، وبعد الانتقال إلى GCM لا يمكن استنتاجها من النص المشفّر.
global $blindIndex;
$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $data['phone'] ?? null) : null;
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $data['email'] ?? null) : null;
$nameBidx = $blindIndex ? $blindIndex->index(
'driver.name',
trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''))
) : null;
// مفتاح ربط جداول التحقق — يجب أن يطابق otpPhoneKey() حرفياً
$phoneKey = otpPhoneKey($data['phone'] ?? null);
$toEncryptDriver = [
"phone","email","first_name","last_name","name_arabic","gender",
"national_number","address","site","fullNameMaritial","birthdate"
];
foreach ($toEncryptDriver as $f) {
if (!empty($data[$f])) {
$data[$f] = $encryptionHelper->encryptData($data[$f]);
}
}
// Encrypt car sensitive data
$car['vin'] = $encryptionHelper->encryptData($car['vin']);
$car['car_plate'] = $encryptionHelper->encryptData($car['car_plate']);
$car['owner'] = $encryptionHelper->encryptData($car['owner']);
/* ================== 5) Start transaction ================== */
$con->beginTransaction();
/* ================== 6) Check duplicate ================== */
$dup = $con->prepare(
"SELECT id FROM driver
WHERE phone = :p OR email = :e
OR (:pb IS NOT NULL AND phone_bidx = :pb)
OR (:eb IS NOT NULL AND email_bidx = :eb)"
);
$dup->execute([
':p' => $data['phone'],
':e' => $data['email'],
':pb' => $phoneBidx,
':eb' => $emailBidx,
]);
if ($dup->rowCount() > 0) {
$con->rollBack();
jsonError("Phone or email already registered.");
exit;
}
/* ================== 7) Insert Driver ================== */
$sqlDriver = "
INSERT INTO driver (
id, phone, email, password, gender, license_type, national_number,
name_arabic, issue_date, expiry_date, license_categories,
address, licenseIssueDate, status, birthdate, site,
first_name, last_name, accountBank, bankCode,
employmentType, ai_data, user_input, maritalStatus,
fullNameMaritial, expirationDate, created_at, updated_at,
phone_bidx, email_bidx, name_bidx, phone_key
) VALUES (
:id, :phone, :email, :pwd, :gender, :license_type, :national_number,
:name_arabic, :issue_date, :expiry_date, :license_categories,
:address, :licenseIssueDate, :status, :birthdate, :site,
:first_name, :last_name, :accountBank, :bankCode,
:employmentType, :ai_data, :user_input, :maritalStatus,
:fullNameMaritial, :expirationDate, NOW(), NOW(),
:phone_bidx, :email_bidx, :name_bidx, :phone_key
)
";
$insD = $con->prepare($sqlDriver);
$okD = $insD->execute([
':id' => $data['id'],
':phone' => $data['phone'],
':email' => $data['email'],
':pwd' => $pwdHashed,
':gender' => !empty($data['gender']) ? $data['gender'] : 'Male',
':license_type' => !empty($data['license_type']) ? $data['license_type'] : 'yet',
':national_number' => $data['national_number'],
':name_arabic' => $data['name_arabic'],
':issue_date' => !empty($data['issue_date']) ? $data['issue_date'] : '2020-01-01',
':expiry_date' => !empty($data['expiry_date']) ? $data['expiry_date'] : 'yet',
':license_categories' => !empty($data['license_categories']) ? $data['license_categories'] : 'B',
':address' => $data['address'],
':licenseIssueDate' => !empty($data['licenseIssueDate']) ? $data['licenseIssueDate'] : '2020-01-01',
':status' => 'pending_review',
':birthdate' => $data['birthdate'],
':site' => !empty($data['site']) ? $data['site'] : 'demascus',
':first_name' => $data['first_name'],
':last_name' => $data['last_name'],
':accountBank' => 'yet',
':bankCode' => 'yet',
':employmentType' => !empty($data['employmentType']) ? $data['employmentType'] : 'yet',
':ai_data' => $aiRawText ?: null,
':user_input' => $userInputJson ?: null,
':maritalStatus' => !empty($data['maritalStatus']) ? $data['maritalStatus'] : 'yet',
':fullNameMaritial' => !empty($data['fullNameMaritial']) ? $data['fullNameMaritial'] : 'yet',
':expirationDate' => !empty($data['expirationDate']) ? $data['expirationDate'] : 'yet',
':phone_bidx' => $phoneBidx,
':email_bidx' => $emailBidx,
':name_bidx' => $nameBidx,
':phone_key' => $phoneKey,
]);
if (!$okD) {
$con->rollBack();
jsonError("Failed to insert driver.");
exit;
}
$driverID = $data['id'];
/* ================== 8) Insert Vehicle ================== */
// ✅ استقبال القيم الجديدة (التصنيف والوقود) مع تعيين افتراضي 1
$vCatID = filterRequest("vehicle_category_id");
$vCatID = ($vCatID !== null && $vCatID !== '') ? $vCatID : 1; // 1 = Car
$fTypeID = filterRequest("fuel_type_id");
$fTypeID = ($fTypeID !== null && $fTypeID !== '') ? $fTypeID : 1; // 1 = Petrol
$hasCar = $con->prepare("SELECT 1 FROM CarRegistration WHERE driverID = :d LIMIT 1");
$hasCar->execute([':d' => $driverID]);
$isDefault = $hasCar->rowCount() === 0 ? 1 : 0;
$sqlCar = "
INSERT INTO CarRegistration (
driverID, vin, car_plate, make, model, year, expiration_date,
color, owner, color_hex, fuel,
vehicle_category_id, fuel_type_id,
isDefault, created_at, status
) VALUES (
:driverID, :vin, :car_plate, :make, :model, :year, :expiration_date,
:color, :owner, :color_hex, :fuel,
:vehicle_category_id, :fuel_type_id,
:isDefault, NOW(), 'yet'
)
";
$insC = $con->prepare($sqlCar);
$okC = $insC->execute([
':driverID' => $driverID,
':vin' => $car['vin'],
':car_plate' => $car['car_plate'],
':make' => $car['make'],
':model' => $car['model'],
':year' => $car['year'],
':expiration_date' => $car['expiration_date'],
':color' => $car['color'],
':owner' => $car['owner'],
':color_hex' => $car['color_hex'],
':fuel' => $car['fuel'], // النص القديم (للتوافق)
':vehicle_category_id' => $vCatID, // ✅ العمود الجديد
':fuel_type_id' => $fTypeID, // ✅ العمود الجديد
':isDefault' => $isDefault,
]);
if (!$okC) {
$con->rollBack();
jsonError("Failed to insert car registration.");
exit;
}
$carRegID = $con->lastInsertId();
/* ================== 9) Store document links ================== */
$insDoc = $con->prepare("
INSERT INTO driver_documents (driverID, doc_type, image_name, link, upload_date)
VALUES (:driverID, :doc_type, :image_name, :link, NOW())
");
foreach ($docKeys as $k) {
if (!isset($docUrls[$k])) continue;
$url = $docUrls[$k];
$name = basename(parse_url($url, PHP_URL_PATH) ?? '');
if ($name === '') { $name = $k . '_' . time() . '.jpg'; }
$insDoc->execute([
':driverID' => $driverID,
':doc_type' => $k,
':image_name' => $name,
':link' => $url,
]);
if ($k === 'profile_picture') {
$insProfile = $con->prepare("
INSERT INTO imageProfileCaptain (driverID, image_name, link)
VALUES (:driverID, :image_name, :link)
");
$insProfile->execute([
':driverID' => $driverID,
':image_name' => $name,
':link' => $url,
]);
}
}
/* ================== 10) Commit ================== */
$con->commit();
/* ================== 11) Notification ================== */
try {
$fcmSendUrl = getenv('FCM_ENDPOINT_URL') ?: 'http://nginx/backend/ride/firebase/send_fcm.php';
$driverFullName = $raw_first_name . ' ' . $raw_last_name;
$notificationTitle = 'تسجيل سائق جديد';
$notificationBody = "سائق جديد ($driverFullName) سجل برقم ID: $driverID وهو بانتظار المراجعة والتفعيل.";
$notificationPayload = json_encode([
'target' => 'service',
'title' => $notificationTitle,
'body' => $notificationBody,
'isTopic' => true,
'category' => 'new_driver_registration'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fcmSendUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=UTF-8']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $notificationPayload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_exec($ch);
curl_close($ch);
} catch (Exception $notifyEx) {
error_log("register_driver_and_car NOTIFY ERROR: " . $notifyEx->getMessage());
}
printSuccess([
'status' => 'success',
'driverID' => $driverID,
'carRegID' => $carRegID,
'documents' => $docUrls
]);
} catch (Exception $e) {
if (isset($con) && $con instanceof PDO && $con->inTransaction()) {
$con->rollBack();
}
$msg = $e->getMessage();
error_log("register_driver_and_car ERROR: " . $msg);
jsonError("Server error: $msg", 400);
} catch (PDOException $e) {
if (isset($con) && $con instanceof PDO && $con->inTransaction()) {
$con->rollBack();
}
error_log("register_driver_and_car PDO: " . $e->getMessage());
jsonError("Database error: " . $e->getMessage());
}
?>
+16
View File
@@ -0,0 +1,16 @@
<?php
require_once __DIR__ . '/../../connect.php';
$id = filterRequest("id");
$sql = "DELETE FROM `passengers` WHERE `id` = :id";
$stmt = $con->prepare($sql);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
if ($stmt->rowCount() > 0) {
jsonSuccess(null, "Passenger deleted successfully.");
} else {
jsonError("Failed to delete passenger.");
}
?>
+87
View File
@@ -0,0 +1,87 @@
<?php
// File: send_otp_driver.php (إصدار بدون RaseelPlus)
require_once __DIR__ . '/../../../connect.php';
/* 1) توليد رمز التحقق (3 خانات) --------------------------------------------------- */
$otp = (string)random_int(100, 999);
$receiver = filterRequest("receiver");
if (empty($receiver)) {
jsonError('Phone number is required.');
exit();
}
/* 2) إرسال عبر بوابة الفلاش كول / واتساب ------------------------------ */
$nabehUrl = 'https://otp.intaleqapp.com/api/request-otp.php';
$appKey = getenv('NABEH_OTP_APP_KEY');
$payload = [
'phone' => $receiver,
'device_type' => 'android',
'method' => 'whatsapp',
'code' => $otp
];
$ch = curl_init($nabehUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-App-Key: $appKey"
],
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 5
]);
$res = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("⚠️ [Flash Call OTP Token Driver] Curl Error: $error");
jsonError('Failed to connect to OTP service');
exit;
}
$decoded = json_decode((string)$res, true);
$sentOK = ($httpCode === 200 && ($decoded['success'] ?? false));
if ($sentOK) {
/* 3) تشفير البيانات وحفظها في DB ----------------------------------- */
$receiver_enc = otpPhoneKey($receiver);
$otp_enc = otpPhoneKey($otp); // يجب أن يطابق صيغة المقارنة في verify_otp
$exp = date('Y-m-d H:i:s', strtotime('+5 minutes'));
$now = date('Y-m-d H:i:s');
try {
// حذف رموز قديمة
$con->prepare("DELETE FROM token_verification_driver WHERE phone_number = ?")
->execute([$receiver_enc]);
$stmt = $con->prepare("
INSERT INTO token_verification_driver
(phone_number, token, expiration_time, verified, created_at)
VALUES (?, ?, ?, 0, ?)
");
$stmt->execute([$receiver_enc, $otp_enc, $exp, $now]);
// Also save to Redis for verify_otp compatibility
if ($redis) {
$redis->setex("otp:driver:$receiver", 300, $otp);
}
jsonSuccess(null, 'OTP sent and saved successfully');
} catch (PDOException $e) {
error_log("[send_otp_driver.php] " . $e->getMessage());
jsonError('OTP sent but failed to save to database');
}
} else {
jsonError('Failed to send OTP');
}
?>
+83
View File
@@ -0,0 +1,83 @@
<?php
require_once __DIR__ . '/../../../connect.php';
$phoneNumber = filterRequest("phone_number");
$otp = filterRequest("otp");
if (empty($phoneNumber) || empty($otp)) {
jsonError("Phone number and OTP are required.");
exit();
}
$phoneNumber_encrypted = otpPhoneKey($phoneNumber);
// الرمز يُقارن بالتساوي أيضاً، فيحتاج نفس الصيغة الثابتة
$otp_encrypted = otpPhoneKey($otp);
try {
$stmt = $con->prepare("
SELECT * FROM token_verification_driver
WHERE phone_number = ? AND token = ?
");
$stmt->execute([$phoneNumber_encrypted, $otp_encrypted]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
$expiration_time = strtotime($result['expiration_time']);
if (time() <= $expiration_time) {
$con->prepare("UPDATE token_verification_driver SET verified = 1 WHERE id = ?")
->execute([$result['id']]);
$driverStmt = $con->prepare("SELECT id FROM driver WHERE phone = ?");
$driverStmt->execute([$phoneNumber_encrypted]);
$driver = $driverStmt->fetch(PDO::FETCH_ASSOC);
if ($driver) {
$driverID = $driver['id'];
$newToken = filterRequest("token");
$fingerPrint = filterRequest("fingerPrint");
if ($newToken && $fingerPrint) {
$tokenEncrypted = $encryptionHelper->encryptData($newToken);
$checkTokenStmt = $con->prepare("SELECT id FROM driverToken WHERE captain_id = ?");
$checkTokenStmt->execute([$driverID]);
if ($checkTokenStmt->rowCount() > 0) {
$con->prepare("UPDATE driverToken SET token = ?, fingerPrint = ? WHERE captain_id = ?")
->execute([$tokenEncrypted, $fingerPrint, $driverID]);
} else {
$con->prepare("INSERT INTO driverToken (token, fingerPrint, captain_id, created_at) VALUES (?, ?, ?, NOW())")
->execute([$tokenEncrypted, $fingerPrint, $driverID]);
}
$response = [
"message" => "Driver token verified and updated.",
"isRegistered" => true,
"driverID" => $driverID
];
jsonSuccess($response);
} else {
jsonError("Token or fingerprint missing.");
}
} else {
printSuccess([
"message" => "Phone verified, but driver not found.",
"isRegistered" => false
]);
}
} else {
jsonError("OTP expired. Request a new one.");
}
} else {
jsonError("Invalid OTP.");
}
} catch (PDOException $e) {
error_log("[verify_otp_driver.php] " . $e->getMessage());
jsonError("Database error occurred.");
}
+53
View File
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/../../connect.php';
$id = filterRequest("id");
$columnValues = [];
$params = [':id' => $id];
// الحقول التي تحتاج تشفير
$fieldsToEncrypt = [
"phone", "email", "gender", "birthdate", "site",
"first_name", "last_name", "accountBank", "education",
"employmentType", "maritalStatus"
];
// الحقول غير المشفرة
$plainFields = ["status", "bankCode", "updated_at"];
foreach ($_POST as $key => $value) {
$filtered = filterRequest($key);
if ($key === "password") {
// هاش لكلمة المرور
$hashed = password_hash($filtered, PASSWORD_DEFAULT);
$columnValues[] = "`password` = :password";
$params[':password'] = $hashed;
} elseif (in_array($key, $fieldsToEncrypt)) {
$encrypted = $encryptionHelper->encryptData($filtered);
$columnValues[] = "`$key` = :$key";
$params[":$key"] = $encrypted;
} elseif (in_array($key, $plainFields)) {
$columnValues[] = "`$key` = :$key";
$params[":$key"] = $filtered;
}
}
// بناء جملة التحديث
if (empty($columnValues)) {
jsonError("No data provided to update.");
exit;
}
$setClause = implode(", ", $columnValues);
$sql = "UPDATE `driver` SET $setClause WHERE `id` = :id";
$stmt = $con->prepare($sql);
$stmt->execute($params);
if ($stmt->rowCount() > 0) {
jsonSuccess(null, "Driver data updated successfully");
} else {
jsonError("Failed to update driver data");
}
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/../../connect.php';
// Sanitize and validate input
$driverId = filterRequest("driverId");
// SQL query to check if a gift already exists for the driver (unclaimed)
$checkSql = "SELECT COUNT(*) FROM driver_gifts WHERE driver_id = :driverId -- AND is_claimed = 0";
try {
$checkStmt = $con->prepare($checkSql);
$checkStmt->bindParam(':driverId', $driverId, PDO::PARAM_INT);
$checkStmt->execute();
$giftExists = $checkStmt->fetchColumn();
if ($giftExists > 0) {
jsonError("Gift already exists for this driver");
exit;
}
// Insert a new claimed gift
$sql = "INSERT INTO driver_gifts (driver_id, gift_description, is_claimed)
VALUES (:driverId, 'new account 300 le', 1)";
$stmt = $con->prepare($sql);
$stmt->bindParam(':driverId', $driverId, PDO::PARAM_INT);
$stmt->execute();
if ($stmt->rowCount() > 0) {
jsonSuccess(null, "Gift data saved successfully");
} else {
jsonError("Failed to save gift data");
}
} catch (PDOException $e) {
error_log("Database Error: " . $e->getMessage());
jsonError("An error occurred while saving the data");
}
?>
@@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/../../connect.php';
// استقبال معرف السائق
$id = filterRequest("id");
// استقبال بيانات شام كاش من التطبيق
$accountBank = filterRequest("accountBank"); // الاسم (مثال: siro)
$bankCode = filterRequest("bankCode"); // الكود الطويل (مثال: 80f23afe...)
// التحقق من وصول البيانات المطلوبة
if ($id && $accountBank && $bankCode) {
try {
// 1. تشفير اسم الحساب (حسب القواعد في السكربت السابق accountBank مشفر)
$encryptedAccountBank = $encryptionHelper->encryptData($accountBank);
// 2. كود المحفظة يبقى كما هو (حسب القواعد bankCode غير مشفر)
$plainBankCode = $encryptionHelper->encryptData($bankCode);
// 3. جملة التحديث
$stmt = $con->prepare("UPDATE `driver` SET `accountBank` = ?, `bankCode` = ? WHERE `id` = ?");
$stmt->execute(array($encryptedAccountBank, $plainBankCode, $id));
// التحقق من نجاح العملية
// rowCount > 0 يعني تم التحديث، أحياناً يعطي 0 إذا كانت البيانات هي نفسها لم تتغير
// لذا نرسل نجاح في كلتا الحالتين طالما لم يحدث Error
jsonSuccess(null, "ShamCash info updated successfully");
} catch (PDOException $e) {
// في حال وجود خطأ في قاعدة البيانات
error_log("[updateShamCashDriver] " . $e->getMessage());
jsonError("Database Error");
}
} else {
jsonError("Missing required fields: id, accountBank, or bankCode");
}
?>
+157
View File
@@ -0,0 +1,157 @@
<?php
// File: upload_serial_document.php
// يرفع صورة وثيقة إلى مسار خاص (خارج الويب العام) ويُرجع Signed URL مؤقّت.
// بيئتك
require_once __DIR__ . '/../../connect.php'; // يجب أن يوفّر: $con (اختياري) + printSuccess/printFailure + filterRequest
// --------- إعدادات ---------
const MAX_FILE_MB = 5;
const ALLOWED_MIMES = ['image/jpeg','image/png','image/webp']; // فقط صور
const UPLOAD_ROOT = __DIR__ . "/../../private_uploads"; // مجلد خاص (غير عام)
$SIGN_SECRET = getenv('SECRET_KEY_HMAC'); // غيّرها واقرأها من .env
$host = getenv('APP_DOMAIN');
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
define('PUBLIC_BASE', "$protocol://$host/siro");
const SIGNED_TTL_SEC = 172800; // 2 days = 60*60*24
// أنشئ مجلد الرفع إن لم يكن موجودًا
if (!is_dir(UPLOAD_ROOT)) { @mkdir(UPLOAD_ROOT, 0700, true); }
// Log entry
uploadLog("🚀 [uploadSyrianDocs.php] Document upload script started.");
// (اختياري) هيدرز أمان
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$hmacHeader = $_SERVER['HTTP_X_HMAC_AUTH'] ?? '';
// TODO: تحقّق حسب منطقك إن أردت فرض المصادقة هنا.
// --------- حقول مطلوبة من Flutter عبر filterRequest ---------
$driverId = filterRequest('driver_id');
$docType = filterRequest('doc_type');
$purpose = filterRequest('purpose'); // اختياري
uploadLog("📥 Request params: driver_id=$driverId, doc_type=$docType");
if (empty($driverId) || empty($docType)) {
uploadLog("❌ Missing driver_id or doc_type params.", 'ERROR');
jsonError("driver_id and doc_type are required.");
exit;
}
// اسمح فقط بقيم محددة للوثائق
$allowedDocTypes = [
'driver_license_front',
'driver_license_back',
'car_license_front',
'car_license_back',
];
if (!in_array($docType, $allowedDocTypes, true)) {
uploadLog("❌ Invalid doc_type value: $docType", 'ERROR');
jsonError("Invalid doc_type.");
exit;
}
// --------- التحقق من الملف ---------
if (isset($_FILES['file'])) {
uploadLog('$_FILES[\'file\'] metadata', 'INFO', [
'name' => $_FILES['file']['name'] ?? 'unknown',
'type' => $_FILES['file']['type'] ?? 'unknown',
'size' => $_FILES['file']['size'] ?? 0,
'upload_error_code' => $_FILES['file']['error'] ?? UPLOAD_ERR_OK
]);
} else {
uploadLog("No 'file' payload was sent in the request.", 'WARNING');
}
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$err = $_FILES['file']['error'] ?? 'missing_file';
uploadLog("❌ File upload validation failed. Code: $err", 'ERROR');
jsonError("No file uploaded or upload error.");
exit;
}
$tmpPath = $_FILES['file']['tmp_name'];
$origName = $_FILES['file']['name'] ?? 'upload.bin';
$size = filesize($tmpPath);
if ($size === false || $size <= 0) {
jsonError("Invalid file size."); exit;
}
if ($size > MAX_FILE_MB * 1024 * 1024) {
jsonError("File too large. Max " . MAX_FILE_MB . " MB."); exit;
}
// MIME دقيق
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpPath) ?: 'application/octet-stream';
if (!in_array($mime, ALLOWED_MIMES, true)) {
jsonError("Unsupported file type: $mime"); exit;
}
// لاحقة الامتداد
$extMap = [
'image/jpeg' => '.jpg',
'image/png' => '.png',
'image/webp' => '.webp',
];
$ext = $extMap[$mime];
// --------- توليد مسار حتمي بدون تاريخ ---------
// تنظيف driver_id لاسم ملف آمن
$driverIdSafe = preg_replace('/[^A-Za-z0-9_\-]/', '_', $driverId);
// شجرة مجلدات ثابتة من hash(driver_id) لتوزيع الملفات
$h = hash('sha1', $driverIdSafe);
$subdir = substr($h, 0, 2) . '/' . substr($h, 2, 2);
$destDir = UPLOAD_ROOT . '/' . $subdir;
if (!is_dir($destDir)) { @mkdir($destDir, 0700, true); }
// الاسم النهائي بدون تاريخ
$serverName = "{$driverIdSafe}__{$docType}{$ext}";
$destPath = $destDir . '/' . $serverName;
// استبدال أي نسخة قديمة عن قصد (overwrite) - مع حماية ضد path traversal
$resolvedDest = realpath($destPath) ?: $destPath;
$resolvedRoot = realpath(UPLOAD_ROOT) ?: UPLOAD_ROOT;
if (is_file($destPath) && str_starts_with($resolvedDest, $resolvedRoot)) {
@unlink($destPath);
}
// نقل الملف
if (!move_uploaded_file($tmpPath, $destPath)) {
jsonError("Failed to save the uploaded file.");
exit;
}
@chmod($destPath, 0600);
// --------- Signed URL ---------
// سنضمّن driver_id و doc_type و ext في الرابط والتوقيع.
// ext بدون النقطة
$extShort = ltrim($ext, '.');
$expires = time() + SIGNED_TTL_SEC;
// الرسالة الموقّعة: driver_id:doc_type:ext:expires
$message = $driverIdSafe . ':' . $docType . ':' . $extShort . ':' . $expires;
$signature = hash_hmac('sha256', $message, SIGN_SECRET);
// رابط القراءة عبر البوابة الآمنة فقط
// ملاحظة: لا نُرجع المسار الحقيقي، فقط معطيات موقّعة
$fileUrl = PUBLIC_BASE . "/secure_image.php"
. "?driver_id={$driverIdSafe}"
. "&doc_type={$docType}"
. "&ext={$extShort}"
. "&expires={$expires}"
. "&signature={$signature}";
// --------- استجابة ---------
uploadLog("✅ Document upload succeeded. URL: $fileUrl");
printSuccess([
"status" => "success",
"success_file" => true,
"file_url" => $fileUrl,
"file_name" => $serverName, // الاسم الفعلي المحفوظ
"driver_id" => $driverIdSafe,
"doc_type" => $docType,
"mime_type" => $mime,
"size_bytes" => $size,
"expires_at" => date('c', $expires)
]);
+93
View File
@@ -0,0 +1,93 @@
<?php
// ============================================================
// auth/syria/uploadImage.php
// رفع صور وثائق السائق (هوية، رخصة، صورة شخصية، ...)
// يخزّنها في مجلدات حسب الدولة: auth/uploads/{country}/
// ============================================================
require_once __DIR__ . '/../../connect.php';
uploadLog("🚀 [uploadImage.php] Document upload started.");
// --------- قراءة الحقول ---------
$driverID = filterRequest('driverID');
$imageType = filterRequest('imageType');
$country = filterRequest('country');
if (empty($imageType)) {
jsonError('imageType is required.');
}
// --------- تحديد الدولة ---------
$country = strtolower(trim($country ?: ''));
if (empty($country)) {
if (!empty($driverID)) {
try {
$stmt = $con->prepare("SELECT country FROM drivers WHERE id = ?");
$stmt->execute([$driverID]);
$country = strtolower(trim((string)$stmt->fetchColumn()));
} catch (Exception $e) {
$country = '';
}
}
if (empty($country)) {
$country = 'jordan';
}
}
if (!in_array($country, ['syria', 'jordan', 'egypt'])) {
$country = 'jordan';
}
// --------- بادئة اسم الملف ---------
$prefix = !empty($driverID) ? $driverID : 'unregistered';
uploadLog("📥 Params: driverID=" . ($driverID ?: 'null') . ", imageType=$imageType, country=$country");
// --------- رفع الملف ---------
$targetDir = __DIR__ . "/../../auth/uploads/{$country}/";
$result = uploadImageSecure('image', $targetDir, $prefix . '_' . $imageType);
if (!$result['success']) {
uploadLog("❌ Upload failed: {$result['error']}", 'ERROR', [
'driverID' => $driverID,
'imageType' => $imageType,
'country' => $country,
]);
jsonError($result['error']);
}
// --------- إزالة العشوائية من اسم الملف ---------
// الاسم يكون فقط: {driverID}_{imageType}.jpg (بدون random hex)
$ext = pathinfo($result['filename'], PATHINFO_EXTENSION);
$simpleName = $prefix . '_' . $imageType . '.' . $ext;
$simplePath = rtrim($targetDir, '/') . '/' . $simpleName;
if (file_exists($simplePath)) {
unlink($simplePath); // overwrite
}
rename($result['path'], $simplePath);
$result['filename'] = $simpleName;
$result['path'] = $simplePath;
// --------- بناء الرابط العام ---------
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? (getenv('APP_DOMAIN') ?: 'api.siromove.com');
$basePath = rtrim(dirname(dirname(dirname($_SERVER['SCRIPT_NAME']))), '/');
$url = "$protocol://$host{$basePath}/auth/uploads/{$country}/{$result['filename']}";
uploadLog("✅ Uploaded: {$result['path']} -> $url", 'INFO', [
'driverID' => ($driverID ?: 'null'),
'imageType' => $imageType,
'country' => $country,
]);
jsonSuccess([
'url' => $url,
'file_link' => $url,
'filename' => $result['filename'],
'driverID' => ($driverID ?: ''),
'imageType' => $imageType,
'country' => $country,
]);