> */ private array $ranges; private string $snapshotDate; private string $fetchedAt; /** @var array */ private array $sources; /** * @param array> $ranges * @param array $sources */ private function __construct(array $ranges, string $snapshotDate, string $fetchedAt, array $sources) { $this->ranges = $ranges; $this->snapshotDate = $snapshotDate; $this->fetchedAt = $fetchedAt; $this->sources = $sources; } public static function defaultFilePath(): string { return dirname(__DIR__) . '/data/crawler-ip-ranges.json'; } public static function fromDefaultFile(): self { return self::fromFile(self::defaultFilePath()); } public static function fromFile(string $path): self { if (!is_readable($path)) { throw new RuntimeException('Cannot read crawler-ip-ranges.json: ' . json_encode($path, self::MESSAGE_VALUE_FLAGS)); } $raw = file_get_contents($path); if ($raw === false) { throw new RuntimeException('Failed to read crawler-ip-ranges.json: ' . json_encode($path, self::MESSAGE_VALUE_FLAGS)); } $decoded = json_decode($raw, true); if (!is_array($decoded)) { throw new InvalidArgumentException('crawler-ip-ranges.json contains invalid JSON: ' . json_encode(json_last_error_msg(), self::MESSAGE_VALUE_FLAGS)); } return self::fromArray($decoded); } /** * @param array $data */ public static function fromArray(array $data): self { if (($data['schema_version'] ?? null) !== 1) { throw new InvalidArgumentException('Missing or unsupported schema_version'); } if (!isset($data['ranges']) || !is_array($data['ranges'])) { throw new InvalidArgumentException('Missing ranges map'); } $ranges = []; foreach ($data['ranges'] as $crawlerId => $cidrs) { if (!is_string($crawlerId) || !is_array($cidrs)) { throw new InvalidArgumentException('Invalid ranges entry (expected crawler-id key and CIDR-list value)'); } $list = []; foreach ($cidrs as $cidr) { if (!is_string($cidr) || '' === $cidr) { throw new InvalidArgumentException('ranges[' . json_encode($crawlerId, self::MESSAGE_VALUE_FLAGS) . '] contains an invalid CIDR'); } $list[] = $cidr; } $ranges[$crawlerId] = $list; } $sources = []; foreach ($data['sources'] ?? [] as $crawlerId => $url) { if (is_string($crawlerId) && is_string($url)) { $sources[$crawlerId] = $url; } } return new self( $ranges, isset($data['snapshot_date']) && is_string($data['snapshot_date']) ? $data['snapshot_date'] : '', isset($data['fetched_at']) && is_string($data['fetched_at']) ? $data['fetched_at'] : '', $sources ); } /** * @return array */ public function rangesFor(string $crawlerId): array { return $this->ranges[$crawlerId] ?? []; } public function hasRanges(string $crawlerId): bool { return [] !== ($this->ranges[$crawlerId] ?? []); } /** * @return array */ public function crawlerIds(): array { return array_keys($this->ranges); } public function snapshotDate(): string { return $this->snapshotDate; } public function fetchedAt(): string { return $this->fetchedAt; } /** * @return array */ public function sources(): array { return $this->sources; } }