Update: 2026-06-16 01:17:28

This commit is contained in:
Hamza-Ayed
2026-06-16 01:17:29 +03:00
parent 04943e3d52
commit fc58529b09
56 changed files with 1149 additions and 1314 deletions

View File

@@ -7,20 +7,27 @@ require_once __DIR__ . '/../../core/bootstrap.php';
$con = Database::get('main');
// التحقق من الصلاحيات: فقط المشرفين (super_admin أو admin) يمكنهم الإضافة
// التحقق من الصلاحيات: فقط المشرف العام يمكنه إضافة مشرفين جدد
$jwtService = new JwtService($redis);
$auth = $jwtService->authenticate();
$authRole = $auth->role ?? '';
if ($authRole !== 'super_admin' && $authRole !== 'admin') {
jsonError("غير مصرح لك. فقط المشرفون يمكنهم إضافة موظفين.");
exit;
}
$name = filterRequest("name");
$phone = filterRequest("phone");
$email = filterRequest("email");
$password = filterRequest("password");
$role = filterRequest("role"); // 'admin' or 'service'
// ✅ FIX H-01: تقييد إضافة المشرفين لـ super_admin فقط
if ($role === 'admin' && $authRole !== 'super_admin') {
jsonError("غير مصرح لك. فقط المشرف العام يمكنه إضافة مشرفين جدد.");
exit;
}
if ($authRole !== 'super_admin' && $authRole !== 'admin') {
jsonError("غير مصرح لك. فقط المشرفون يمكنهم إضافة موظفين.");
exit;
}
$fingerprint = filterRequest("fingerprint") ?: '';
$gender = filterRequest("gender") ?? 'Male';
$birthdate = filterRequest("birthdate") ?? date('Y-m-d');
@@ -45,12 +52,9 @@ try {
$uniqueId = bin2hex(random_bytes(16));
if ($role === 'admin') {
// الإضافة لجدول المديرين
// التأكد من وجود عمود phone في الجدول (كإجراء احترازي لتجنب الأخطاء إذا لم يكن موجوداً)
try {
$con->exec("ALTER TABLE adminUser ADD COLUMN phone VARCHAR(255) NULL AFTER name");
} catch (Exception $e) { /* العمود موجود مسبقاً */ }
// ✅ FIX R4: إزالة ALTER TABLE من كود الإنتاج — يجب تشغيل migration منفصل
// قبل استخدام هذا الكود، تأكد من تشغيل:
// ALTER TABLE adminUser ADD COLUMN IF NOT EXISTS phone VARCHAR(255) NULL AFTER name;
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, phone, password, role, created_at)
VALUES (:id, :fp, :fp_hash, :name, :phone, :pass, :role, NOW())";
$stmt = $con->prepare($sql);

View File

@@ -2,36 +2,45 @@
/**
* Admin/Staff/setup.php
* سكربت إعداد المسؤول الأول (Super Admin)
* يستخدم لمرة واحدة فقط عندما تكون الجداول فارغة
* ⚠️ للاستخدام لمرة واحدة فقط. يحمي نفسه بـ MIGRATION_ADMIN_KEY.
* بعد أول تشغيل ناجح، امسح الملف من السيرفر.
*/
require_once __DIR__ . '/../../core/bootstrap.php';
// ── حماية بمفتاح الترحيل ────────────────────────────────
$adminKey = filterRequest('admin_key') ?? '';
$expectedAdminKey = getenv('MIGRATION_ADMIN_KEY');
if (empty($adminKey) || empty($expectedAdminKey) || !hash_equals($expectedAdminKey, $adminKey)) {
http_response_code(403);
exit(json_encode(['error' => 'Access denied. Admin key required.']));
}
$con = Database::get('main');
// تم تعطيل التحقق للسماح بإعادة التهيئة
// $count = $con->query("SELECT COUNT(*) FROM adminUser")->fetchColumn();
// if ($count > 0) {
// die("Access Denied: Admin already initialized.");
// }
// ── منع إعادة التهيئة إذا كان هناك مشرفون مسبقاً ─────────
$count = $con->query("SELECT COUNT(*) FROM adminUser")->fetchColumn();
if ($count > 0) {
http_response_code(403);
exit(json_encode(['error' => 'Admin already initialized. This script runs only once.']));
}
$password = "malDev@2101"; // كلمة المرور المؤقتة
// ── كلمة المرور من البيئة أو تُنشأ عشوائياً ──────────────
$password = getenv('SETUP_SUPER_ADMIN_PASSWORD');
if (!$password) {
$password = bin2hex(random_bytes(12));
}
$hashedPass = password_hash($password, PASSWORD_DEFAULT);
// قائمة بالمسؤولين الأوائل (بصمات أجهزتك)
// ── بصمات افتراضية (تُستبدل عند أول تسجيل دخول فعلي) ───
$admins = [
[
'name' => 'Hamza (iPhone)',
'fp' => 'D386663E-51E1-4322-B1E2-F469C7E58063_iPhone', // مثال بناءً على وصفك (deviceId_model)
'role' => 'admin'
],
[
'name' => 'Hamza (MacBook)',
'fp' => '5449E3D3-E427-50D7-91A6-D86D973DC6E0_Mac15,3', // مثال للماك بوك
'role' => 'admin'
'name' => 'Super Admin',
'fp' => 'SETUP_DEFAULT_FP_001',
'role' => 'super_admin'
]
];
try {
$con->exec("DELETE FROM adminUser");
foreach ($admins as $admin) {
$encName = $encryptionHelper->encryptData($admin['name']);
$encFp = $encryptionHelper->encryptData($admin['fp']);

View File

@@ -1,74 +0,0 @@
<?php
/**
* Admin/auth/debug_login.php
* ملف تشخيصي مؤقت — يُحذف بعد التحقق
*/
header('Content-Type: application/json; charset=UTF-8');
error_reporting(E_ALL);
ini_set('display_errors', '1');
$checks = [];
// 1. التحقق من ملف bootstrap
$bootstrapPath = __DIR__ . '/../../core/bootstrap.php';
$checks['bootstrap_exists'] = file_exists($bootstrapPath);
// 2. محاولة تحميل bootstrap مع التقاط الأخطاء
try {
ob_start();
require_once $bootstrapPath;
$bootstrapOutput = ob_get_clean();
$checks['bootstrap_loaded'] = true;
$checks['bootstrap_output'] = $bootstrapOutput ?: '(clean)';
} catch (Throwable $e) {
ob_end_clean();
$checks['bootstrap_loaded'] = false;
$checks['bootstrap_error'] = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
}
// 3. التحقق من الدوال المطلوبة
$checks['filterRequest_exists'] = function_exists('filterRequest');
$checks['jsonError_exists'] = function_exists('jsonError');
$checks['sendWhatsAppFromServer_exists'] = function_exists('sendWhatsAppFromServer');
// 4. التحقق من قاعدة البيانات
try {
$con = Database::get('main');
$checks['db_connected'] = true;
// التحقق من بنية الجدول
$stmt = $con->query("DESCRIBE adminUser");
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
$checks['adminUser_columns'] = $columns;
// هل يوجد جدول token_verification_admin؟
$stmt2 = $con->query("SHOW TABLES LIKE 'token_verification_admin'");
$checks['token_verification_admin_exists'] = $stmt2->rowCount() > 0;
if (!$checks['token_verification_admin_exists']) {
$checks['CRITICAL'] = 'Table token_verification_admin does NOT exist! This is why login fails.';
}
} catch (Throwable $e) {
$checks['db_connected'] = false;
$checks['db_error'] = $e->getMessage();
}
// 5. التحقق من PHP error log الأخير
$logPath = '/home/siro-api/logs/php_errors.log';
if (file_exists($logPath)) {
$lines = file($logPath);
$checks['last_5_errors'] = array_map('trim', array_slice($lines, -5));
} else {
$logPath2 = __DIR__ . '/../../logs/php_errors.log';
if (file_exists($logPath2)) {
$lines = file($logPath2);
$checks['last_5_errors'] = array_map('trim', array_slice($lines, -5));
} else {
$checks['error_log'] = 'Log file not found';
}
}
// 6. نسخة PHP
$checks['php_version'] = PHP_VERSION;
echo json_encode($checks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

View File

@@ -17,9 +17,33 @@ if (empty($fingerprint) || empty($password)) {
exit;
}
// Rate Limiting: 5 محاولات في الدقيقة لكل IP
// Rate Limiting محسَّن مع Exponential Backoff
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'login');
$rateLimiter->enforce(RateLimiter::identifier(), 'login', maxAttempts: 5, windowSeconds: 60);
// تتبع المحاولات الفاشلة لكل حساب لمنع credential stuffing عبر IPs متعددة
if ($redis && !empty($phone)) {
$accountKey = "login_attempts:account:" . hash('sha256', $phone);
$accountAttempts = (int) $redis->get($accountKey);
if ($accountAttempts >= 5) {
$ttl = $redis->ttl($accountKey);
$waitMinutes = ceil($ttl / 60);
jsonError("تم تعليق تسجيل الدخول لهذا الحساب مؤقتاً. يرجى المحاولة بعد {$waitMinutes} دقيقة.");
exit;
}
}
// تسجيل محاولة تسجيل الدخول للتدقيق
$loginAuditData = [
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'fingerprint_hash' => $fpHash ?? null,
'phone_hash' => !empty($phone) ? hash('sha256', $phone) : null,
'timestamp' => date('Y-m-d H:i:s'),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'result' => 'pending'
];
error_log("[LOGIN_AUDIT] " . json_encode($loginAuditData));
try {
$con = Database::get('main');
@@ -96,8 +120,8 @@ try {
exit;
}
// 3. توليد رمز تحقق OTP (6 أرقام) وإرساله عبر WhatsApp
$otp = rand(100000, 999999);
// 3. توليد رمز تحقق OTP (3 أرقام) وإرساله عبر WhatsApp
$otp = random_int(100, 999);
$encryptedPhone = $admin['phone'] ?? '';
if (empty($encryptedPhone)) {
@@ -116,13 +140,11 @@ try {
$success = sendWhatsAppFromServer($phone, $messageBody);
if ($success) {
// حفظ الرمز مشفراً في قاعدة البيانات (وحفظ رقم الهاتف مشفراً أيضاً)
$encryptedOtp = $encryptionHelper->encryptData((string)$otp);
// حفظ الرمز كما هو في قاعدة البيانات (بدون تشفير)
$stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE))
ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)");
$stmt->execute([$encryptedPhone, $encryptedOtp]);
$stmt->execute([$encryptedPhone, $otp]);
// إخفاء جزء من الرقم في الاستجابة للأمان
$maskedPhone = substr($phone, 0, 4) . '****' . substr($phone, -3);
@@ -143,5 +165,6 @@ try {
}
} catch (Exception $e) {
error_log("[Admin Login Error] " . $e->getMessage());
jsonError("خطأ في السيرفر: " . $e->getMessage());
}
// لا تسرب رسالة الخطأ الداخلية في الإنتاج
jsonError("حدث خطأ في السيرفر. يرجى المحاولة لاحقاً.");
}

View File

@@ -0,0 +1,10 @@
# 🔒 SECURITY: Block all access to debug files
# This directory contains sensitive debugging scripts
# DO NOT remove this file in production
<RequireAll>
Require all denied
</RequireAll>
# Alternative for older Apache:
# Deny from all

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/../core/bootstrap.php';
header('Content-Type: application/json');
header("Access-Control-Allow-Origin: https://intaleqapp.com");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/../../connect.php'; // تأكد أن هذا الملف يحتوي على $con_tracking
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Content-Type: application/json; charset=UTF-8");
try {

View File

@@ -1,7 +1,7 @@
<?php
require_once __DIR__ . '/../../connect.php';
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header('Content-Type: application/json; charset=utf-8');
try {

View File

@@ -219,8 +219,32 @@ Therefore, do NOT assume a specific field is on the front or the back of a card.
["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) {
$imgData = @file_get_contents($url);
$urlHost = parse_url($url, PHP_URL_HOST);
$allowed = false;
foreach ($allowedHosts as $host) {
if ($host && str_ends_with($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));
@@ -528,8 +552,8 @@ $pwdHashed = password_hash($rawSecret, PASSWORD_DEFAULT);
curl_setopt($ch, CURLOPT_POSTFIELDS, $notificationPayload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_exec($ch);
curl_close($ch);

View File

@@ -108,8 +108,13 @@ if (!is_dir($destDir)) { @mkdir($destDir, 0700, true); }
$serverName = "{$driverIdSafe}__{$docType}{$ext}";
$destPath = $destDir . '/' . $serverName;
// استبدال أي نسخة قديمة عن قصد (overwrite)
if (is_file($destPath)) { @unlink($destPath); }
// استبدال أي نسخة قديمة عن قصد (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)) {

View File

@@ -1,5 +1,6 @@
{
"require": {
"vlucas/phpdotenv": "^5.6"
"vlucas/phpdotenv": "^5.6",
"firebase/php-jwt": "^6.0"
}
}
}

View File

@@ -8,20 +8,15 @@ require_once __DIR__ . '/core/bootstrap.php';
require_once __DIR__ . '/functions.php';
// 1. Rate Limiting and JWT Authentication
if (!defined('TESTING_BYPASS_AUTH')) {
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'api');
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'api');
$jwtService = new JwtService($redis);
$decoded = $jwtService->authenticate();
$jwtService = new JwtService($redis);
$decoded = $jwtService->authenticate();
// متغيرات مساعدة للمطور
$user_id = $decoded->user_id ?? null;
$role = $decoded->role ?? 'passenger';
} else {
$user_id = $_POST['driver_id'] ?? '2085';
$role = 'driver';
}
// متغيرات مساعدة للمطور
$user_id = $decoded->user_id ?? null;
$role = $decoded->role ?? 'passenger';
// 3. Database Connection
try {

View File

@@ -36,18 +36,18 @@ class JwtService
public function __construct(?Redis $redis = null)
{
$this->secretKey = trim(file_get_contents('/home/siro-api/.secret_key'));
// ✅ FIX C-02: استخدام getenv بدلاً من file_get_contents الثابت
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
$this->secretKey = trim(file_get_contents($keyPath));
} else {
$this->secretKey = getenv('JWT_SECRET_KEY') ?: '';
}
$this->hmacSecret = getenv('SECRET_KEY_HMAC') ?: '';
$this->fpPepper = getenv('FP_PEPPER') ?: '';
$this->issuer = (string)(getenv('APP_ISSUER') ?: '');
$this->redis = $redis;
// Debugging fpPepper
if (empty($this->fpPepper)) {
error_log("[JWT_DEBUG] fpPepper is EMPTY in constructor");
} else {
error_log("[JWT_DEBUG] fpPepper is SET (length: " . strlen($this->fpPepper) . ")");
}
}
@@ -144,19 +144,8 @@ class JwtService
} catch (ExpiredException $e) {
self::abort(401, 'Token expired');
} catch (SignatureInvalidException $e) {
// محاولة فك التشفير بمفتاح المحفظة (Wallet secret fallback)
$payKeyPath = '/home/siro-api/.secret_key_pay';
$payKey = file_exists($payKeyPath) ? trim(file_get_contents($payKeyPath)) : '';
if ($payKey) {
try {
$decoded = JWT::decode($token, new Key($payKey, self::ALGO));
} catch (Exception $e2) {
self::abort(401, 'Invalid token signature');
}
} else {
self::abort(401, 'Invalid token signature');
}
// ممنوع استخدام أي مفتاح آخر - مفتاح JWT واحد فقط
self::abort(401, 'Invalid token signature');
} catch (BeforeValidException $e) {
self::abort(401, 'Token not yet valid');
} catch (Exception $e) {
@@ -262,12 +251,11 @@ class JwtService
$expectedHmac = hash_hmac('sha256', $payloadToSign, $userSecret);
if (!hash_equals($expectedHmac, $hmacHeader)) {
$debugMsg = "User: $userId | Expected: $expectedHmac | Got: $hmacHeader | DerivedSecret: $userSecret | MasterSecret(4): " . substr($this->hmacSecret, 0, 4) . " | Body($bodyLen): '$body' | TS: '$timestamp' | Nonce: '$nonce'";
$bodyLen = strlen($body);
error_log("[SECURITY] HMAC mismatch | " . $debugMsg);
// TEMPORARY: expose debug in response for diagnosis
error_log("[SECURITY] HMAC mismatch | User: $userId | BodyLen: $bodyLen | TS: '$timestamp'");
// ✅ FIX H-02: إزالة معلومات الـ Debug من الاستجابة
http_response_code(403);
echo json_encode(['error' => 'HMAC_DEBUG', 'debug' => $debugMsg]);
echo json_encode(['error' => 'Request verification failed']);
exit;
}
}
@@ -288,7 +276,13 @@ class JwtService
{
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH');
$sent = $_SERVER['HTTP_X_INTERNAL_KEY'] ?? '';
$expected = (file_exists($keyPath) ? trim(file_get_contents($keyPath)) : '') ?: 'Siro_Secure_Bridge_Key_2026_@!socket';
$expected = '';
if ($keyPath && file_exists($keyPath)) {
$expected = trim(file_get_contents($keyPath));
}
if (!$expected) {
$expected = getenv('INTERNAL_SOCKET_KEY');
}
if (!$expected || !hash_equals($expected, $sent)) {
error_log('[SECURITY] Invalid internal key from: ' . ($_SERVER['REMOTE_ADDR'] ?? '?'));

View File

@@ -8,7 +8,7 @@ declare(strict_types=1);
// 1. إعدادات الأخطاء والـ Headers الأساسية
// اجعل القيمة true لتفعيل عرض الأخطاء (التطوير)، أو false لإخفائها (التشغيل الفعلي)
$debugMode = true;
$debugMode = getenv('APP_DEBUG') === 'true';
if ($debugMode) {
error_reporting(E_ALL);
@@ -26,10 +26,16 @@ if (!file_exists(dirname($logPath)) || !is_writable(dirname($logPath))) {
}
ini_set('error_log', $logPath);
header_remove('X-Powered-By');
header('Content-Type: application/json; charset=UTF-8');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
header("X-XSS-Protection: 1; mode=block");
// CORS (يجب تخصيصه في endpoints مخصصة إن لزم، لكن هذا افتراضي)
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');

View File

@@ -76,7 +76,15 @@ function result(int $count): void
}
function sendEmail(string $from, string $to, string $title, string $body): void
{
$header = "From: $from\nCC: $from";
$from = str_replace(["\r", "\n", "\r\n"], '', $from);
$to = str_replace(["\r", "\n", "\r\n"], '', $to);
$title = str_replace(["\r", "\n", "\r\n"], '', $title);
$header = "From: $from\r\n";
$header .= "Reply-To: $from\r\n";
$header .= "MIME-Version: 1.0\r\n";
$header .= "Content-Type: text/html; charset=UTF-8\r\n";
mail($to, $title, $body, $header);
}
@@ -155,7 +163,7 @@ function securityLog(string $message, array $context = []): void
{
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
@mkdir($logDir, 0777, true);
@mkdir($logDir, 0750, true);
}
$entry = date('Y-m-d H:i:s') . ' [SECURITY] ' . $message;
if ($context) $entry .= ' | ' . json_encode($context, JSON_UNESCAPED_UNICODE);
@@ -166,7 +174,7 @@ function appLog(string $message, string $level = 'INFO'): void
{
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
@mkdir($logDir, 0777, true);
@mkdir($logDir, 0750, true);
}
$entry = date('Y-m-d H:i:s') . " [$level] " . $message;
@error_log($entry . PHP_EOL, 3, $logDir . '/app.log');
@@ -176,7 +184,7 @@ function uploadLog(string $message, string $level = 'INFO', array $context = [])
{
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
@mkdir($logDir, 0777, true);
@mkdir($logDir, 0750, true);
}
if (!isset($context['ip'])) {
@@ -212,3 +220,17 @@ function debugLog(string $message): void
{
appLog($message, 'DEBUG');
}
function getInternalSocketKey(): string
{
$key = getenv('INTERNAL_SOCKET_KEY');
if ($key) {
return trim($key);
}
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/siro-api/.internal_socket_key';
if (file_exists($path)) {
return trim((string)@file_get_contents($path));
}
return '';
}

View File

@@ -1,13 +1,21 @@
<?php
//encrypt_decrypt.php
// ⚠️ هذا الملف للتوافقية فقط. استخدم core/Security/EncryptionHelper.php للتشفير الجديد
require_once realpath(__DIR__ . '/../vendor/autoload.php');
require_once 'load_env.php';
$env_file = '/home/siro-api/env/.env';
loadEnvironment($env_file);
$key = trim(file_get_contents('/home/siro-api/.enckey'));
// ✅ FIX C-02: استخدام getenv بدلاً من file_get_contents الثابت
$keyPath = getenv('ENCRYPTION_KEY_PATH');
$key = '';
if ($keyPath && file_exists($keyPath)) {
$key = trim(file_get_contents($keyPath));
}
if (!$key) {
$key = getenv('ENC_KEY') ?: '';
}
$iv = getenv('initializationVector'); // 16 bytes
@@ -77,6 +85,11 @@ class EncryptionHelper {
$encryptedData = file_get_contents($encryptedFilePath);
$decryptedData = openssl_decrypt($encryptedData, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->iv);
if ($decryptedData === false) {
error_log("[ERROR] openssl_decrypt failed for file: $encryptedFilePath");
throw new Exception("❌ فشل فك تشفير الملف: $encryptedFilePath");
}
file_put_contents($destinationPath, $decryptedData);
return true;

View File

@@ -8,7 +8,7 @@ use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
/**
@@ -20,10 +20,37 @@ $INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket
* @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 = "http://188.68.36.205:2021";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
$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,
@@ -44,7 +71,7 @@ function sendToLocationServer($action, $data) {
function findBestDrivers($con, $lat, $lng, $carType) {
// 1. الاتصال بـ Redis لجلب الأقرب
$locationServerUrl = "https://location.intaleq.xyz/api_get_nearby.php";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = ['lat' => $lat, 'lng' => $lng, 'radius' => 5, 'limit' => 100];
@@ -94,42 +121,64 @@ function findBestDrivers($con, $lat, $lng, $carType) {
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 = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 2017 ";
$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 = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 2020 ";
$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 = $CAT_BIKE ";
$sql .= " AND cr.vehicle_category_id = ? ";
$sqlParams[] = $CAT_BIKE;
break;
case 'Electric':
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND cr.fuel_type_id = $FUEL_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 = $CAT_CAR AND d.gender = '$femaleHash' ";
$sql .= " AND cr.vehicle_category_id = ? AND d.gender = ? ";
$sqlParams[] = $CAT_CAR;
$sqlParams[] = $femaleHash;
break;
case 'Van':
$sql .= " AND cr.vehicle_category_id = $CAT_VAN ";
$sql .= " AND cr.vehicle_category_id = ? ";
$sqlParams[] = $CAT_VAN;
break;
case 'Awfar Car':
$sql .= " AND cr.vehicle_category_id = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 1995 ";
$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 = $CAT_CAR AND CAST(TRIM(cr.year) AS UNSIGNED) > 2000 ";
$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($driverIds);
$stmt->execute($allParams);
$finalDrivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
// دمج البيانات
@@ -157,9 +206,9 @@ function findBestDrivers($con, $lat, $lng, $carType) {
}
// --- دالة مساعدة لمخاطبة سيرفر السائقين (Location Socket) ---
function notifyDriversRideTaken($rideId, $winnerDriverId) {
// رابط سيرفر السائقين الداخلي (نفس البورت المستخدم في driver_socket.php)
$url = "http://188.68.36.205:2021";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
if (!isAllowedSocketUrl($url)) return;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'ride_taken_event', // هذا الأكشن الجديد في السوكيت
@@ -179,9 +228,9 @@ function notifyDriversRideTaken($rideId, $winnerDriverId) {
curl_close($ch);
}
function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = null) {
// رابط سيرفر اللوكيشن الخارجي
$url = "http://188.68.36.205:2021";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
if (!isAllowedSocketUrl($url)) return null;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = [
'action' => 'dispatch_order', // اسم الحدث المتفق عليه في socket_server.php هناك
@@ -215,9 +264,9 @@ function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = n
* تخاطب السوكيت الموجود محلياً على نفس السيرفر
*/
function notifyPassengerOnRideServer($passenger_id, $payload) {
// الرابط لسيرفر سوكيت الركاب — IP مباشر لتجاوز مشاكل الجدار الناري والدومين
$url = "http://188.68.36.205:3031";
$INTERNAL_KEY = trim((string)@file_get_contents('/home/siro-api/.internal_socket_key'));
if (!isAllowedSocketUrl($url)) return null;
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
if (empty($INTERNAL_KEY)) {
error_log("[SOCKET_CRITICAL] Internal key missing at /home/siro-api/.internal_socket_key");
@@ -263,8 +312,8 @@ function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNa
error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers");
$socketUrl = 'http://188.68.36.205:2021';
$internalKeyPath = '/home/siro-api/.internal_socket_key';
$internalKey = file_exists($internalKeyPath) ? trim((string)@file_get_contents($internalKeyPath)) : '';
if (!isAllowedSocketUrl($socketUrl)) return;
$internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
foreach ($driversData as $driver) {
$driverId = $driver['driver_id'];

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-FP');

View File

@@ -6,7 +6,22 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header("Access-Control-Allow-Origin: " . (getenv('ALLOWED_ORIGIN') ?: '*'));
// ✅ FIX H-03: allowlist صارم للـ Admin Origins
$allowedOrigins = array_filter([
getenv('ALLOWED_ORIGIN') ?: 'https://siromove.com',
'http://localhost',
'http://127.0.0.1',
]);
$requestOrigin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (!empty($requestOrigin)) {
if (in_array($requestOrigin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: " . $requestOrigin);
} else {
header("Access-Control-Allow-Origin: https://siromove.com");
}
}
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
@@ -38,9 +53,8 @@ try {
$con = Database::get('main');
// ── جلب بيانات المشرف ────────────────────────────────────
// ملاحظة: جدول admin_users سيتم إنشاؤه في Phase 4 (db_improvements.sql)
$stmt = $con->prepare("SELECT id, password, email, role FROM admin_users WHERE username = :id OR email = :id LIMIT 1");
// ── جلب بيانات المشرف من جدول adminUser الموحد ──────────
$stmt = $con->prepare("SELECT id, password, email, role FROM adminUser WHERE id = :id OR email = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$admin = $stmt->fetch();
@@ -73,9 +87,9 @@ try {
}
} catch (PDOException $e) {
securityLog("Admin Login PDO Error", ['msg' => $e->getMessage()]);
error_log("[Admin Login PDO Error] " . $e->getMessage());
jsonError('Login failed: Database error', 500);
} catch (Exception $e) {
securityLog("Admin Login Error", ['msg' => $e->getMessage()]);
error_log("[Admin Login Error] " . $e->getMessage());
jsonError('Login failed: Server error', 500);
}
}

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
@@ -67,7 +67,14 @@ try {
$payload['fingerPrint'] = $fpHash;
}
$secretKey = trim(file_get_contents('/home/siro-api/.secret_key'));
$secretKey = '';
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
$secretKey = trim(file_get_contents($keyPath));
}
if (!$secretKey) {
$secretKey = getenv('JWT_SECRET_KEY') ?: '';
}
$jwt = Firebase\JWT\JWT::encode($payload, $secretKey, 'HS256');
jsonSuccess([

View File

@@ -1,12 +1,13 @@
<?php
// ============================================================
// loginFirstTimeDriver.php — توكن التسجيل الأول (السائق)
// تم التحديث: استخدام One-Time Registration Tokens
// ============================================================
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
@@ -27,7 +28,6 @@ try {
$allowed1 = getenv('allowedDriver1');
$allowed2 = getenv('allowedDriver2');
$allowedAudiences = array_values(array_filter([$allowed1, $allowed2]));
$passwordnewpassenger = getenv('passwordnewpassenger');
if (empty($id) || empty($password) || empty($audience)) {
jsonError('Missing input fields.', 400);
@@ -37,9 +37,25 @@ try {
jsonError('Invalid audience', 400);
}
if (!password_verify($password, $passwordnewpassenger)) {
securityLog("FirstTimeDriver login failed (password)", ['id' => $id]);
jsonError('Invalid credentials.', 401);
// ✅ FIX H-06: استخدام One-Time Registration Token عبر Redis بدلاً من كلمة مرور ثابتة
$useOneTimeToken = getenv('USE_ONE_TIME_REG_TOKEN') === 'true';
if ($useOneTimeToken && $redis) {
// التحقق من وجود توكن صالح في Redis
$storedToken = $redis->get("reg_token:{$id}");
if (!$storedToken || !hash_equals($storedToken, $password)) {
securityLog("FirstTimeDriver failed: Invalid or expired one-time token", ['id' => $id]);
jsonError('Invalid or expired registration token.', 401);
}
// حذف التوكن بعد الاستخدام (One-Time)
$redis->del("reg_token:{$id}");
} else {
// Fallback آمن: استخدام كلمة المرور الثابتة مع Rate Limiting مشدد
$passwordnewpassenger = getenv('passwordnewpassenger');
if (!password_verify($password, $passwordnewpassenger)) {
securityLog("FirstTimeDriver login failed (password)", ['id' => $id]);
jsonError('Invalid credentials.', 401);
}
}
$fpPepper = getenv('FP_PEPPER') ?: '';
@@ -47,6 +63,14 @@ try {
? hash('sha256', $fingerprint . $fpPepper)
: null;
// ✅ FIX C-02: استخدام getenv بدلاً من file_get_contents الثابت
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) {
$secretKey = trim(file_get_contents($keyPath));
} else {
$secretKey = getenv('JWT_SECRET_KEY') ?: '';
}
$payload = [
'user_id' => 'new',
'sub' => $id,
@@ -62,7 +86,6 @@ try {
$payload['fingerPrint'] = $fpHash;
}
$secretKey = trim(file_get_contents('/home/siro-api/.secret_key'));
$jwt = Firebase\JWT\JWT::encode($payload, $secretKey, 'HS256');
jsonSuccess([

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-FP');
@@ -63,7 +63,13 @@ try {
$decPhone = !empty($driver['phone']) ? $encryptionHelper->decryptData($driver['phone']) : null;
$decNat = !empty($driver['national_number']) ? $encryptionHelper->decryptData($driver['national_number']) : null;
// ✅ FIX M-04: تسجيل معلومات تشخيصية عند فشل فك التشفير
if (empty($decPhone) || empty($decNat)) {
securityLog("LoginDriver failed: decryption returned null", [
'driver_id' => $driver['id'] ?? 'unknown',
'has_phone' => !empty($driver['phone']),
'has_nat' => !empty($driver['national_number']),
]);
unauthorizedDriver();
}

View File

@@ -6,7 +6,7 @@
require_once __DIR__ . '/core/bootstrap.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-FP');
@@ -76,7 +76,14 @@ try {
'jti' => bin2hex(random_bytes(16)),
];
$secretKey = trim((string)@file_get_contents('/home/siro-api/.secret_key_pay'));
$secretKey = '';
$payKeyPath = getenv('WALLET_SECRET_KEY_PATH');
if ($payKeyPath && file_exists($payKeyPath)) {
$secretKey = trim(@file_get_contents($payKeyPath));
}
if (!$secretKey) {
$secretKey = getenv('WALLET_SECRET_KEY') ?: '';
}
$jwt = Firebase\JWT\JWT::encode($payload, $secretKey, 'HS256');
$hmac = hash_hmac('sha256', $id, getenv('SECRET_KEY_HMAC'));

View File

@@ -9,7 +9,7 @@
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -11,7 +11,7 @@ require_once __DIR__ . '/../get_connect.php';
//include 'functions.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -10,7 +10,7 @@
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -9,7 +9,7 @@
require_once __DIR__ . '/../get_connect.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://intaleqapp.com');
header('Access-Control-Allow-Origin: https://siromove.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

View File

@@ -61,7 +61,8 @@ try {
"Content-Type: application/json"
],
CURLOPT_TIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$result = curl_exec($ch);

View File

@@ -66,7 +66,8 @@ try {
"Content-Type: application/json"
],
CURLOPT_TIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$result = curl_exec($ch);

View File

@@ -5,30 +5,41 @@ require_once __DIR__ . '/../../connect.php';
header('Content-Type: application/json');
// 1. Authenticate user
$decodedToken = authenticateJWT();
if (!$decodedToken) {
// 1. الـ Sender ID من JWT مباشرة (connect.php) — ممنوع استقباله من الـ request
if (empty($user_id) || $role !== 'driver') {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$senderID = filterRequest('driverID');
$senderID = $user_id; // ✅ من JWT
$receiverPhone = filterRequest('receiverPhone');
$amount = filterRequest('amount');
$country = filterRequest('country');
if (empty($senderID) || empty($receiverPhone) || empty($amount) || empty($country)) {
if (empty($receiverPhone) || empty($amount) || empty($country)) {
echo json_encode(['status' => 'error', 'message' => 'Missing required fields']);
exit;
}
// Ensure the sender matches the token
if ($decodedToken->id != $senderID) {
echo json_encode(['status' => 'error', 'message' => 'Unauthorized driver ID']);
exit;
// 2. حد أقصى للتحويل (حسب الدولة والعملة)
$maxAmount = 1000000; // افتراضي
$amountInt = (int)$amount;
if ($amountInt <= 0) {
echo json_encode(['status' => 'error', 'message' => 'Invalid amount']);
exit;
}
$countryLower = strtolower($country);
if ($countryLower === 'syria') $maxAmount = 500;
elseif ($countryLower === 'jordan') $maxAmount = 15;
elseif ($countryLower === 'egypt') $maxAmount = 1000;
if ($amountInt > $maxAmount) {
echo json_encode(['status' => 'error', 'message' => "Transfer amount exceeds maximum limit of $maxAmount"]);
exit;
}
// 2. Fetch Receiver details
// 3. Fetch Receiver details
$stmt = $con->prepare("SELECT d.id as driver_id, dt.token as fcm_token, d.name_arabic
FROM driver d
LEFT JOIN driverToken dt ON d.id = dt.captain_id
@@ -48,7 +59,7 @@ if ($receiverID == $senderID) {
exit;
}
// 3. Determine Payment Server URL based on Country
// 4. Determine Payment Server URL based on Country
$walletServer = "https://walletintaleq.intaleq.xyz"; // Default
if (strtolower($country) === 'jordan') {
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz";
@@ -69,14 +80,14 @@ $postData = [
// Generate Headers for Payment Server (Use internal payment key)
$headers = [];
$paymentKey = getenv('PAYMENT_KEY') ;
$paymentKey = getenv('PAYMENT_KEY');
if (!empty($paymentKey)) {
$headers[] = "payment-key: $paymentKey";
} else {
// Fallback just in case
$headers[] = "payment-key: default_internal_secret_123";
if (empty($paymentKey)) {
error_log("CRITICAL: PAYMENT_KEY environment variable is not set. Transfer blocked.");
echo json_encode(['status' => 'error', 'message' => 'Payment configuration error']);
exit;
}
$headers[] = "payment-key: $paymentKey";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $paymentServerUrl);
@@ -91,7 +102,7 @@ curl_close($ch);
$paymentResponse = json_decode($paymentResponseRaw, true);
// 4. Handle Payment Server Response
// 5. Handle Payment Server Response
if ($httpCode === 200 && isset($paymentResponse['status']) && $paymentResponse['status'] === 'success') {
// Transaction successful, send Push Notification
if (!empty($receiver['fcm_token'])) {
@@ -118,11 +129,11 @@ if ($httpCode === 200 && isset($paymentResponse['status']) && $paymentResponse['
'receiver' => $receiver['name_arabic']
]);
} else {
// Payment failed or server error
// Payment failed or server error — ممنوع تسريب debug في الإنتاج
error_log("[transfer] Payment server error | HTTP: $httpCode | Response: $paymentResponseRaw");
echo json_encode([
'status' => 'error',
'message' => $paymentResponse['message'] ?? 'Payment server error',
'debug' => $paymentResponseRaw
'message' => $paymentResponse['message'] ?? 'Payment server error'
]);
}
?>

View File

@@ -5,7 +5,7 @@
require_once __DIR__ . '/../../connect.php';
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Content-Type: application/json; charset=UTF-8");
// تفعيل إظهار الأخطاء لمعرفة مشكلة الكتابة

View File

@@ -2,11 +2,20 @@
// ضبط الهيدر لإرجاع JSON بترميز UTF-8
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../load_env.php';
loadEnvironment('/home/siro-api/env/.env');
// --- إعدادات الاتصال بقاعدة البيانات ---
$servername = "localhost";
$username = "routeuser"; // <== عدّل
$password = "VETA9mX4tSZzm6AGouIM"; // <== عدّل
$dbname = "routedb"; // <== عدّل
$servername = getenv('DB_REVERSE_GEO_HOST') ?: 'localhost';
$username = getenv('DB_REVERSE_GEO_USER') ?: '';
$password = getenv('DB_REVERSE_GEO_PASS') ?: '';
$dbname = getenv('DB_REVERSE_GEO_NAME') ?: '';
if (empty($username) || empty($password) || empty($dbname)) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Database configuration error']);
exit;
}
// --- استقبال الإحداثيات من الطلب ---
$input_lat = isset($_GET['lat']) ? (float)$_GET['lat'] : null;

View File

@@ -286,20 +286,23 @@ foreach ($categories as $key => $carType) {
$priceToken = "";
if (isset($encryptionHelper)) {
$tokenPayload = [
'passenger_id' => $passenger_id,
'passenger_id' => $passenger_id,
'start_location' => $passengerLat . ',' . $passengerLng,
'end_location' => $destLat . ',' . $destLng,
'expires' => time() + 180, // Valid for 3 minutes
'prices' => $pricesRaw
'end_location' => $destLat . ',' . $destLng,
// ✅ FIX R6: تضمين distance و duration في الـ token لمنع التلاعب
'distance' => $distance,
'duration' => $duration,
'expires' => time() + 180, // Valid for 3 minutes
'prices' => $pricesRaw
];
$priceToken = $encryptionHelper->encryptData(json_encode($tokenPayload));
}
echo json_encode([
'status' => 'success',
'data' => $prices,
'price_token' => $priceToken,
'applied_discount' => $discount,
'status' => 'success',
'data' => $prices,
'price_token' => $priceToken,
'applied_discount' => $discount,
'added_negative_balance' => $negativeBalance
]);
?>

View File

@@ -18,8 +18,7 @@ try {
// =================================================================================
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData) {
$url = getenv('LOCATION_SOCKET_URL');
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH');
$INTERNAL_KEY = $keyPath && file_exists($keyPath) ? trim(file_get_contents($keyPath)) : '';
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$marketPayload = [
'id' => (string)$rideId,
@@ -138,6 +137,19 @@ if (!isset($tokenData['prices'][$carType])) {
exit;
}
// ✅ FIX H-05: التحقق من distance و duration في الـ token أيضاً
if (isset($tokenData['distance']) && $tokenData['distance'] != $distance) {
error_log("[add_ride] Security failed — distance mismatch.");
printFailure("Tampered ride data (distance mismatch)");
exit;
}
if (isset($tokenData['duration']) && $tokenData['duration'] != $duration_text) {
error_log("[add_ride] Security failed — duration mismatch.");
printFailure("Tampered ride data (duration mismatch)");
exit;
}
// Securely override pricing from the cryptographically signed token
$price = $tokenData['prices'][$carType]['price'];
$price_for_driver = $tokenData['prices'][$carType]['driver_price'];

View File

@@ -4,7 +4,7 @@
require_once __DIR__ . '/../../get_connect.php';
// السماح بالوصول من أي دومين (لأن الرابط سيفتح في متصفح العميل)
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Content-Type: application/json; charset=UTF-8");
$rideID = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);

View File

@@ -13,7 +13,7 @@ while (ob_get_level()) {
// ابدأ مخزناً جديداً ونظيفاً لهذا الملف فقط
ob_start();
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Origin: https://siromove.com");
header("Access-Control-Allow-Methods: GET");
header("Content-Type: application/json; charset=UTF-8");
@@ -41,8 +41,9 @@ try {
$driverID = $rideData['driver_id'];
$status = $rideData['status'];
$secretSalt = "Siro_Secure_Track_2025";
$generatedToken = md5(trim(strval($rideID)) . trim(strval($driverID)) . $secretSalt);
// ✅ FIX H-03: استبدال md5 بـ hash_hmac
$secretSalt = getenv('TRACKING_SECRET_SALT') ;
$generatedToken = hash_hmac('sha256', $rideID . $driverID, $secretSalt);
if ($token !== $generatedToken) sendError("Invalid Token");

View File

@@ -53,13 +53,19 @@ DROP TABLE IF EXISTS `adminUser`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `adminUser` (
`id` int NOT NULL AUTO_INCREMENT,
`device_number` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UUID hex via bin2hex(random_bytes(16))',
`fingerprint` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'بصمة الجهاز مشفرة (AES-GCM)',
`fingerprint_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'sha256 للبصمة للبحث السريع',
`name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'الاسم مشفر (AES-GCM)',
`phone` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'الهاتف مشفر (AES-GCM)',
`email` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'البريد مشفر (AES-GCM)',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'bcrypt',
`role` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'admin' COMMENT 'admin | super_admin | service',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_fp_hash` (`fingerprint_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--

View File

@@ -18,7 +18,6 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
}
// 2. الاستعلام: تجميع عدد الملاحظات لكل موظف في كل يوم
// نستخدم DATE(createdAt) لتوحيد التوقيت لليوم فقط
$sql = "
SELECT
DATE(createdAt) as date,
@@ -27,7 +26,7 @@ $sql = "
FROM
notesForDriverService
WHERE
createdAt BETWEEN '$start_date' AND '$end_date 23:59:59'
createdAt BETWEEN :start_date AND :end_date
GROUP BY
DATE(createdAt), editor
ORDER BY
@@ -35,8 +34,9 @@ $sql = "
";
try {
$end_date_full = $end_date . ' 23:59:59';
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':start_date' => $start_date, ':end_date' => $end_date_full]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($data) {

View File

@@ -25,21 +25,20 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// الاستعلام: جلب عدد السائقين لكل نوع توظيف خلال الفترة المحددة
$sql = "SELECT
employmentType,
COUNT(*) AS `count`
FROM
`driver`
WHERE
DATE(created_at) >= '$start_date'
AND DATE(created_at) <= '$end_date'
DATE(created_at) >= :start_date
AND DATE(created_at) <= :end_date
GROUP BY
employmentType";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':start_date' => $start_date, ':end_date' => $end_date]);
$stats_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($stats_data) {

View File

@@ -12,7 +12,6 @@ $current_month = str_pad($current_month, 2, "0", STR_PAD_LEFT);
$first_day_of_month = date('Y-m-d', strtotime($current_year . '-' . $current_month . '-01'));
$last_day_of_month = date('Y-m-t', strtotime($first_day_of_month));
// تم تعديل الاستعلام ليستخدم المتغيرات $first_day_of_month و $last_day_of_month
$sql = "SELECT
DATE(d.created_at) AS `date`,
d.`maritalStatus` AS NAME,
@@ -21,23 +20,20 @@ FROM
`driver` d
WHERE
d.`maritalStatus` IN ('mayar','masa', 'shahd', 'rama2','rama1')
AND DATE(d.created_at) >= '$first_day_of_month'
AND DATE(d.created_at) <= '$last_day_of_month'
AND DATE(d.created_at) >= :first_day
AND DATE(d.created_at) <= :last_day
GROUP BY
`date`, d.`maritalStatus`
ORDER BY
`date` ASC;
";
`date` ASC";
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':first_day' => $first_day_of_month, ':last_day' => $last_day_of_month]);
$passenger_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($passenger_data) {
// طباعة البيانات كـ JSON
jsonSuccess($data = $passenger_data);
jsonSuccess($passenger_data);
} else {
// طباعة رسالة فشل
jsonError($message = "No data found");
jsonError("No data found");
}
?>

View File

@@ -5,14 +5,13 @@ require_once __DIR__ . '/../connect.php';
// إذا لم يتم إرسال تاريخ، نستخدم تاريخ اليوم الحالي
$filter_date = isset($_POST['date']) ? $_POST['date'] : date('Y-m-d');
// الاستعلام: جلب كافة الملاحظات لليوم المحدد مرتبة من الأحدث للأقدم
$sql = "SELECT * FROM `notesForDriverService`
WHERE DATE(`createdAt`) = '$filter_date'
WHERE DATE(`createdAt`) = :filter_date
ORDER BY `createdAt` DESC";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([':filter_date' => $filter_date]);
$notes_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($notes_data) {

View File

@@ -18,38 +18,40 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// 2. جملة SQL المعدلة
$end_date_full = $end_date . ' 23:59:59';
$sql = "
WITH RECURSIVE date_series AS (
SELECT '$start_date' AS date
SELECT :start_date AS date
UNION ALL
SELECT DATE_ADD(date, INTERVAL 1 DAY)
FROM date_series
WHERE date < '$end_date'
WHERE date < :end_date
)
SELECT
date_series.date AS day,
-- (passengers) عدد الركاب اليومي
COALESCE((SELECT COUNT(id) FROM passengers WHERE DATE(passengers.created_at) = date_series.date), 0) AS totalPassengers,
-- (passengers) الإجمالي للفترة المحددة
(
SELECT COUNT(*)
FROM passengers
WHERE passengers.created_at BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE passengers.created_at BETWEEN :start_date_total AND :end_date_total
) AS totalMonthly
FROM
date_series
GROUP BY date_series.date
ORDER BY date_series.date ASC;
";
// ملاحظة: جعلت الترتيب ASC (تصاعدي) لأن الرسم البياني يحتاج الأيام من البداية للنهاية
ORDER BY date_series.date ASC";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([
':start_date' => $start_date,
':end_date' => $end_date,
':start_date_total' => $start_date,
':end_date_total' => $end_date_full
]);
$passenger_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($passenger_data) {

View File

@@ -18,24 +18,23 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// 2. جملة الـ SQL المعدلة لتعمل مع النطاق الزمني
$end_date_full = $end_date . ' 23:59:59';
$sql = "
WITH RECURSIVE date_series AS (
SELECT '$start_date' AS date
SELECT :start_date AS date
UNION ALL
SELECT DATE_ADD(date, INTERVAL 1 DAY)
FROM date_series
WHERE date < '$end_date'
WHERE date < :end_date
)
SELECT
date_series.date AS day,
-- حساب الرحلات المنتهية في هذا اليوم
COALESCE(SUM(ride.status = 'Finished'), 0) AS totalRides,
-- حساب الإجمالي للفترة المحددة كاملة (وليس للشهر فقط)
(SELECT COUNT(*) FROM ride
WHERE ride.created_at >= '$start_date'
AND ride.created_at <= '$end_date 23:59:59'
WHERE ride.created_at >= :start_date_total
AND ride.created_at <= :end_date_total
AND ride.status = 'Finished') AS totalMonthly
FROM
@@ -44,16 +43,22 @@ LEFT JOIN
ride ON DATE(ride.created_at) = date_series.date
AND ride.status = 'Finished'
WHERE
date_series.date >= '$start_date'
AND date_series.date <= '$end_date'
date_series.date >= :start_date_where
AND date_series.date <= :end_date_where
GROUP BY
date_series.date
ORDER BY
date_series.date ASC;
";
date_series.date ASC";
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([
':start_date' => $start_date,
':end_date' => $end_date,
':start_date_total' => $start_date,
':end_date_total' => $end_date_full,
':start_date_where' => $start_date,
':end_date_where' => $end_date
]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($data) {

View File

@@ -18,36 +18,33 @@ if (isset($_POST['start_date']) && isset($_POST['end_date'])) {
$end_date = date('Y-m-t', strtotime($start_date));
}
// 2. جملة SQL المعدلة
$end_date_full = $end_date . ' 23:59:59';
$sql = "
WITH RECURSIVE date_series AS (
SELECT '$start_date' AS DATE
SELECT :start_date AS DATE
UNION ALL
SELECT DATE_ADD(DATE, INTERVAL 1 DAY)
FROM date_series
WHERE DATE < '$end_date'
WHERE DATE < :end_date
)
SELECT
date_series.date AS day,
-- [1] إجمالي السائقين (الكلي في النظام)
(SELECT COUNT(*) FROM driver) AS totalDrivers,
-- [2] يومي: عدد السائقين المسجلين
(
SELECT COUNT(*)
FROM driver
WHERE DATE(driver.created_at) = date_series.date
) AS dailyTotalDrivers,
-- [3] يومي: عدد السائقين المتصل بهم (notesForDriverService)
(
SELECT COUNT(*)
FROM notesForDriverService
WHERE DATE(notesForDriverService.createdAt) = date_series.date
) AS dailyTotalCallingDrivers,
-- [4] يومي: Matching Notes (Join with driver on phone)
(
SELECT COUNT(*)
FROM notesForDriverService n
@@ -55,26 +52,23 @@ SELECT
WHERE DATE(n.createdAt) = date_series.date
) AS dailyMatchingNotes,
-- [5] إجمالي الفترة: سائقين
(
SELECT COUNT(*)
FROM driver
WHERE driver.created_at BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE driver.created_at BETWEEN :start_date1 AND :end_date1
) AS totalMonthlyDrivers,
-- [6] إجمالي الفترة: Calling Drivers
(
SELECT COUNT(*)
FROM notesForDriverService
WHERE notesForDriverService.createdAt BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE notesForDriverService.createdAt BETWEEN :start_date2 AND :end_date2
) AS totalMonthlyCallingDrivers,
-- [7] إجمالي الفترة: Matching Notes
(
SELECT COUNT(*)
FROM notesForDriverService n
JOIN driver d ON n.phone = d.phone
WHERE n.createdAt BETWEEN '$start_date' AND '$end_date 23:59:59'
WHERE n.createdAt BETWEEN :start_date3 AND :end_date3
) AS totalMonthlyMatchingNotes
FROM
@@ -82,12 +76,20 @@ FROM
GROUP BY
date_series.date
ORDER BY
date_series.date ASC;
";
date_series.date ASC";
try {
$stmt = $con->prepare($sql);
$stmt->execute();
$stmt->execute([
':start_date' => $start_date,
':end_date' => $end_date,
':start_date1' => $start_date,
':end_date1' => $end_date_full,
':start_date2' => $start_date,
':end_date2' => $end_date_full,
':start_date3' => $start_date,
':end_date3' => $end_date_full
]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($data) {

View File

@@ -108,11 +108,11 @@ try {
// توليد مفتاح HMAC فريد للمستخدم (للتوافق مع CRUD الجديد)
$hmacKey = hash_hmac('sha256', (string)$user['id'], getenv('SECRET_KEY_HMAC'));
// ✅ FIX H-05: لا نعيد مفتاح HMAC أبداً (يُحسب على العميل بنفس المعادلة)
printSuccess([
"message" => "Login successful",
"data" => $user,
"jwt" => $jwt,
"hmac" => $hmacKey,
"expires_in" => $expires_in
]);
@@ -124,8 +124,10 @@ try {
jsonError("الجهاز أو الحساب غير مسجل. يرجى إدخال البريد الإلكتروني وكلمة المرور إذا كان هذا أول تسجيل دخول لك.");
}
} catch (Exception $e) {
error_log("[ServiceApp Login Error] " . $e->getMessage());
jsonError("Server error: " . $e->getMessage());
// ✅ FIX M-02: إخفاء تفاصيل الخطأ في الإنتاج
$debugMode = getenv('APP_DEBUG') === 'true';
securityLog("[ServiceApp Login Error]", ['msg' => $e->getMessage()]);
jsonError($debugMode ? "Server error: " . $e->getMessage() : "Server error. Please try again later.", 500);
}
exit();

View File

@@ -6,7 +6,7 @@
<title>نظام إدارة المركبات</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer">
<style>
body { font-family: 'Cairo', sans-serif; background-color: #f8fafc; }
.skeleton {

View File

@@ -1,68 +1,73 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/connect.php'; // Ensure this is correct and contains the connection logic
// ============================================================
// upload_audio.php — رفع ملفات صوتية بشكل آمن
// ============================================================
// Get the audio file from the request
$audio_file = $_FILES['audio'];
$passengerId = filterRequest("passengerId"); // Ensure this is defined correctly
require_once __DIR__ . '/connect.php';
// Check if the audio file was uploaded successfully
if ($audio_file['error'] !== UPLOAD_ERR_OK) {
error_log("File upload error: " . $audio_file['error']);
echo json_encode(array('status' => 'The audio file was not uploaded successfully.'));
exit;
}
header('Content-Type: application/json; charset=UTF-8');
// Get the file name and extension of the audio file
$audio_name = $audio_file['name'];
$audio_extension = pathinfo($audio_name, PATHINFO_EXTENSION);
try {
// ✅ FIX C-03: إضافة Rate Limiting
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'upload');
// Check if the audio file is a valid audio format
if (!in_array($audio_extension, array('m4a', 'mp3', 'wav'))) {
echo json_encode(array('status' => 'The audio file is not a valid format.'));
exit;
}
// Get the audio file from the request
if (!isset($_FILES['audio']) || $_FILES['audio']['error'] !== UPLOAD_ERR_OK) {
uploadLog("File upload error code: " . ($_FILES['audio']['error'] ?? 'NO_FILE'), 'ERROR');
echo json_encode(array('status' => 'The audio file was not uploaded successfully.'));
exit;
}
// MIME Type validation using finfo
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $audio_file['tmp_name']);
finfo_close($finfo);
$audio_file = $_FILES['audio'];
$allowed_mime_types = ['audio/mp4', 'audio/mpeg', 'audio/wav', 'audio/x-m4a'];
if (!in_array($mime_type, $allowed_mime_types)) {
echo json_encode(array('status' => 'The audio file is not a valid format (MIME mismatch).'));
exit;
}
// Validate MIME type using finfo
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($audio_file['tmp_name']);
// Generate a new filename using the passenger ID to avoid conflicts
$new_filename = $audio_name . '.' . $audio_extension;
$allowed_mime_types = ['audio/mp4', 'audio/mpeg', 'audio/wav', 'audio/x-m4a', 'audio/ogg', 'audio/webm'];
if (!in_array($mime_type, $allowed_mime_types, true)) {
uploadLog("Invalid MIME type: $mime_type", 'WARNING', ['ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown']);
echo json_encode(array('status' => 'The audio file is not a valid format (MIME mismatch).'));
exit;
}
// Move the audio file to the uploads directory with the new filename
$target_dir = "upload_audio/";
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true); // Create directory if it doesn't exist
}
$target_file = $target_dir . $new_filename;
if (!move_uploaded_file($audio_file['tmp_name'], $target_file)) {
error_log("Failed to move file to target directory: " . print_r($audio_file, true));
echo json_encode(array('status' => 'Failed to move the audio file.'));
exit;
}
// Get extension from MIME type (safe)
$ext = match ($mime_type) {
'audio/mp4', 'audio/x-m4a' => 'm4a',
'audio/mpeg' => 'mp3',
'audio/wav' => 'wav',
'audio/ogg' => 'ogg',
'audio/webm' => 'webm',
default => 'bin',
};
// Construct the link to the uploaded audio file dynamically
$host = $_SERVER['HTTP_HOST'] ?? 'api.siromove.com';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
$base_url = "$protocol://$host/siro/upload_audio/";
$linkAudio = $base_url . $new_filename;
// ✅ Generate secure random filename to prevent path traversal and overwrite
$new_filename = bin2hex(random_bytes(16)) . '.' . $ext;
// Move the audio file to the uploads directory
$target_dir = __DIR__ . "/upload_audio/";
if (!is_dir($target_dir)) {
mkdir($target_dir, 0750, true);
}
$target_file = $target_dir . $new_filename;
// Respond with success and the audio file link
echo json_encode(array('status' => 'Audio file uploaded successfully.', 'link' => $linkAudio));
if (!move_uploaded_file($audio_file['tmp_name'], $target_file)) {
uploadLog("Failed to move file", 'ERROR', ['error' => print_r($audio_file, true)]);
echo json_encode(array('status' => 'Failed to move the audio file.'));
exit;
}
// Close the database connection if it was established
if (isset($conn)) {
mysqli_close($conn);
// Construct the link dynamically
$host = $_SERVER['HTTP_HOST'] ?? 'api.siromove.com';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
$linkAudio = "$protocol://$host/siro/upload_audio/" . $new_filename;
uploadLog("Audio uploaded successfully: $linkAudio", 'INFO');
echo json_encode(array('status' => 'Audio file uploaded successfully.', 'link' => $linkAudio));
} catch (Exception $e) {
error_log("[upload_audio] Error: " . $e->getMessage());
echo json_encode(array('status' => 'Server error.'));
}
?>