1
|
|
|
<?php |
2
|
|
|
/***************************************************************************\ |
3
|
|
|
| Sypex Geo version 2.2.3 | |
4
|
|
|
| (c)2006-2014 zapimir [email protected] http://sypex.net/ | |
5
|
|
|
| (c)2006-2014 BINOVATOR [email protected] | |
6
|
|
|
|---------------------------------------------------------------------------| |
7
|
|
|
| created: 2006.10.17 18:33 modified: 2014.06.20 18:57 | |
8
|
|
|
|---------------------------------------------------------------------------| |
9
|
|
|
| Sypex Geo is released under the terms of the BSD license | |
10
|
|
|
| http://sypex.net/bsd_license.txt | |
11
|
|
|
\***************************************************************************/ |
12
|
|
|
namespace Geography; |
13
|
|
|
define ('SXGEO_FILE', 0); |
14
|
|
|
define ('SXGEO_MEMORY', 1); |
15
|
|
|
define ('SXGEO_BATCH', 2); |
16
|
|
|
class SxGeo { |
17
|
|
|
protected $fh; |
18
|
|
|
protected $ip1c; |
19
|
|
|
protected $info; |
20
|
|
|
protected $range; |
21
|
|
|
protected $db_begin; |
22
|
|
|
protected $b_idx_str; |
23
|
|
|
protected $m_idx_str; |
24
|
|
|
protected $b_idx_arr; |
25
|
|
|
protected $m_idx_arr; |
26
|
|
|
protected $m_idx_len; |
27
|
|
|
protected $db_items; |
28
|
|
|
protected $country_size; |
29
|
|
|
protected $db; |
30
|
|
|
protected $regions_db; |
31
|
|
|
protected $cities_db; |
32
|
|
|
|
33
|
|
|
public $id2iso = array( |
34
|
|
|
'', 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'CW', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', |
35
|
|
|
'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', |
36
|
|
|
'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', |
37
|
|
|
'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', |
38
|
|
|
'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'SX', 'GA', 'GB', 'GD', 'GE', 'GF', |
39
|
|
|
'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', |
40
|
|
|
'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JM', 'JO', 'JP', 'KE', |
41
|
|
|
'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', |
42
|
|
|
'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', |
43
|
|
|
'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', |
44
|
|
|
'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', |
45
|
|
|
'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', |
46
|
|
|
'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD', 'TF', |
47
|
|
|
'TG', 'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TL', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', |
48
|
|
|
'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'RS', 'ZA', |
49
|
|
|
'ZM', 'ME', 'ZW', 'A1', 'XK', 'O1', 'AX', 'GG', 'IM', 'JE', 'BL', 'MF', 'BQ', 'SS' |
50
|
|
|
); |
51
|
|
|
|
52
|
|
|
public $batch_mode = false; |
53
|
|
|
public $memory_mode = false; |
54
|
|
|
|
55
|
|
|
public function __construct($db_file = 'SxGeo.dat', $type = SXGEO_FILE){ |
56
|
|
|
$this->fh = fopen($db_file, 'rb'); |
57
|
|
|
// Сначала убеждаемся, что есть файл базы данных |
58
|
|
|
$header = fread($this->fh, 40); // В версии 2.2 заголовок увеличился на 8 байт |
59
|
|
|
if(substr($header, 0, 3) != 'SxG') die("Can't open {$db_file}\n"); |
|
|
|
|
60
|
|
|
$info = unpack('Cver/Ntime/Ctype/Ccharset/Cb_idx_len/nm_idx_len/nrange/Ndb_items/Cid_len/nmax_region/nmax_city/Nregion_size/Ncity_size/nmax_country/Ncountry_size/npack_size', substr($header, 3)); |
61
|
|
|
if($info['b_idx_len'] * $info['m_idx_len'] * $info['range'] * $info['db_items'] * $info['time'] * $info['id_len'] == 0) die("Wrong file format {$db_file}\n"); |
|
|
|
|
62
|
|
|
$this->range = $info['range']; |
63
|
|
|
$this->b_idx_len = $info['b_idx_len']; |
|
|
|
|
64
|
|
|
$this->m_idx_len = $info['m_idx_len']; |
65
|
|
|
$this->db_items = $info['db_items']; |
66
|
|
|
$this->id_len = $info['id_len']; |
|
|
|
|
67
|
|
|
$this->block_len = 3 + $this->id_len; |
|
|
|
|
68
|
|
|
$this->max_region = $info['max_region']; |
|
|
|
|
69
|
|
|
$this->max_city = $info['max_city']; |
|
|
|
|
70
|
|
|
$this->max_country = $info['max_country']; |
|
|
|
|
71
|
|
|
$this->country_size= $info['country_size']; |
72
|
|
|
$this->batch_mode = $type & SXGEO_BATCH; |
73
|
|
|
$this->memory_mode = $type & SXGEO_MEMORY; |
74
|
|
|
$this->pack = $info['pack_size'] ? explode("\0", fread($this->fh, $info['pack_size'])) : ''; |
|
|
|
|
75
|
|
|
$this->b_idx_str = fread($this->fh, $info['b_idx_len'] * 4); |
76
|
|
|
$this->m_idx_str = fread($this->fh, $info['m_idx_len'] * 4); |
77
|
|
|
|
78
|
|
|
$this->db_begin = ftell($this->fh); |
79
|
|
|
if ($this->batch_mode) { |
80
|
|
|
$this->b_idx_arr = array_values(unpack("N*", $this->b_idx_str)); // Быстрее в 5 раз, чем с циклом |
81
|
|
|
unset ($this->b_idx_str); |
82
|
|
|
$this->m_idx_arr = str_split($this->m_idx_str, 4); // Быстрее в 5 раз чем с циклом |
83
|
|
|
unset ($this->m_idx_str); |
84
|
|
|
} |
85
|
|
|
if ($this->memory_mode) { |
86
|
|
|
$this->db = fread($this->fh, $this->db_items * $this->block_len); |
87
|
|
|
$this->regions_db = $info['region_size'] > 0 ? fread($this->fh, $info['region_size']) : ''; |
88
|
|
|
$this->cities_db = $info['city_size'] > 0 ? fread($this->fh, $info['city_size']) : ''; |
89
|
|
|
} |
90
|
|
|
$this->info = $info; |
91
|
|
|
$this->info['regions_begin'] = $this->db_begin + $this->db_items * $this->block_len; |
92
|
|
|
$this->info['cities_begin'] = $this->info['regions_begin'] + $info['region_size']; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
protected function search_idx($ipn, $min, $max){ |
96
|
|
|
if($this->batch_mode){ |
97
|
|
|
while($max - $min > 8){ |
98
|
|
|
$offset = ($min + $max) >> 1; |
99
|
|
|
if ($ipn > $this->m_idx_arr[$offset]) $min = $offset; |
100
|
|
|
else $max = $offset; |
101
|
|
|
} |
102
|
|
|
while ($ipn > $this->m_idx_arr[$min] && $min++ < $max){}; |
|
|
|
|
103
|
|
|
} |
104
|
|
|
else { |
105
|
|
View Code Duplication |
while($max - $min > 8){ |
|
|
|
|
106
|
|
|
$offset = ($min + $max) >> 1; |
107
|
|
|
if ($ipn > substr($this->m_idx_str, $offset*4, 4)) $min = $offset; |
108
|
|
|
else $max = $offset; |
109
|
|
|
} |
110
|
|
|
while ($ipn > substr($this->m_idx_str, $min*4, 4) && $min++ < $max){}; |
|
|
|
|
111
|
|
|
} |
112
|
|
|
return $min; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
protected function search_db($str, $ipn, $min, $max){ |
116
|
|
|
if($max - $min > 1) { |
117
|
|
|
$ipn = substr($ipn, 1); |
118
|
|
View Code Duplication |
while($max - $min > 8){ |
|
|
|
|
119
|
|
|
$offset = ($min + $max) >> 1; |
120
|
|
|
if ($ipn > substr($str, $offset * $this->block_len, 3)) $min = $offset; |
121
|
|
|
else $max = $offset; |
122
|
|
|
} |
123
|
|
|
while ($ipn >= substr($str, $min * $this->block_len, 3) && ++$min < $max){}; |
|
|
|
|
124
|
|
|
} |
125
|
|
|
else { |
126
|
|
|
$min++; |
127
|
|
|
} |
128
|
|
|
return hexdec(bin2hex(substr($str, $min * $this->block_len - $this->id_len, $this->id_len))); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
public function get_num($ip){ |
132
|
|
|
$ip1n = (int)$ip; // Первый байт |
133
|
|
|
if($ip1n == 0 || $ip1n == 10 || $ip1n == 127 || $ip1n >= $this->b_idx_len || false === ($ipn = ip2long($ip))) return false; |
134
|
|
|
$ipn = pack('N', $ipn); |
135
|
|
|
$this->ip1c = chr($ip1n); |
136
|
|
|
// Находим блок данных в индексе первых байт |
137
|
|
|
if ($this->batch_mode){ |
138
|
|
|
$blocks = array('min' => $this->b_idx_arr[$ip1n-1], 'max' => $this->b_idx_arr[$ip1n]); |
139
|
|
|
} |
140
|
|
|
else { |
141
|
|
|
$blocks = unpack("Nmin/Nmax", substr($this->b_idx_str, ($ip1n - 1) * 4, 8)); |
142
|
|
|
} |
143
|
|
|
if ($blocks['max'] - $blocks['min'] > $this->range){ |
144
|
|
|
// Ищем блок в основном индексе |
145
|
|
|
$part = $this->search_idx($ipn, floor($blocks['min'] / $this->range), floor($blocks['max'] / $this->range)-1); |
146
|
|
|
// Нашли номер блока в котором нужно искать IP, теперь находим нужный блок в БД |
147
|
|
|
$min = $part > 0 ? $part * $this->range : 0; |
148
|
|
|
$max = $part > $this->m_idx_len ? $this->db_items : ($part+1) * $this->range; |
149
|
|
|
// Нужно проверить чтобы блок не выходил за пределы блока первого байта |
150
|
|
|
if($min < $blocks['min']) $min = $blocks['min']; |
151
|
|
|
if($max > $blocks['max']) $max = $blocks['max']; |
152
|
|
|
} |
153
|
|
|
else { |
154
|
|
|
$min = $blocks['min']; |
155
|
|
|
$max = $blocks['max']; |
156
|
|
|
} |
157
|
|
|
$len = $max - $min; |
158
|
|
|
// Находим нужный диапазон в БД |
159
|
|
|
if ($this->memory_mode) { |
160
|
|
|
return $this->search_db($this->db, $ipn, $min, $max); |
161
|
|
|
} |
162
|
|
|
else { |
163
|
|
|
fseek($this->fh, $this->db_begin + $min * $this->block_len); |
164
|
|
|
return $this->search_db(fread($this->fh, $len * $this->block_len), $ipn, 0, $len); |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
protected function readData($seek, $max, $type){ |
169
|
|
|
$raw = ''; |
170
|
|
|
if($seek && $max) { |
171
|
|
|
if ($this->memory_mode) { |
172
|
|
|
$raw = substr($type == 1 ? $this->regions_db : $this->cities_db, $seek, $max); |
173
|
|
|
} else { |
174
|
|
|
fseek($this->fh, $this->info[$type == 1 ? 'regions_begin' : 'cities_begin'] + $seek); |
175
|
|
|
$raw = fread($this->fh, $max); |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
return $this->unpack($this->pack[$type], $raw); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
protected function parseCity($seek, $full = false){ |
182
|
|
|
if(!$this->pack) return false; |
183
|
|
|
$only_country = false; |
184
|
|
|
if($seek < $this->country_size){ |
185
|
|
|
$country = $this->readData($seek, $this->max_country, 0); |
186
|
|
|
$city = $this->unpack($this->pack[2]); |
187
|
|
|
$city['lat'] = $country['lat']; |
188
|
|
|
$city['lon'] = $country['lon']; |
189
|
|
|
$only_country = true; |
190
|
|
|
} |
191
|
|
|
else { |
192
|
|
|
$city = $this->readData($seek, $this->max_city, 2); |
193
|
|
|
$country = array('id' => $city['country_id'], 'iso' => $this->id2iso[$city['country_id']]); |
194
|
|
|
unset($city['country_id']); |
195
|
|
|
} |
196
|
|
|
if($full) { |
197
|
|
|
$region = $this->readData($city['region_seek'], $this->max_region, 1); |
198
|
|
|
if(!$only_country) $country = $this->readData($region['country_seek'], $this->max_country, 0); |
199
|
|
|
unset($city['region_seek']); |
200
|
|
|
unset($region['country_seek']); |
201
|
|
|
return array('city' => $city, 'region' => $region, 'country' => $country); |
202
|
|
|
} |
203
|
|
|
else { |
204
|
|
|
unset($city['region_seek']); |
205
|
|
|
return array('city' => $city, 'country' => array('id' => $country['id'], 'iso' => $country['iso'])); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
protected function unpack($pack, $item = ''){ |
210
|
|
|
$unpacked = array(); |
211
|
|
|
$empty = empty($item); |
212
|
|
|
$pack = explode('/', $pack); |
213
|
|
|
$pos = 0; |
214
|
|
|
foreach($pack AS $p){ |
215
|
|
|
list($type, $name) = explode(':', $p); |
216
|
|
|
$type0 = $type{0}; |
217
|
|
|
if($empty) { |
218
|
|
|
$unpacked[$name] = $type0 == 'b' || $type0 == 'c' ? '' : 0; |
219
|
|
|
continue; |
220
|
|
|
} |
221
|
|
|
switch($type0){ |
222
|
|
|
case 't': |
223
|
|
|
case 'T': $l = 1; break; |
|
|
|
|
224
|
|
|
case 's': |
225
|
|
|
case 'n': |
226
|
|
|
case 'S': $l = 2; break; |
|
|
|
|
227
|
|
|
case 'm': |
228
|
|
|
case 'M': $l = 3; break; |
|
|
|
|
229
|
|
|
case 'd': $l = 8; break; |
|
|
|
|
230
|
|
|
case 'c': $l = (int)substr($type, 1); break; |
|
|
|
|
231
|
|
|
case 'b': $l = strpos($item, "\0", $pos)-$pos; break; |
|
|
|
|
232
|
|
|
default: $l = 4; |
|
|
|
|
233
|
|
|
} |
234
|
|
|
$val = substr($item, $pos, $l); |
235
|
|
|
switch($type0){ |
236
|
|
|
case 't': $v = unpack('c', $val); break; |
|
|
|
|
237
|
|
|
case 'T': $v = unpack('C', $val); break; |
|
|
|
|
238
|
|
|
case 's': $v = unpack('s', $val); break; |
|
|
|
|
239
|
|
|
case 'S': $v = unpack('S', $val); break; |
|
|
|
|
240
|
|
|
case 'm': $v = unpack('l', $val . (ord($val{2}) >> 7 ? "\xff" : "\0")); break; |
|
|
|
|
241
|
|
|
case 'M': $v = unpack('L', $val . "\0"); break; |
|
|
|
|
242
|
|
|
case 'i': $v = unpack('l', $val); break; |
|
|
|
|
243
|
|
|
case 'I': $v = unpack('L', $val); break; |
|
|
|
|
244
|
|
|
case 'f': $v = unpack('f', $val); break; |
|
|
|
|
245
|
|
|
case 'd': $v = unpack('d', $val); break; |
|
|
|
|
246
|
|
|
|
247
|
|
|
case 'n': $v = current(unpack('s', $val)) / pow(10, $type{1}); break; |
|
|
|
|
248
|
|
|
case 'N': $v = current(unpack('l', $val)) / pow(10, $type{1}); break; |
|
|
|
|
249
|
|
|
|
250
|
|
|
case 'c': $v = rtrim($val, ' '); break; |
|
|
|
|
251
|
|
|
case 'b': $v = $val; $l++; break; |
|
|
|
|
252
|
|
|
} |
253
|
|
|
$pos += $l; |
254
|
|
|
$unpacked[$name] = is_array($v) ? current($v) : $v; |
255
|
|
|
} |
256
|
|
|
return $unpacked; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
public function get($ip){ |
260
|
|
|
return $this->max_city ? $this->getCity($ip) : $this->getCountry($ip); |
261
|
|
|
} |
262
|
|
View Code Duplication |
public function getCountry($ip){ |
263
|
|
|
if($this->max_city) { |
264
|
|
|
$tmp = $this->parseCity($this->get_num($ip)); |
265
|
|
|
return $tmp['country']['iso']; |
266
|
|
|
} |
267
|
|
|
else return $this->id2iso[$this->get_num($ip)]; |
268
|
|
|
} |
269
|
|
View Code Duplication |
public function getCountryId($ip){ |
270
|
|
|
if($this->max_city) { |
271
|
|
|
$tmp = $this->parseCity($this->get_num($ip)); |
272
|
|
|
return $tmp['country']['id']; |
273
|
|
|
} |
274
|
|
|
else return $this->get_num($ip); |
275
|
|
|
} |
276
|
|
|
public function getCity($ip){ |
277
|
|
|
$seek = $this->get_num($ip); |
278
|
|
|
return $seek ? $this->parseCity($seek) : false; |
279
|
|
|
} |
280
|
|
|
public function getCityFull($ip){ |
281
|
|
|
$seek = $this->get_num($ip); |
282
|
|
|
return $seek ? $this->parseCity($seek, 1) : false; |
|
|
|
|
283
|
|
|
} |
284
|
|
|
public function about(){ |
285
|
|
|
$charset = array('utf-8', 'latin1', 'cp1251'); |
286
|
|
|
$types = array('n/a', 'SxGeo Country', 'SxGeo City RU', 'SxGeo City EN', 'SxGeo City', 'SxGeo City Max RU', 'SxGeo City Max EN', 'SxGeo City Max'); |
287
|
|
|
return array( |
288
|
|
|
'Created' => date('Y.m.d', $this->info['time']), |
289
|
|
|
'Timestamp' => $this->info['time'], |
290
|
|
|
'Charset' => $charset[$this->info['charset']], |
291
|
|
|
'Type' => $types[$this->info['type']], |
292
|
|
|
'Byte Index' => $this->b_idx_len, |
293
|
|
|
'Main Index' => $this->m_idx_len, |
294
|
|
|
'Blocks In Index Item' => $this->range, |
295
|
|
|
'IP Blocks' => $this->db_items, |
296
|
|
|
'Block Size' => $this->block_len, |
297
|
|
|
'City' => array( |
298
|
|
|
'Max Length' => $this->max_city, |
299
|
|
|
'Total Size' => $this->info['city_size'], |
300
|
|
|
), |
301
|
|
|
'Region' => array( |
302
|
|
|
'Max Length' => $this->max_region, |
303
|
|
|
'Total Size' => $this->info['region_size'], |
304
|
|
|
), |
305
|
|
|
'Country' => array( |
306
|
|
|
'Max Length' => $this->max_country, |
307
|
|
|
'Total Size' => $this->info['country_size'], |
308
|
|
|
), |
309
|
|
|
); |
310
|
|
|
} |
311
|
|
|
} |
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exit
expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.