1<?php
2/**
3 * Horde optimized interface to the MaxMind IP Address->Country listing.
4 *
5 * Based on PHP geoip.inc library by MaxMind LLC:
6 *   http://www.maxmind.com/download/geoip/api/php/
7 *
8 * Originally based on php version of the geoip library written in May
9 * 2002 by jim winstead <jimw@apache.org>
10 *
11 * Copyright 2003 MaxMind LLC
12 * Copyright 2003-2016 Horde LLC (http://www.horde.org/)
13 *
14 * This library is free software; you can redistribute it and/or
15 * modify it under the terms of the GNU Lesser General Public
16 * License as published by the Free Software Foundation; either
17 * version 2.1 of the License, or (at your option) any later version.
18 *
19 * See the enclosed file COPYING for license information (LGPL). If you
20 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
21 *
22 * @author   Michael Slusarz <slusarz@horde.org>
23 * @category Horde
24 * @package  Nls
25 */
26class Horde_Nls_Geoip
27{
28    /* TODO */
29    const GEOIP_COUNTRY_BEGIN = 16776960;
30    const STRUCTURE_INFO_MAX_SIZE = 20;
31    const STANDARD_RECORD_LENGTH = 3;
32
33    /**
34     * Country list.
35     *
36     * @var array
37     */
38    protected $_countryCodes = array(
39        '', 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO',
40        'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE',
41        'BF', 'BG', 'BH', 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT',
42        'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
43        'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ',
44        'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
45        'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'FX', 'GA', 'UK',
46        'GD', 'GE', 'GF', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR',
47        'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU',
48        'ID', 'IE', 'IL', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JM', 'JO',
49        'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY',
50        'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV',
51        'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO',
52        'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ',
53        'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU',
54        'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN',
55        'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RU', 'RW', 'SA',
56        'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM',
57        'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG',
58        'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TP', 'TR', 'TT', 'TV', 'TW',
59        'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG',
60        'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'YU', 'ZA', 'ZM', 'ZR',
61        'ZW', 'A1', 'A2', 'O1'
62    );
63
64    /**
65     * The location of the GeoIP database.
66     *
67     * @var string
68     */
69    protected $_datafile;
70
71    /**
72     * The open filehandle to the GeoIP database.
73     *
74     * @var resource
75     */
76    protected $_fh;
77
78    /**
79     * Constructor.
80     *
81     * @param string $datafile         The location of the GeoIP database.
82     */
83    public function __construct($datafile)
84    {
85        $this->_datafile = $datafile;
86    }
87
88    /**
89     * Open the GeoIP database.
90     *
91     * @return boolean  False on error.
92     */
93    protected function _open()
94    {
95        /* Return if we already have an object. */
96        if (!empty($this->_fh)) {
97            return true;
98        }
99
100        /* Return if no datafile specified. */
101        if (empty($this->_datafile)) {
102            return false;
103        }
104
105        $this->_fh = fopen($this->_datafile, 'rb');
106        if (!$this->_fh) {
107            return false;
108        }
109
110        $filepos = ftell($this->_fh);
111        fseek($this->_fh, -3, SEEK_END);
112        for ($i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; ++$i) {
113            $delim = fread($this->_fh, 3);
114            if ($delim == (chr(255) . chr(255) . chr(255))) {
115                break;
116            } else {
117                fseek($this->_fh, -4, SEEK_CUR);
118            }
119        }
120        fseek($this->_fh, $filepos, SEEK_SET);
121
122        return true;
123    }
124
125    /**
126     * Returns the country ID and Name for a given hostname.
127     *
128     * @param string $name  The hostname.
129     *
130     * @return mixed  An array with 'code' as the country code and 'name' as
131     *                the country name, or false if not found.
132     */
133    public function getCountryInfo($name)
134    {
135        if (Horde_Util::extensionExists('geoip')) {
136            $id = @geoip_country_code_by_name($name);
137            $cname = @geoip_country_name_by_name($name);
138            return (!empty($id) && !empty($cname)) ?
139                array('code' => Horde_String::lower($id), 'name' => $cname):
140                false;
141        }
142
143        $id = $this->countryIdByName($name);
144        if (!empty($id)) {
145            $code = $this->_countryCodes[$id];
146            return array(
147                'code' => Horde_String::lower($code),
148                'name' => $this->_getName($code)
149            );
150        }
151
152        return false;
153    }
154
155    /**
156     * Returns the country ID for a hostname.
157     *
158     * @param string $name  The hostname.
159     *
160     * @return integer  The GeoIP country ID.
161     */
162    public function countryIdByName($name)
163    {
164        if (!$this->_open()) {
165            return false;
166        }
167
168        $addr = gethostbyname($name);
169        if (!$addr || ($addr == $name)) {
170            return false;
171        }
172
173        return $this->countryIdByAddr($addr);
174    }
175
176    /**
177     * Returns the country abbreviation (2-letter) for a hostname.
178     *
179     * @param string $name  The hostname.
180     *
181     * @return integer  The country abbreviation.
182     */
183    public function countryCodeByName($name)
184    {
185        if ($this->_open()) {
186            $country_id = $this->countryIdByName($name);
187            if ($country_id !== false) {
188                return $this->_countryCodes[$country_id];
189            }
190        }
191
192        return false;
193    }
194
195    /**
196     * Returns the country name for a hostname.
197     *
198     * @param string $name  The hostname.
199     *
200     * @return integer  The country name.
201     */
202    public function countryNameByName($name)
203    {
204        if ($this->_open()) {
205            $country_id = $this->countryCodeByName($name);
206            if ($country_id !== false) {
207                return $this->_getName($country_id);
208            }
209        }
210
211        return false;
212    }
213
214    /**
215     * Returns the country ID for an IP Address.
216     *
217     * @param string $addr  The IP Address.
218     *
219     * @return integer  The GeoIP country ID.
220     */
221    public function countryIdByAddr($addr)
222    {
223        if (!$this->_open()) {
224            return false;
225        }
226
227        $ipnum = ip2long($addr);
228        $country = $this->_seekCountry($ipnum);
229
230        return ($country === false)
231            ? ''
232            : ($this->_seekCountry($ipnum) - self::GEOIP_COUNTRY_BEGIN);
233    }
234
235    /**
236     * Returns the country abbreviation (2-letter) for an IP Address.
237     *
238     * @param string $addr  The IP Address.
239     *
240     * @return integer  The country abbreviation.
241     */
242    public function countryCodeByAddr($addr)
243    {
244        if ($this->_open()) {
245            $country_id = $this->countryIdByAddr($addr);
246            if ($country_id !== false) {
247                return $this->_countryCodes[$country_id];
248            }
249        }
250
251        return false;
252    }
253
254    /**
255     * Returns the country name for an IP address.
256     *
257     * @param string $addr  The IP address.
258     *
259     * @return mixed  The country name.
260     */
261    public function countryNameByAddr($addr)
262    {
263        if ($this->_open()) {
264            $country_id = $this->countryCodeByAddr($addr);
265            if ($country_id !== false) {
266                return $this->_getName($country_id);
267            }
268        }
269
270        return false;
271    }
272
273    /**
274     * Finds a country by IP Address in the GeoIP database.
275     *
276     * @param string $ipnum  The IP Address to search for.
277     *
278     * @return mixed  The country ID or false if not found.
279     */
280    protected function _seekCountry($ipnum)
281    {
282        $offset = 0;
283
284        for ($depth = 31; $depth >= 0; --$depth) {
285            if (fseek($this->_fh, 2 * self::STANDARD_RECORD_LENGTH * $offset, SEEK_SET) != 0) {
286                return false;
287            }
288            $buf = fread($this->_fh, 2 * self::STANDARD_RECORD_LENGTH);
289            $x = array(0, 0);
290
291            for ($i = 0; $i < 2; ++$i) {
292                for ($j = 0; $j < self::STANDARD_RECORD_LENGTH; ++$j) {
293                    $x[$i] += ord($buf[self::STANDARD_RECORD_LENGTH * $i + $j]) << ($j * 8);
294                }
295            }
296            if ($ipnum & (1 << $depth)) {
297                if ($x[1] >= self::GEOIP_COUNTRY_BEGIN) {
298                    return $x[1];
299                }
300                $offset = $x[1];
301            } else {
302                if ($x[0] >= self::GEOIP_COUNTRY_BEGIN) {
303                    return $x[0];
304                }
305                $offset = $x[0];
306            }
307        }
308
309        return false;
310    }
311
312    /**
313     * Given a 2-letter country code, returns a country string.
314     *
315     * @param string $code  The country code.
316     *
317     * @return string  The country string.
318     */
319    protected function _getName($code)
320    {
321        $code = Horde_String::upper($code);
322
323        $geoip_codes = array(
324            'AP' => Horde_Nls_Translation::t("Asia/Pacific Region"),
325            'EU' => Horde_Nls_Translation::t("Europe"),
326            'A1' => Horde_Nls_Translation::t("Anonymous Proxy"),
327            'A2' => Horde_Nls_Translation::t("Satellite Provider"),
328            'O1' => Horde_Nls_Translation::t("Other")
329        );
330
331        return isset($geoip_codes[$code])
332            ? $geoip_codes[$code]
333            : strval(Horde_Nls::getCountryISO($code));
334    }
335
336}
337