Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.94% covered (success)
92.94%
79 / 85
83.33% covered (warning)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
GeoIp2EnterpriseInfoRetriever
92.94% covered (success)
92.94%
79 / 85
83.33% covered (warning)
83.33%
10 / 12
28.28
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReader
n/a
0 / 0
n/a
0 / 0
2
 retrieveFromIP
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
6
 getCoordinates
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getAsn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOrganization
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCountryNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLocations
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
3.33
 getIsp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConnectionType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProxyType
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace MediaWiki\IPInfo\InfoRetriever;
4
5use GeoIp2\Database\Reader;
6use GeoIp2\Exception\AddressNotFoundException;
7use GeoIp2\Model\AnonymousIp;
8use GeoIp2\Model\Enterprise;
9use MediaWiki\Config\ServiceOptions;
10use MediaWiki\IPInfo\Info\Coordinates;
11use MediaWiki\IPInfo\Info\Info;
12use MediaWiki\IPInfo\Info\Location;
13use MediaWiki\IPInfo\Info\ProxyType;
14
15/**
16 * Manager for getting information from the MaxMind GeoIp2 Enterprise database.
17 */
18class GeoIp2EnterpriseInfoRetriever implements InfoRetriever {
19    /**
20     * @internal For use by ServiceWiring
21     */
22    public const CONSTRUCTOR_OPTIONS = [
23        'IPInfoGeoIP2EnterprisePath',
24    ];
25
26    /** @var ServiceOptions */
27    private $options;
28
29    /** @var ReaderFactory */
30    private $readerFactory;
31
32    /**
33     * @param ServiceOptions $options
34     * @param ReaderFactory $readerFactory
35     */
36    public function __construct(
37        ServiceOptions $options,
38        ReaderFactory $readerFactory
39    ) {
40        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
41        $this->options = $options;
42        $this->readerFactory = $readerFactory;
43    }
44
45    /** @inheritDoc */
46    public function getName(): string {
47        return 'ipinfo-source-geoip2';
48    }
49
50    /**
51     * @param string $filename
52     * @return Reader|null null if the file path or file is invalid
53     * @codeCoverageIgnore tested when retrieveFromIP is run
54     */
55    public function getReader( string $filename ): ?Reader {
56        $path = $this->options->get( 'IPInfoGeoIP2EnterprisePath' );
57
58        if ( $path === false ) {
59            return null;
60        }
61
62        return $this->readerFactory->get( $path, $filename );
63    }
64
65    /**
66     * @inheritDoc
67     * @return Info
68     */
69    public function retrieveFromIP( string $ip ): Info {
70        $info = array_fill_keys(
71            [
72                'coordinates',
73                'asn',
74                'organization',
75                'countryNames',
76                'locations',
77                'isp',
78                'connectionType',
79                'userType',
80                'proxyType',
81            ],
82            null
83        );
84
85        $enterpriseReader = $this->getReader( 'GeoIP2-Enterprise.mmdb' );
86        if ( $enterpriseReader ) {
87            try {
88                $enterpriseInfo = $enterpriseReader->enterprise( $ip );
89
90                $info['coordinates'] = $this->getCoordinates( $enterpriseInfo );
91                $info['asn'] = $this->getAsn( $enterpriseInfo );
92                $info['organization'] = $this->getOrganization( $enterpriseInfo );
93                $info['countryNames'] = $this->getCountryNames( $enterpriseInfo );
94                $info['locations'] = $this->getLocations( $enterpriseInfo );
95                $info['isp'] = $this->getIsp( $enterpriseInfo );
96                $info['connectionType'] = $this->getConnectionType( $enterpriseInfo );
97                $info['userType'] = $this->getUserType( $enterpriseInfo );
98            } catch ( AddressNotFoundException $e ) {
99                // No need to do anything if it fails
100                // $info defaults to null values
101            }
102        }
103
104        $anonymousIpReader = $this->getReader( 'GeoIP2-Anonymous-IP.mmdb' );
105        if ( $anonymousIpReader ) {
106            try {
107                $anonymousIpInfo = $anonymousIpReader->anonymousIp( $ip );
108                $isLegitimateProxy = null;
109                if ( isset( $enterpriseInfo ) ) {
110                    $isLegitimateProxy = (bool)$enterpriseInfo->traits->isLegitimateProxy;
111                }
112                $info['proxyType'] = $this->getProxyType( $anonymousIpInfo, $isLegitimateProxy );
113            } catch ( AddressNotFoundException $e ) {
114                // No need to do anything if it fails
115                // $info defaults to null values
116            }
117        }
118
119        return new Info(
120            $info['coordinates'],
121            $info['asn'],
122            $info['organization'],
123            $info['countryNames'],
124            $info['locations'],
125            $info['isp'],
126            $info['connectionType'],
127            $info['userType'],
128            $info['proxyType']
129        );
130    }
131
132    /**
133     * @param Enterprise $info
134     * @return Coordinates|null null if IP address does not return a latitude/longitude
135     */
136    private function getCoordinates( Enterprise $info ): ?Coordinates {
137        $location = $info->location;
138        if ( !$location->latitude || !$location->longitude ) {
139            return null;
140        }
141
142        return new Coordinates(
143            $location->latitude,
144            $location->longitude
145        );
146    }
147
148    /**
149     * @param Enterprise $info
150     * @return int|null null if this IP address does not return an ASN
151     */
152    private function getAsn( Enterprise $info ): ?int {
153        return $info->traits->autonomousSystemNumber;
154    }
155
156    /**
157     * @param Enterprise $info
158     * @return string|null null if this IP address does not return an organization
159     */
160    private function getOrganization( Enterprise $info ): ?string {
161        return $info->traits->autonomousSystemOrganization;
162    }
163
164    /**
165     * @param Enterprise $info
166     * @return array|null null if this IP address does not return a country
167     */
168    private function getCountryNames( Enterprise $info ): ?array {
169        return $info->country->names;
170    }
171
172    /**
173     * @param Enterprise $info
174     * @return Location[]|null null if this IP address does not return a location
175     */
176    private function getLocations( Enterprise $info ): ?array {
177        if ( !$info->city->geonameId || !$info->city->name ) {
178            return null;
179        }
180
181        $locations = [ new Location(
182            $info->city->geonameId,
183            $info->city->name
184        ) ];
185
186        /** MaxMind returns the locations sorted largest area to smallest.
187         * array_reverse is used to convert them to the preferred order of
188         * smallest to largest
189         */
190        return array_merge( $locations, array_map(
191            static function ( $subdivision ) {
192                return new Location(
193                    $subdivision->geonameId,
194                    $subdivision->name
195                );
196            },
197            array_reverse( $info->subdivisions )
198        ) );
199    }
200
201    /**
202     * @param Enterprise $info
203     * @return string|null null if GeoIP2 does not return an ISP
204     */
205    private function getIsp( Enterprise $info ): ?string {
206        return $info->traits->isp;
207    }
208
209    /**
210     * @param Enterprise $info
211     * @return string|null null if GeoIP2 does not return a connection type
212     */
213    public function getConnectionType( Enterprise $info ): ?string {
214        return $info->traits->connectionType;
215    }
216
217    /**
218     * @param Enterprise $info
219     * @return string|null null if GeoIP2 does not return a connection type
220     */
221    private function getUserType( Enterprise $info ): ?string {
222        return $info->traits->userType;
223    }
224
225    /**
226     * @param AnonymousIp $anonymousIpinfo
227     * @param bool|null $isLegitimateProxy
228     * @return ProxyType
229     */
230    private function getProxyType( AnonymousIp $anonymousIpinfo, ?bool $isLegitimateProxy ): ProxyType {
231        return new ProxyType(
232            (bool)$anonymousIpinfo->isAnonymousVpn || null,
233            (bool)$anonymousIpinfo->isPublicProxy || null,
234            (bool)$anonymousIpinfo->isResidentialProxy || null,
235            $isLegitimateProxy,
236            (bool)$anonymousIpinfo->isTorExitNode || null,
237            (bool)$anonymousIpinfo->isHostingProvider || null
238        );
239    }
240}