enforce_rate_limit(); return $this->do_scan( $attachment_id, $file_url ); } /** * Builds a domain → count map from a list of URL-bearing items. * * Each item must be an array with a string 'url' key. Items missing the key * or whose host cannot be parsed are silently skipped. * * @param array $items URL-bearing items (e.g. pages or backlink objects). * * @return array Domain → occurrence count, sorted descending, max 10. */ protected function extract_top_domains( array $items ): array { $counts = array(); foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } $url_val = $item['url'] ?? null; if ( ! is_string( $url_val ) || '' === $url_val ) { continue; } $host = wp_parse_url( $url_val, PHP_URL_HOST ); if ( ! is_string( $host ) || '' === $host ) { continue; } $counts[ $host ] = ( $counts[ $host ] ?? 0 ) + 1; } arsort( $counts ); return array_slice( $counts, 0, 50, true ); } /** * Checks and increments the per-minute request counter via transients. * * Throws if the counter has reached the configured limit. * * @throws \RuntimeException When the rate limit for the current minute is exhausted. */ private function enforce_rate_limit(): void { $key = 'mra_rl_' . $this->provider_slug() . '_' . gmdate( 'YmdHi' ); $raw = get_transient( $key ); $current = is_numeric( $raw ) ? (int) $raw : 0; if ( $current >= $this->rate_limit() ) { // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new \RuntimeException( sprintf( 'Rate limit of %d req/min exceeded for provider "%s".', $this->rate_limit(), $this->provider_slug() ) ); // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped } set_transient( $key, $current + 1, 90 ); } }