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