lastFetchMeta ?? []; } public function fetchItems(): ?array { $this->lastFetchMeta = null; $public = $this->fetchPublic(); if ($public !== null) { return $public; } if ($this->canAuth()) { $items = $this->fetchRemoteFull(); if ($items !== null) { return $items; } } return null; } private function canAuth(): bool { return $this->sharedSecret() !== '' || $this->bearerToken() !== ''; } /** * @return array>|null */ private function fetchRemoteFull(): ?array { foreach ($this->attemptOrigins() as $origin) { try { $response = Http::acceptJson() ->withHeaders(array_merge($this->authHeaders('GET', self::REMOTE_PATH), [ 'Cache-Control' => 'no-store', ])) ->timeout(6) ->get($origin.self::REMOTE_PATH); if (! $response->successful() || ! $this->isJsonResponse($response)) { Log::warning('SuperAdmin program-settings remote failed', [ 'origin' => $origin, 'status' => $response->status(), ]); continue; } $items = $this->extractItems($response->json()); if ($this->hasSettingRows($items)) { $this->lastFetchMeta = [ 'channel' => 'remote', 'origin' => $origin, ]; return $items; } } catch (\Throwable $e) { Log::warning('SuperAdmin program-settings remote exception', [ 'origin' => $origin, 'error' => $e->getMessage(), ]); } } return null; } /** * @return array>|null */ private function fetchPublic(): ?array { $domains = $this->domainCandidates(); if ($domains === []) { return null; } foreach ($domains as $domain) { foreach ($this->attemptOrigins() as $origin) { try { $response = Http::acceptJson() ->withHeaders([ 'Cache-Control' => 'no-store', 'Pragma' => 'no-cache', ]) ->withOptions(['http_errors' => false]) ->timeout(6) ->get($origin.self::PUBLIC_PATH, [ 'domain' => $domain, '_ts' => (string) time(), ]); if (! $response->successful() || ! $this->isJsonResponse($response)) { continue; } $items = $this->extractItems($response->json()); if ($this->hasSettingRows($items)) { $this->lastFetchMeta = [ 'channel' => 'public', 'origin' => $origin, 'domain' => $domain, ]; return $items; } } catch (\Throwable $e) { Log::warning('SuperAdmin public program-settings exception', [ 'origin' => $origin, 'domain' => $domain, 'error' => $e->getMessage(), ]); } } } return null; } /** * سه دامنه × سه دور (مستر، بعد اسلیوها). * * @return iterable */ private function attemptOrigins(): iterable { $origins = $this->origins(); for ($round = 1; $round <= self::RETRY_ROUNDS; $round++) { foreach ($origins as $origin) { yield $origin; } } } /** * اول montzar (منبع اصلی)، بعد origin/SMS صریح، بعد failover. * قبلاً SMS اول بود و اگر رپلیکای کهنه جواب ۲۰۰ می‌داد، montzar هیچ‌وقت خوانده نمی‌شد. * * @return list */ private function origins(): array { $defaults = [ 'https://adm.montzar.ir', 'https://adm.montech.ir', 'https://adm.montest.ir', ]; $extra = []; $explicit = trim((string) config('services.superadmin.origin', '')); if ($explicit !== '') { $extra[] = rtrim($explicit, '/'); } $sms = trim((string) config('services.superadmin.sms_base_url', '')); if ($sms !== '') { $parsed = parse_url($sms); if (! empty($parsed['scheme']) && ! empty($parsed['host'])) { $port = isset($parsed['port']) ? ':'.$parsed['port'] : ''; $extra[] = $parsed['scheme'].'://'.$parsed['host'].$port; } } // montzar اول؛ extra فقط اگر هنوز در لیست نیست به انتها اضافه می‌شود return array_values(array_unique(array_merge($defaults, $extra))); } /** * @param array>|null $items */ private function hasSettingRows(?array $items): bool { return is_array($items) && $items !== []; } private function isJsonResponse($response): bool { $contentType = strtolower((string) $response->header('Content-Type')); return str_contains($contentType, 'json'); } /** * @param mixed $data * @return array>|null */ private function extractItems($data): ?array { if (! is_array($data)) { return null; } if (array_is_list($data)) { return $data; } foreach (['settings', 'items', 'data'] as $key) { if (isset($data[$key]) && is_array($data[$key])) { return $data[$key]; } } return null; } /** * @return array */ private function authHeaders(string $method, string $canonicalPath): array { $secret = $this->sharedSecret(); if ($secret !== '') { $domain = $this->domainCandidates()[0] ?? ''; $timestamp = (string) time(); $payload = $timestamp.'.'.strtoupper($method).'.'.$canonicalPath.'.'.$domain; $signature = hash_hmac('sha256', $payload, $secret); return [ 'X-Abshode-Domain' => $domain, 'X-Abshode-Timestamp' => $timestamp, 'X-Abshode-Signature' => $signature, ]; } $token = $this->bearerToken(); if ($token !== '') { return [ 'Authorization' => 'Bearer '.$token, ]; } return []; } private function sharedSecret(): string { return trim((string) config('services.superadmin.sms_shared_secret', '')); } private function bearerToken(): string { return trim((string) config('services.superadmin.remote_token', '')); } /** * دامنه‌های کاندید برای resolve نسخه در سوپرادمین. * * ترتیب مهم است: اولین پاسخ ۲۰۰ معتبر برنده است. * Host واقعی درخواست باید قبل از APP_URL و SUPERADMIN_SMS_DOMAIN باشد؛ * وگرنه دامنهٔ SMS/اشتراکی (با فلگ‌های پیش‌فرض false) به‌اشتباه اعمال می‌شود * و همگام‌سازی «موفق» دیده می‌شود ولی منو روشن نمی‌شود. * * @return list */ private function domainCandidates(): array { $candidates = []; try { $request = request(); if ($request) { // فرانت و API ممکن است دامنهٔ جدا باشند (my.goldarvin.ir → goldarvin.gholaktala.ir). // سوپرادمین دامنهٔ اپ را می‌شناسد نه Hostِ API؛ Origin/Referer را اول بگذار // وگرنه ۳ origin × ۳ retry روی ۴۰۴ حدود ۱۰–۱۷ ثانیه طول می‌کشد. $origin = (string) $request->headers->get('Origin', ''); if ($origin !== '') { $candidates[] = $this->normalizeHost($origin); } $referer = (string) $request->headers->get('Referer', ''); if ($referer !== '') { $candidates[] = $this->normalizeHost($referer); } $forwarded = (string) $request->headers->get('X-Forwarded-Host', ''); if ($forwarded !== '') { $candidates[] = $this->normalizeHost(explode(',', $forwarded)[0] ?? ''); } $candidates[] = $this->normalizeHost((string) $request->getHost()); } } catch (\Throwable) { // CLI / بدون request } $fromApp = parse_url((string) config('app.url'), PHP_URL_HOST); if (is_string($fromApp) && $fromApp !== '') { $candidates[] = $this->normalizeHost($fromApp); } $override = trim((string) config('services.superadmin.sms_domain', '')); if ($override !== '') { $candidates[] = $this->normalizeHost($override); } $out = []; foreach ($candidates as $host) { foreach ($this->expandDomainAliases($host) as $alias) { if ($alias === '' || in_array($alias, $out, true)) { continue; } if (in_array($alias, ['localhost', '127.0.0.1', '::1'], true)) { continue; } $out[] = $alias; } } return $out; } /** * admin.example.com / www.example.com / api.example.com → example.com هم امتحان شود. * * @return list */ private function expandDomainAliases(string $host): array { $host = $this->normalizeHost($host); if ($host === '') { return []; } $aliases = [$host]; foreach (['www.', 'admin.', 'panel.', 'app.', 'api.', 'node.', 'backend.', 'server.'] as $prefix) { if (str_starts_with($host, $prefix)) { $bare = substr($host, strlen($prefix)); if ($bare !== '') { $aliases[] = $bare; } } } if (! str_starts_with($host, 'www.')) { $aliases[] = 'www.'.$host; } return array_values(array_unique($aliases)); } private function normalizeHost(string $value): string { $host = strtolower(trim($value)); $host = preg_replace('#^https?://#', '', $host) ?? $host; $host = explode('/', $host)[0] ?? $host; $host = explode(':', $host)[0] ?? $host; return $host; } }