Passed
Push — develop ( afff43...70b7ee )
by Paul
14:29
created

Geolocation::requestArgs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace GeminiLabs\SiteReviews;
4
5
use GeminiLabs\SiteReviews\Defaults\GeolocationDefaults;
6
use GeminiLabs\SiteReviews\Modules\Sanitizer;
7
8
class Geolocation
9
{
10
    public const API_URL = 'http://ip-api.com';
11
12
    public const FIELDS = [
13
        'city',
14
        'continentCode',
15
        'countryCode',
16
        'isp',
17
        'message',
18
        'query',
19
        'region',
20
        'status',
21
    ];
22
23
    /**
24
     * Transient key for rate limit tracking.
25
     */
26
    public const RATE_LIMIT_KEY = 'glsr_ip_api_rate_limit';
27
28
    /**
29
     * Rate limit safety buffer in seconds.
30
     */
31
    public const RATE_LIMIT_SAFETY_BUFFER = 5;
32
33
    protected Api $api;
34
35
    public function __construct()
36
    {
37
        $this->api = glsr(Api::class, ['url' => static::API_URL]);
38
    }
39
40
    public function batchLookup(array $ipaddresses): Response
41
    {
42
        $data = array_values(array_filter(array_map(
43
            [glsr(Sanitizer::class), 'sanitizeIpAddress'],
44
            $ipaddresses
45
        )));
46
        if (empty($data)) {
47
            return new Response();
48
        }
49
        $this->checkRateLimits();
50
        $path = sprintf('/batch?fields=%s', implode(',', static::FIELDS));
51
        $response = $this->api->post($path, $this->requestArgs([
52
            'body' => wp_json_encode($data),
53
            'headers' => ['Content-Type' => 'application/json'],
54
        ]));
55
        $this->handleRateLimits($response);
56
        if ($response->successful()) {
57
            $body = $response->body();
58
            $response->body = array_map([glsr(GeolocationDefaults::class), 'unguardedRestrict'], $body);
59
        }
60
        return $response;
61
    }
62
63
    public function lookup(string $ipOrDomain, bool $allowDomain = false): Response
64
    {
65
        $entity = glsr(Sanitizer::class)->sanitizeIpAddress($ipOrDomain) ?: (
66
            $allowDomain ? filter_var($ipOrDomain, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME) : false
67
        );
68
        if (empty($entity)) {
69
            return new Response();
70
        }
71
        $this->checkRateLimits();
72
        $path = sprintf('/json/%s?fields=%s', $entity, implode(',', static::FIELDS));
73
        $response = $this->api->get($path, $this->requestArgs());
74
        $this->handleRateLimits($response);
75
        if ($response->successful()) {
76
            $body = $response->body();
77
            $response->body = glsr(GeolocationDefaults::class)->unguardedRestrict($body);
78
        }
79
        return $response;
80
    }
81
82
    /**
83
     * Check rate limits based on transient.
84
     */
85
    protected function checkRateLimits(): void
86
    {
87
        $transient = get_transient(static::RATE_LIMIT_KEY);
88
        if ($transient && 0 === $transient['remaining']) {
89
            $waitTime = max(0, $transient['reset_time'] - time());
90
            if ($waitTime > 0) {
91
                glsr_log()->warning("Geolocation: Rate limit reached, waiting {$waitTime} seconds");
92
                sleep($waitTime);
93
            }
94
        }
95
    }
96
97
    /**
98
     * Handle rate limits based on transient and response headers.
99
     *
100
     * `/json` GET requests are limited to 45 requests per minute.
101
     * `/batch` POST requests are limited to 15 requests per minute.
102
     */
103
    protected function handleRateLimits(Response $response): void
104
    {
105
        $remainingRequests = (int) $response->headers['x-rl'];
106
        $resetTime = (int) $response->headers['x-ttl'] + static::RATE_LIMIT_SAFETY_BUFFER;
107
        set_transient(static::RATE_LIMIT_KEY, [
108
            'remaining' => $remainingRequests,
109
            'reset_time' => time() + $resetTime,
110
        ], $resetTime);
111
    }
112
113
    protected function requestArgs(array $extra = []): array
114
    {
115
        return wp_parse_args($extra, [
116
            'blocking' => true,
117
            'max_retries' => 3,
118
            'timeout' => 15,
119
        ]);
120
    }
121
}
122