'error', 'message' => 'Unauthorized']); exit; } function resolveServiceAccountPath(): string { $envPath = getenv('FIREBASE_SERVICE_ACCOUNT_PATH') ?: ''; if (!empty($envPath) && file_exists($envPath)) { return $envPath; } // if (file_exists('/keys/firebase_service_account.json')) { // return '/keys/firebase_service_account.json'; // } if (file_exists('/keys/service-account.json')) { return '/keys/service-account.json'; } // if (file_exists(__DIR__ . '/../../keys/firebase_service_account.json')) { // return __DIR__ . '/../../keys/firebase_service_account.json'; // } return __DIR__ . '/service-account.json'; } $serviceAccountFile = resolveServiceAccountPath(); // السماح فقط بـ POST if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['status' => 'error', 'message' => 'Only POST allowed.']); exit; } // استقبال البيانات $json_input = file_get_contents('php://input'); $requestData = json_decode($json_input, true); $target = $requestData['target'] ?? null; $title = $requestData['title'] ?? null; $body = $requestData['body'] ?? null; $isTopic = $requestData['isTopic'] ?? false; $tone = $requestData['tone'] ?? 'default'; $customData = $requestData['data'] ?? []; if (!$target) { // ‏يعني أن المُرسِل لا يملك توكن الطرف الآخر — عادةً tokenPassenger أو // ‏driverToken فارغ في التطبيق لأنه لم يصله في حمولة القبول. error_log('[SEND_FCM] REJECTED 400 — empty target. category=' . (string)($requestData['data']['category'] ?? '?')); http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Missing: target, title, or body.']); exit; } // ============================================================================ // دالة Base64 URL-Safe Encoding (ضرورية للـ JWT) // ============================================================================ function base64UrlEncode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } // ============================================================================ // دالة المصادقة (Google OAuth2) // ============================================================================ function getAccessToken($credentialsPath) { if (!file_exists($credentialsPath)) return null; $credentials = json_decode(file_get_contents($credentialsPath), true); $clientEmail = $credentials['client_email']; $privateKey = $credentials['private_key']; $now = time(); $header = base64UrlEncode(json_encode(['alg' => 'RS256', 'typ' => 'JWT'])); $claim = base64UrlEncode(json_encode([ 'iss' => $clientEmail, 'scope' => 'https://www.googleapis.com/auth/firebase.messaging', 'aud' => 'https://oauth2.googleapis.com/token', 'exp' => $now + 3600, 'iat' => $now ])); $signature = ''; openssl_sign("$header.$claim", $signature, $privateKey, 'SHA256'); $jwt = "$header.$claim." . base64UrlEncode($signature); $ch = curl_init("https://oauth2.googleapis.com/token"); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // بلا مهلة كان الطلب يقدر يعلّق حتى مهلة PHP نفسها، فتُسقط الرسالة بصمت // وهذا أحد أسباب "الإشعار مرات يوصل ومرات لا". curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); $res = curl_exec($ch); curl_close($ch); return json_decode($res, true)['access_token'] ?? null; } // الحصول على Access Token $accessToken = getAccessToken($serviceAccountFile); if (!$accessToken) { http_response_code(500); echo json_encode(['status' => 'error', 'message' => 'Failed to get Access Token.']); exit; } // جلب Project ID $creds = json_decode(file_get_contents($serviceAccountFile), true); $projectId = $creds['project_id']; $fcmUrl = "https://fcm.googleapis.com/v1/projects/$projectId/messages:send"; // ============================================================================ // بناء هيكل الرسالة // ============================================================================ // 🔧 توحيد نغمة أندرويد مع FcmService::send — هناك 'ding' تُترجم إلى 'default'، // وهنا كانت تُمرَّر كما هي فيبحث أندرويد عن ملف صوت اسمه "ding" وقد لا يوجد // فيصل الإشعار بلا صوت (أو لا يُلفت النظر إطلاقاً). $androidSound = ($tone === 'ding' || $tone === 'default') ? 'default' : $tone; $messagePayload = [ 'message' => [ 'notification' => [ 'title' => $title, 'body' => $body ], 'android' => [ 'priority' => 'HIGH', 'notification' => [ 'sound' => $androidSound, 'channel_id' => 'high_importance_channel' // تأكد من تطابقه مع Android ] ], 'apns' => [ 'headers' => ['apns-priority' => '10'], 'payload' => [ 'aps' => [ 'sound' => ($tone === 'ding' || $tone === 'default') ? 'default' : (str_ends_with($tone, '.caf') ? $tone : $tone . '.caf'), 'content-available' => 1 ] ] ] ] ]; // تحديد الهدف (Topic أو Token) if ($isTopic) { $messagePayload['message']['topic'] = $target; } else { $messagePayload['message']['token'] = $target; } // ============================================================================ // 🔥 معالجة Data Payload (يجب أن تكون String: String فقط) // ============================================================================ // FcmService::send يحقن title/body/tone/category/type داخل data، وتطبيق الراكب // يقرأ message.data['title'] أولاً. هذا المسار كان لا يحقنها، فأي رسالة تمرّ من // هنا تظهر بعنوان فارغ عند العميل الذي يعتمد على data. نوحّد السلوك. $customData = array_merge(is_array($customData) ? $customData : [], [ 'title' => (string)$title, 'body' => (string)$body, 'tone' => (string)$tone, ]); if (!empty($customData)) { $processedData = []; foreach ($customData as $key => $val) { if (is_array($val) || is_object($val)) { // تحويل المصفوفات/الكائنات إلى JSON String $processedData[$key] = json_encode($val, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { // تحويل أي قيمة أخرى إلى String $processedData[$key] = (string)$val; } } $messagePayload['message']['data'] = $processedData; } // ============================================================================ // الإرسال الفعلي إلى FCM // ============================================================================ $ch = curl_init($fcmUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json; charset=UTF-8' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($messagePayload, JSON_UNESCAPED_UNICODE)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // ‏هذه النقطة هي مسار كل الرسائل والمكالمات بين الراكب والسائق (التطبيقان // ‏يستدعيانها مباشرة)، ولم تكن تسجّل شيئاً إطلاقاً — بخلاف FcmService التي // ‏تطبع [FCM_DEBUG]. فكان فشل الرسائل غير قابل للتشخيص: لا سبب ولا رد Google. $logCategory = (string)($customData['category'] ?? ''); $logTarget = $isTopic ? "topic:$target" : substr((string)$target, 0, 16) . '…'; error_log( "[SEND_FCM] category=$logCategory target=$logTarget http=$httpCode" . " len=" . strlen((string)$target) . ($httpCode == 200 ? '' : " response=$result") ); // الرد if ($httpCode == 200) { echo json_encode([ 'status' => 'success', 'message' => 'Notification sent successfully', 'fcm_response' => json_decode($result) ], JSON_UNESCAPED_UNICODE); } else { http_response_code($httpCode); echo json_encode([ 'status' => 'error', 'message' => 'FCM request failed', 'fcm_response' => json_decode($result) ], JSON_UNESCAPED_UNICODE); } ?>