1<?php
2
3namespace GeoIp2\Database;
4
5use GeoIp2\Exception\AddressNotFoundException;
6use GeoIp2\ProviderInterface;
7use MaxMind\Db\Reader as DbReader;
8use MaxMind\Db\Reader\InvalidDatabaseException;
9
10/**
11 * Instances of this class provide a reader for the GeoIP2 database format.
12 * IP addresses can be looked up using the database specific methods.
13 *
14 * ## Usage ##
15 *
16 * The basic API for this class is the same for every database. First, you
17 * create a reader object, specifying a file name. You then call the method
18 * corresponding to the specific database, passing it the IP address you want
19 * to look up.
20 *
21 * If the request succeeds, the method call will return a model class for
22 * the method you called. This model in turn contains multiple record classes,
23 * each of which represents part of the data returned by the database. If
24 * the database does not contain the requested information, the attributes
25 * on the record class will have a `null` value.
26 *
27 * If the address is not in the database, an
28 * {@link \GeoIp2\Exception\AddressNotFoundException} exception will be
29 * thrown. If an invalid IP address is passed to one of the methods, a
30 * SPL {@link \InvalidArgumentException} will be thrown. If the database is
31 * corrupt or invalid, a {@link \MaxMind\Db\Reader\InvalidDatabaseException}
32 * will be thrown.
33 */
34class Reader implements ProviderInterface
35{
36    private $dbReader;
37    private $locales;
38
39    /**
40     * Constructor.
41     *
42     * @param string $filename the path to the GeoIP2 database file
43     * @param array  $locales  list of locale codes to use in name property
44     *                         from most preferred to least preferred
45     *
46     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
47     *                                                     is corrupt or invalid
48     */
49    public function __construct(
50        $filename,
51        $locales = ['en']
52    ) {
53        $this->dbReader = new DbReader($filename);
54        $this->locales = $locales;
55    }
56
57    /**
58     * This method returns a GeoIP2 City model.
59     *
60     * @param string $ipAddress an IPv4 or IPv6 address as a string
61     *
62     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
63     *                                                     not in the database
64     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
65     *                                                     is corrupt or invalid
66     *
67     * @return \GeoIp2\Model\City
68     */
69    public function city($ipAddress)
70    {
71        return $this->modelFor('City', 'City', $ipAddress);
72    }
73
74    /**
75     * This method returns a GeoIP2 Country model.
76     *
77     * @param string $ipAddress an IPv4 or IPv6 address as a string
78     *
79     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
80     *                                                     not in the database
81     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
82     *                                                     is corrupt or invalid
83     *
84     * @return \GeoIp2\Model\Country
85     */
86    public function country($ipAddress)
87    {
88        return $this->modelFor('Country', 'Country', $ipAddress);
89    }
90
91    /**
92     * This method returns a GeoIP2 Anonymous IP model.
93     *
94     * @param string $ipAddress an IPv4 or IPv6 address as a string
95     *
96     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
97     *                                                     not in the database
98     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
99     *                                                     is corrupt or invalid
100     *
101     * @return \GeoIp2\Model\AnonymousIp
102     */
103    public function anonymousIp($ipAddress)
104    {
105        return $this->flatModelFor(
106            'AnonymousIp',
107            'GeoIP2-Anonymous-IP',
108            $ipAddress
109        );
110    }
111
112    /**
113     * This method returns a GeoLite2 ASN model.
114     *
115     * @param string $ipAddress an IPv4 or IPv6 address as a string
116     *
117     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
118     *                                                     not in the database
119     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
120     *                                                     is corrupt or invalid
121     *
122     * @return \GeoIp2\Model\Asn
123     */
124    public function asn($ipAddress)
125    {
126        return $this->flatModelFor(
127            'Asn',
128            'GeoLite2-ASN',
129            $ipAddress
130        );
131    }
132
133    /**
134     * This method returns a GeoIP2 Connection Type model.
135     *
136     * @param string $ipAddress an IPv4 or IPv6 address as a string
137     *
138     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
139     *                                                     not in the database
140     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
141     *                                                     is corrupt or invalid
142     *
143     * @return \GeoIp2\Model\ConnectionType
144     */
145    public function connectionType($ipAddress)
146    {
147        return $this->flatModelFor(
148            'ConnectionType',
149            'GeoIP2-Connection-Type',
150            $ipAddress
151        );
152    }
153
154    /**
155     * This method returns a GeoIP2 Domain model.
156     *
157     * @param string $ipAddress an IPv4 or IPv6 address as a string
158     *
159     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
160     *                                                     not in the database
161     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
162     *                                                     is corrupt or invalid
163     *
164     * @return \GeoIp2\Model\Domain
165     */
166    public function domain($ipAddress)
167    {
168        return $this->flatModelFor(
169            'Domain',
170            'GeoIP2-Domain',
171            $ipAddress
172        );
173    }
174
175    /**
176     * This method returns a GeoIP2 Enterprise model.
177     *
178     * @param string $ipAddress an IPv4 or IPv6 address as a string
179     *
180     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
181     *                                                     not in the database
182     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
183     *                                                     is corrupt or invalid
184     *
185     * @return \GeoIp2\Model\Enterprise
186     */
187    public function enterprise($ipAddress)
188    {
189        return $this->modelFor('Enterprise', 'Enterprise', $ipAddress);
190    }
191
192    /**
193     * This method returns a GeoIP2 ISP model.
194     *
195     * @param string $ipAddress an IPv4 or IPv6 address as a string
196     *
197     * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
198     *                                                     not in the database
199     * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
200     *                                                     is corrupt or invalid
201     *
202     * @return \GeoIp2\Model\Isp
203     */
204    public function isp($ipAddress)
205    {
206        return $this->flatModelFor(
207            'Isp',
208            'GeoIP2-ISP',
209            $ipAddress
210        );
211    }
212
213    private function modelFor($class, $type, $ipAddress)
214    {
215        $record = $this->getRecord($class, $type, $ipAddress);
216
217        $record['traits']['ip_address'] = $ipAddress;
218        $class = 'GeoIp2\\Model\\' . $class;
219
220        return new $class($record, $this->locales);
221    }
222
223    private function flatModelFor($class, $type, $ipAddress)
224    {
225        $record = $this->getRecord($class, $type, $ipAddress);
226
227        $record['ip_address'] = $ipAddress;
228        $class = 'GeoIp2\\Model\\' . $class;
229
230        return new $class($record);
231    }
232
233    private function getRecord($class, $type, $ipAddress)
234    {
235        if (strpos($this->metadata()->databaseType, $type) === false) {
236            $method = lcfirst($class);
237            throw new \BadMethodCallException(
238                "The $method method cannot be used to open a "
239                . $this->metadata()->databaseType . ' database'
240            );
241        }
242        $record = $this->dbReader->get($ipAddress);
243        if ($record === null) {
244            throw new AddressNotFoundException(
245                "The address $ipAddress is not in the database."
246            );
247        }
248        if (!is_array($record)) {
249            // This can happen on corrupt databases. Generally,
250            // MaxMind\Db\Reader will throw a
251            // MaxMind\Db\Reader\InvalidDatabaseException, but occasionally
252            // the lookup may result in a record that looks valid but is not
253            // an array. This mostly happens when the user is ignoring all
254            // exceptions and the more frequent InvalidDatabaseException
255            // exceptions go unnoticed.
256            throw new InvalidDatabaseException(
257                "Expected an array when looking up $ipAddress but received: "
258                . gettype($record)
259            );
260        }
261
262        return $record;
263    }
264
265    /**
266     * @throws \InvalidArgumentException if arguments are passed to the method
267     * @throws \BadMethodCallException   if the database has been closed
268     *
269     * @return \MaxMind\Db\Reader\Metadata object for the database
270     */
271    public function metadata()
272    {
273        return $this->dbReader->metadata();
274    }
275
276    /**
277     * Closes the GeoIP2 database and returns the resources to the system.
278     */
279    public function close()
280    {
281        $this->dbReader->close();
282    }
283}
284