['requests' => 5, 'window' => 60], // 5 محاولات / دقيقة 'otp' => ['requests' => 3, 'window' => 300], // 3 محاولات / 5 دقائق 'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة 'api' => ['requests' => 120, 'window' => 60], // 120 طلب / دقيقة 'ride' => ['requests' => 30, 'window' => 60], // 30 طلب / دقيقة 'upload' => ['requests' => 10, 'window' => 300], // 10 رفع / 5 دقائق ]; public function __construct(?Redis $redis) { $this->redis = $redis; } // ── فحص الحد ───────────────────────────────────────────── // $identifier: IP:userId أو IP فقط // $type: login | otp | api | ride | upload public function check(string $identifier, string $type = 'api'): bool { if (!$this->redis) { // HIGH-01 FIX: fallback مع ملف بدلاً من تمرير كل الطلبات return $this->fileBasedCheck($identifier, $type); } $limit = self::LIMITS[$type] ?? self::LIMITS['api']; $window = $limit['window']; $max = $limit['requests']; $key = "rate:{$type}:{$identifier}"; $current = $this->redis->incr($key); if ($current === 1) { $this->redis->expire($key, $window); } return $current <= $max; } // ── تطبيق الحد وإيقاف الطلب إن تجاوز ───────────────────── public function enforce(string $identifier, string $type = 'api'): void { if (!$this->check($identifier, $type)) { $limit = self::LIMITS[$type] ?? self::LIMITS['api']; $window = $limit['window']; error_log("[RATE_LIMIT] Blocked: $identifier | type: $type"); http_response_code(429); header("Retry-After: $window"); echo json_encode([ 'error' => 'Too many requests. Please slow down.', 'retry_after' => $window, ]); exit; } } // ── بناء معرّف المستخدم ──────────────────────────────────── public static function identifier(?string $userId = null): string { $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; return $userId ? "{$ip}:{$userId}" : $ip; } // ── إعادة تعيين عداد (مثلاً بعد تسجيل دخول ناجح) ─────────── public function reset(string $identifier, string $type = 'login'): void { if ($this->redis) { $this->redis->del("rate:{$type}:{$identifier}"); } else { // HIGH-01: مسح ملف الفل باك عند إعادة التعيين $key = self::sanitizeKey("rate:{$type}:{$identifier}"); $tmpFile = sys_get_temp_dir() . "/rate_{$key}.json"; if (file_exists($tmpFile)) { @unlink($tmpFile); } } } // ── Fallback باستخدام ملفات مؤقتة عند تعطل Redis ─────────── private function fileBasedCheck(string $identifier, string $type): bool { $limit = self::LIMITS[$type] ?? self::LIMITS['api']; $window = $limit['window']; $max = $limit['requests']; $key = self::sanitizeKey("rate:{$type}:{$identifier}"); $tmpFile = sys_get_temp_dir() . "/rate_{$key}.json"; $now = time(); $data = []; if (file_exists($tmpFile)) { $data = json_decode(file_get_contents($tmpFile), true) ?: []; } // تنظيف النوافذ القديمة $data = array_filter($data, fn($ts) => $ts > ($now - $window)); if (count($data) >= $max) { error_log("[RATE_LIMIT_FB] File-based block: $identifier | type: $type"); return false; } $data[] = $now; file_put_contents($tmpFile, json_encode($data)); return true; } private static function sanitizeKey(string $key): string { return preg_replace('/[^a-zA-Z0-9_\-:]/', '_', $key); } }