'failure', 'message' => 'Forbidden. Super Admin access required to broadcast notifications.', ], JSON_UNESCAPED_UNICODE); exit; } $audience = filterRequest('audience'); $title = filterRequest('title'); $body = filterRequest('body'); // المواضيع المسموح بها فقط — يشترك بها التطبيقان (intaleq_driver / intaleq_rider). // قصرها على قائمة ثابتة يمنع استخدام النقطة لبثّ رسائل إلى مواضيع عشوائية // أو إلى توكن جهاز بعينه. $ALLOWED_AUDIENCES = [ 'drivers' => 'drivers', 'passengers' => 'passengers', ]; if (!isset($ALLOWED_AUDIENCES[$audience])) { jsonError('Invalid audience. Allowed: ' . implode(', ', array_keys($ALLOWED_AUDIENCES)), 400); } $title = trim((string) $title); $body = trim((string) $body); if ($title === '' || $body === '') { jsonError('Both title and body are required.', 400); } if (mb_strlen($title) > 120) { jsonError('Title is too long (max 120 characters).', 400); } if (mb_strlen($body) > 1000) { jsonError('Body is too long (max 1000 characters).', 400); } $topic = $ALLOWED_AUDIENCES[$audience]; // سجل التدقيق قبل الإرسال: نريد أثراً حتى لو فشل النداء أو انقطع. securityLog("Broadcast notification requested", [ 'user_id' => $user_id ?? 'unknown', 'audience' => $audience, 'title' => $title, 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', ]); if (function_exists('logAudit')) { try { logAudit($con, (string) ($user_id ?? 'unknown'), 'إرسال إشعار جماعي', 'notification', $topic, [ 'audience' => $audience, 'title' => $title, 'body' => $body, ]); } catch (Throwable $e) { error_log("[Broadcast] audit log failed: " . $e->getMessage()); } } // الاستدعاء الداخلي لخدمة FCM // من داخل حاوية php لا يوجد خادم ويب على 127.0.0.1 — الويب في حاوية nginx // منفصلة، وتُعرف داخل شبكة Compose باسم الخدمة. هذا كان سبب فشل كل إشعار. $fcmUrl = getenv('FCM_INTERNAL_URL') ?: 'http://nginx/backend/ride/firebase/send_fcm.php'; $payload = json_encode([ 'target' => $topic, 'title' => $title, 'body' => $body, 'isTopic' => true, 'data' => ['category' => 'admin_broadcast'], ], JSON_UNESCAPED_UNICODE); $headers = ['Content-Type: application/json; charset=UTF-8']; $internalKey = getenv('FCM_INTERNAL_API_KEY'); if (!empty($internalKey)) { $headers[] = 'X-API-KEY: ' . $internalKey; } $ch = curl_init($fcmUrl); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_error($ch); curl_close($ch); if ($response === false || $httpCode >= 400) { $reason = $curlErr ?: (is_string($response) ? substr($response, 0, 200) : 'no response'); error_log("[Broadcast] FCM call failed (HTTP $httpCode) via $fcmUrl: $reason"); jsonError("Notification service unreachable at $fcmUrl — $reason", 502); } $decoded = json_decode((string) $response, true); jsonSuccess([ 'audience' => $audience, 'topic' => $topic, 'title' => $title, 'sent_by' => $user_id ?? null, 'sent_at' => date('Y-m-d H:i:s'), 'fcm_status' => $decoded['status'] ?? 'unknown', ], 'Broadcast delivered to the notification service.');