Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.19% covered (success)
92.19%
59 / 64
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
IPoidInfoRetriever
92.19% covered (success)
92.19%
59 / 64
75.00% covered (warning)
75.00%
6 / 8
18.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 retrieveFor
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 makeIPoidInfo
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 retrieveBatch
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 getData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getFeedEndpointUrl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 processRequestBody
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
4.77
1<?php
2
3namespace MediaWiki\IPInfo\InfoRetriever;
4
5use MediaWiki\Config\ServiceOptions;
6use MediaWiki\Http\HttpRequestFactory;
7use MediaWiki\IPInfo\Info\IPoidInfo;
8use MediaWiki\User\UserIdentity;
9use Psr\Log\LoggerInterface;
10use Wikimedia\IPUtils;
11
12/**
13 * Manager for getting information from the iPoid service.
14 */
15class IPoidInfoRetriever extends BaseInfoRetriever {
16    /**
17     * @internal For use by ServiceWiring
18     */
19    public const CONSTRUCTOR_OPTIONS = [
20        'IPInfoIpoidUrl',
21    ];
22
23    public const NAME = 'ipinfo-source-ipoid';
24
25    private ServiceOptions $options;
26
27    private HttpRequestFactory $httpRequestFactory;
28
29    private LoggerInterface $logger;
30
31    public function __construct(
32        ServiceOptions $options,
33        HttpRequestFactory $httpRequestFactory,
34        LoggerInterface $logger
35    ) {
36        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
37        $this->options = $options;
38        $this->httpRequestFactory = $httpRequestFactory;
39        $this->logger = $logger;
40    }
41
42    /** @inheritDoc */
43    public function getName(): string {
44        return self::NAME;
45    }
46
47    /**
48     * @inheritDoc
49     * @return IPoidInfo
50     */
51    public function retrieveFor( UserIdentity $user, ?string $ip ): IPoidInfo {
52        if ( $ip === null ) {
53            return new IPoidInfo();
54        }
55
56        if ( $this->options->get( 'IPInfoIpoidUrl' ) ) {
57            $data = $this->getData( $ip );
58
59            return self::makeIPoidInfo( $data );
60        }
61
62        return new IPoidInfo();
63    }
64
65    /**
66     * Convert raw data from the IPoid service into an {@link IPoidInfo} object.
67     * @param array $data IP data returned by IPInfo
68     * @return IPoidInfo
69     */
70    private static function makeIPoidInfo( array $data ): IPoidInfo {
71        $info = [
72            'behaviors' => $data['behaviors'] ?? null,
73            'risks' => $data['risks'] ?? null,
74            'connectionTypes' => $data['types'] ?? null,
75            'tunnelOperators' => $data['tunnels'] ?? null,
76            'proxies' => $data['proxies'] ?? null,
77            'numUsersOnThisIP' => $data['client_count'] ?? null,
78        ];
79
80        return new IPoidInfo(
81            $info['behaviors'],
82            $info['risks'],
83            $info['connectionTypes'],
84            $info['tunnelOperators'],
85            $info['proxies'],
86            $info['numUsersOnThisIP'],
87        );
88    }
89
90    /**
91     * Retrieve IP information for the given IPs from the IPoid service.
92     * @param UserIdentity $user
93     * @param string[] $ips IP addresses in human-readable form
94     * @return IPoidInfo[] Map of IPoidInfo instances keyed by IP address
95     */
96    public function retrieveBatch( UserIdentity $user, array $ips ): array {
97        if ( !$this->options->get( 'IPInfoIpoidUrl' ) ) {
98            return array_fill_keys( $ips, new IPoidInfo() );
99        }
100
101        $reqs = [];
102
103        foreach ( $ips as $ip ) {
104            $reqs[] = [
105                'url' => $this->getFeedEndpointUrl( $ip ),
106                'method' => 'GET'
107            ];
108        }
109
110        $httpClient = $this->httpRequestFactory->createMultiClient();
111        $reqs = $httpClient->runMulti( $reqs );
112        $infoByIp = [];
113
114        foreach ( $reqs as $i => $req ) {
115            $ip = $ips[$i];
116            if ( $req['response']['code'] === 200 ) {
117                $data = $this->processRequestBody( $ip, $req['response']['body'] );
118                $infoByIp[$ip] = self::makeIPoidInfo( $data );
119            } else {
120                $infoByIp[$ip] = new IPoidInfo();
121            }
122        }
123
124        return $infoByIp;
125    }
126
127    /**
128     * Call the iPoid API to get data for an IP address.
129     *
130     * @param string $ip
131     * @return mixed[] Data returned by iPoid
132     */
133    private function getData( string $ip ): array {
134        $url = $this->getFeedEndpointUrl( $ip );
135        $request = $this->httpRequestFactory->create( $url, [ 'method' => 'GET' ], __METHOD__ );
136        $response = $request->execute();
137
138        if ( $response->isOK() ) {
139            return $this->processRequestBody( $ip, $request->getContent() );
140        }
141
142        return [];
143    }
144
145    /**
146     * Get the IPoid feed endpoint URL for looking up IP data.
147     * @param string $ip The IP to look up data for
148     * @return string Fully qualified endpoint URL
149     */
150    private function getFeedEndpointUrl( string $ip ): string {
151        $baseUrl = $this->options->get( 'IPInfoIpoidUrl' );
152        return $baseUrl . '/feed/v1/ip/' . $ip;
153    }
154
155    /**
156     * Parse the given HTTP request body, which is assumed to hold data for the given IP.
157     *
158     * @param string $ip The IP to fetch data for
159     * @param string $body HTTP response body
160     * @return array Parsed response data for the given IP, or empty array if no data was available.
161     */
162    private function processRequestBody( string $ip, string $body ): array {
163        $content = json_decode( $body, true );
164        if ( is_array( $content ) ) {
165            $sanitizedIp = IPUtils::sanitizeIP( $ip );
166            foreach ( $content as $key => $value ) {
167                if ( $sanitizedIp === IPUtils::sanitizeIP( $key ) ) {
168                    return $value;
169                }
170            }
171
172            return [];
173        }
174
175        $this->logger->debug(
176            "ipoid results were not in the expected format: " . $body
177        );
178
179        return [];
180    }
181
182}