$query 요청 쿼리 파라미터 * @return array|null 정규화된 파라미터 (상한 초과 시 null) */ public static function normalizeQuery(array $query): ?array { foreach (self::SYSTEM_QUERY_PARAMS as $param) { unset($query[$param]); } if (count($query) > self::limit('max_query_params', 10)) { return null; } ksort($query); if (strlen(http_build_query($query)) > self::limit('max_query_length', 512)) { return null; } return $query; } /** * 이 IP 가 미스 렌더를 더 수행할 수 있는지 판정합니다. * * @param string $ip 요청 IP * @return bool 렌더 허용 여부 */ public static function renderAllowed(string $ip): bool { return ! RateLimiter::tooManyAttempts( 'seo-render:'.$ip, self::limit('render_misses_per_minute', 60) ); } /** * 미스 렌더 1건을 예산에서 차감합니다. * * @param string $ip 요청 IP */ public static function recordRender(string $ip): void { RateLimiter::hit('seo-render:'.$ip, 60); } /** * 이 IP 의 요청을 통계로 기록할 수 있는지 판정합니다. * * 통계 테이블이 새로운 증식 축이 되지 않도록 기록 자체에도 상한을 둔다. * * @param string $ip 요청 IP * @return bool 기록 허용 여부 */ public static function statsAllowed(string $ip): bool { return ! RateLimiter::tooManyAttempts( 'seo-stats:'.$ip, self::limit('stats_records_per_minute', 300) ); } /** * 통계 기록 1건을 예산에서 차감합니다. * * @param string $ip 요청 IP */ public static function recordStat(string $ip): void { RateLimiter::hit('seo-stats:'.$ip, 60); } /** * 새 URL 을 캐시에 저장할 수 있는지 판정합니다. * * 이미 인덱스에 있는 키의 **갱신**은 이 판정을 거치지 않는다(호출측 책임) — 저장 규모가 * 늘지 않기 때문이다. * * @param array> $index 현재 캐시 인덱스 * @param string $url 저장하려는 URL (경로 + 정규화 쿼리) * @return bool 저장 허용 여부 */ public static function canStore(array $index, string $url): bool { if (count($index) >= self::limit('max_entries', 20000)) { return false; } $path = self::pathOf($url); $variants = 0; foreach ($index as $entry) { if (self::pathOf((string) ($entry['url'] ?? '')) === $path) { $variants++; } } return $variants < self::limit('max_variants_per_path', 50); } /** * URL 에서 경로 부분만 잘라냅니다. * * @param string $url 캐시 URL (`/path?a=1` 형태) * @return string 경로 */ private static function pathOf(string $url): string { $path = parse_url($url, PHP_URL_PATH); return is_string($path) ? $path : $url; } }