Test Failed
Push — develop ( 33b521...cdbd76 )
by Paul
10:16 queued 21s
created

Geolocation::batchLookup()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 18
rs 9.7998
cc 2
nc 2
nop 1
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 FIELDS = [
11
        'city',
12
        'continentCode',
13
        'countryCode',
14
        'message',
15
        'query',
16
        'region',
17
        'status',
18
    ];
19
20
    public const API_URL = 'http://ip-api.com';
21
22
    /**
23
     * String transient key for rate limit tracking.
24
     */
25
    public const RATE_LIMIT_KEY = 'glsr_ip_api_rate_limit';
26
27
    protected Api $api;
28
29
    public function __construct()
30
    {
31
        $this->api = glsr(Api::class, ['url' => static::API_URL]);
32
    }
33
34
    public function batchLookup(array $ipaddresses): Response
35
    {
36
        $this->checkRateLimits();
37
        $data = array_map([glsr(Sanitizer::class), 'sanitizeIpAddress'], $ipaddresses);
38
        $path = sprintf('/batch?fields=%s', implode(',', static::FIELDS));
39
        $response = $this->api->post($path, [
40
            'blocking' => true,
41
            'body' => wp_json_encode($data),
42
            'headers' => ['Content-Type' => 'application/json'],
43
            'max_retries' => 3,
44
            'timeout' => 15,
45
        ]);
46
        $this->handleRateLimits($response);
47
        if ($response->successful()) {
48
            $body = $response->body();
49
            $response->body = array_map([glsr(GeolocationDefaults::class), 'unguardedRestrict'], $body);
50
        }
51
        return $response;
52
    }
53
54
    public function lookup(string $ipaddress): Response
55
    {
56
        $this->checkRateLimits();
57
        $data = [
58
            'fields' => implode(',', static::FIELDS),
59
        ];
60
        $path = sprintf('/json/%s', glsr(Sanitizer::class)->sanitizeIpAddress($ipaddress));
61
        $response = $this->api->get($path, [
62
            'blocking' => true,
63
            'body' => $data,
64
            'max_retries' => 3,
65
            'timeout' => 15,
66
        ]);
67
        $this->handleRateLimits($response);
68
        if ($response->successful()) {
69
            $body = $response->body();
70
            $response->body = glsr(GeolocationDefaults::class)->unguardedRestrict($body);
71
        }
72
        return $response;
73
    }
74
75
    /**
76
     * Check rate limits based on transient.
77
     */
78
    protected function checkRateLimits(): void
79
    {
80
        $transient = get_transient(static::RATE_LIMIT_KEY);
81
        if ($transient && 0 === $transient['remaining']) {
82
            $waitTime = max(0, $transient['reset_time'] - time());
83
            if ($waitTime > 0) {
84
                glsr_log()->warning("Geolocation: Rate limit reached, waiting {$waitTime} seconds");
85
                sleep($waitTime);
86
            }
87
        }
88
    }
89
90
    /**
91
     * Handle rate limits based on transient and response headers.
92
     *
93
     * `/json` GET requests are limited to 45 requests per minute.
94
     * `/batch` POST requests are limited to 15 requests per minute.
95
     */
96
    protected function handleRateLimits(Response $response): void
97
    {
98
        $remainingRequests = (int) $response->headers['x-rl'];
99
        $resetTime = (int) $response->headers['x-ttl'] + 5; // Add an extra 5 seconds just in case
100
        set_transient(static::RATE_LIMIT_KEY, [
101
            'remaining' => $remainingRequests,
102
            'reset_time' => time() + $resetTime,
103
        ], $resetTime);
104
    }
105
}
106