Issues (1482)

src/service/QqWryService.php (9 issues)

1
<?php
2
3
// +----------------------------------------------------------------------
4
// | ThinkLibrary 6.0 for ThinkPhP 6.0
5
// +----------------------------------------------------------------------
6
// | 版权所有 2017~2020 [ https://www.dtapp.net ]
7
// +----------------------------------------------------------------------
8
// | 官方网站: https://gitee.com/liguangchun/ThinkLibrary
9
// +----------------------------------------------------------------------
10
// | 开源协议 ( https://mit-license.org )
11
// +----------------------------------------------------------------------
12
// | gitee 仓库地址 :https://gitee.com/liguangchun/ThinkLibrary
13
// | github 仓库地址 :https://github.com/GC0202/ThinkLibrary
14
// | Packagist 地址 :https://packagist.org/packages/liguangchun/think-library
15
// +----------------------------------------------------------------------
16
17
namespace DtApp\ThinkLibrary\service;
18
19
use DtApp\ThinkLibrary\exception\DtaException;
20
use DtApp\ThinkLibrary\Service;
21
use think\App;
22
23
/**
24
 * 纯真数据库
25
 * Class QqWryService
26
 * @package DtApp\ThinkLibrary\service
27
 */
28
class QqWryService extends Service
29
{
30
    /**
31
     * QQWry.Dat文件指针
32
     * @var resource
33
     */
34
    private $fp;
35
36
    /**
37
     * 第一条IP记录的偏移地址
38
     *
39
     * @var int
40
     */
41
    private $firstIp;
42
43
    /**
44
     * 最后一条IP记录的偏移地址
45
     * @var int
46
     */
47
    private $lastIp;
48
49
    /**
50
     * IP记录的总条数(不包含版本信息记录)
51
     * @var int
52
     */
53
    private $totalIp;
54
55
    /**
56
     * 不存在
57
     * @var string
58
     */
59
    private $unknown = '未知';
60
61
    /**
62
     * 构造函数,打开 QQWry.Dat 文件并初始化类中的信息
63
     * @param App $app
64
     */
65
    public function __construct(App $app)
66
    {
67
        $this->fp = 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like 0 of type integer is incompatible with the declared type resource of property $fp.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
68
        if (($this->fp = fopen(__DIR__ . '/bin/qqwry.dat', 'rb')) !== false) {
69
            $this->firstIp = $this->getLong();
70
            $this->lastIp = $this->getLong();
71
            $this->totalIp = ($this->lastIp - $this->firstIp) / 7;
72
        }
73
        parent::__construct($app);
74
    }
75
76
    /**
77
     * 设置未知的返回字段
78
     * @param string $unknown
79
     * @return QqWryService
80
     */
81
    public function setUnknown(string $unknown = '未知')
82
    {
83
        $this->unknown = $unknown;
84
        return $this;
85
    }
86
87
    /**
88
     * 获取省信息
89
     * @param string $ip
90
     * @return mixed
91
     * @throws DtaException
92
     */
93
    public function getProvince(string $ip = '')
94
    {
95
        return $this->getLocation($ip)['state'];
96
    }
97
98
    /**
99
     * 获取城市信息
100
     * @param string $ip
101
     * @return mixed
102
     * @throws DtaException
103
     */
104
    public function getCity(string $ip = '')
105
    {
106
        return $this->getLocation($ip)['city'];
107
    }
108
109
    /**
110
     * 获取地区信息
111
     * @param string $ip
112
     * @return mixed
113
     * @throws DtaException
114
     */
115
    public function getArea(string $ip = '')
116
    {
117
        return $this->getLocation($ip)['area'];
118
    }
119
120
    /**
121
     * 获取运营商信息
122
     * @param string $ip
123
     * @return mixed
124
     * @throws DtaException
125
     */
126
    public function getExtend(string $ip = '')
127
    {
128
        return $this->getLocation($ip)['extend'];
129
    }
130
131
    /**
132
     * 根据所给 IP 地址或域名返回所在地区信息
133
     * @param string $ip
134
     * @return mixed|null
135
     * @throws DtaException
136
     */
137
    public function getLocation(string $ip = '')
138
    {
139
        if (empty($ip)) {
140
            $ip = get_ip();
141
        }
142
        if (strpos($ip, 'http://') === 0) {
143
            $ip = substr($ip, 7);
144
            $ip = gethostbyname($ip);
145
        }
146
        static $locationData = [];
147
        if (!isset($locationData[$ip])) {
148
            if (!$this->fp) {
149
                // 如果数据文件没有被正确打开,则直接返回错误
150
                throw new DtaException('数据库文件不存在!');
151
            }
152
            $location['ip'] = $ip;   // 将输入的域名转化为IP地址
0 ignored issues
show
Comprehensibility Best Practice introduced by
$location was never initialized. Although not strictly required by PHP, it is generally a good practice to add $location = array(); before regardless.
Loading history...
153
            $ip = $this->packIp($location['ip']);   // 将输入的IP地址转化为可比较的IP地址
154
            // 不合法的IP地址会被转化为255.255.255.255
155
            // 对分搜索
156
            $l = 0;                         // 搜索的下边界
157
            $u = $this->totalIp;            // 搜索的上边界
158
            $findip = $this->lastIp;        // 如果没有找到就返回最后一条IP记录(QQWry.Dat的版本信息)
159
            while ($l <= $u) {              // 当上边界小于下边界时,查找失败
160
                $i = floor(($l + $u) / 2);  // 计算近似中间记录
161
                fseek($this->fp, $this->firstIp + $i * 7);
0 ignored issues
show
$this->firstIp + $i * 7 of type double is incompatible with the type integer expected by parameter $offset of fseek(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

161
                fseek($this->fp, /** @scrutinizer ignore-type */ $this->firstIp + $i * 7);
Loading history...
162
                $beginip = strrev(fread($this->fp, 4));     // 获取中间记录的开始IP地址
163
                // strrev函数在这里的作用是将little-endian的压缩IP地址转化为big-endian的格式
164
                // 以便用于比较,后面相同。
165
                if ($ip < $beginip) {       // 用户的IP小于中间记录的开始IP地址时
166
                    $u = $i - 1;            // 将搜索的上边界修改为中间记录减一
167
                } else {
168
                    fseek($this->fp, $this->getLong3());
169
                    $endip = strrev(fread($this->fp, 4));   // 获取中间记录的结束IP地址
170
                    if ($ip > $endip) {     // 用户的IP大于中间记录的结束IP地址时
171
                        $l = $i + 1;        // 将搜索的下边界修改为中间记录加一
172
                    } else {                  // 用户的IP在中间记录的IP范围内时
173
                        $findip = $this->firstIp + $i * 7;
174
                        break;              // 则表示找到结果,退出循环
175
                    }
176
                }
177
            }
178
            //获取查找到的IP地理位置信息
179
            fseek($this->fp, $findip);
180
            $location['beginip'] = long2ip($this->getLong());   // 用户IP所在范围的开始地址
181
            $offset = $this->getLong3();
182
            fseek($this->fp, $offset);
183
            $location['endip'] = long2ip($this->getLong());     // 用户IP所在范围的结束地址
184
            $byte = fread($this->fp, 1);    // 标志字节
185
            switch (ord($byte)) {
186
                case 1:                     // 标志字节为1,表示国家和区域信息都被同时重定向
187
                    $countryOffset = $this->getLong3();         // 重定向地址
188
                    fseek($this->fp, $countryOffset);
189
                    $byte = fread($this->fp, 1);    // 标志字节
190
                    switch (ord($byte)) {
191
                        case 2:             // 标志字节为2,表示国家信息又被重定向
192
                            fseek($this->fp, $this->getLong3());
193
                            $location['all'] = $this->getString();
194
                            fseek($this->fp, $countryOffset + 4);
195
                            $location['extend'] = $this->getExtendString();
196
                            break;
197
                        default:            // 否则,表示国家信息没有被重定向
198
                            $location['all'] = $this->getString($byte);
199
                            $location['extend'] = $this->getExtendString();
200
                            break;
201
                    }
202
                    break;
203
                case 2:                     // 标志字节为2,表示国家信息被重定向
204
                    fseek($this->fp, $this->getLong3());
205
                    $location['all'] = $this->getString();
206
                    fseek($this->fp, $offset + 8);
207
                    $location['extend'] = $this->getExtendString();
208
                    break;
209
                default:                    // 否则,表示国家信息没有被重定向
210
                    $location['all'] = $this->getString($byte);
211
                    $location['extend'] = $this->getExtendString();
212
                    break;
213
            }
214
            // CZ88.NET表示没有有效信息
215
            if (trim($location['all']) === 'CZ88.NET') {
216
                $location['all'] = $this->unknown;
217
            }
218
            if (trim($location['extend']) === 'CZ88.NET') {
219
                $location['extend'] = '';
220
            }
221
            $location['all'] = iconv("gb2312", "UTF-8//IGNORE", $location['all']);
222
            $location['extend'] = iconv("gb2312", "UTF-8//IGNORE", $location['extend']);
223
            $location['extend'] = $location['extend'] ?? '';
224
            $parseData = $this->parseLocation($location['all']);
225
            $location['state'] = $parseData[0];
226
            $location['city'] = $parseData[1];
227
            $location['area'] = $parseData[2];
228
229
            // 全部地址
230
            $res['location_all'] = $location['all'];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$res was never initialized. Although not strictly required by PHP, it is generally a good practice to add $res = array(); before regardless.
Loading history...
231
            // 运营商
232
            $res['isp']['name'] = $location['extend'];
233
            // IP
234
            $res['ip']['ipv4'] = $location['ip'];
235
            $res['ip']['beginip'] = $location['beginip'];
236
            $res['ip']['endip'] = $location['endip'];
237
            $res['ip']['trueip'] = ip2long($location['ip']);
238
            $res['ip']['ipv6'] = $this->getNormalizedIP($location['ip']);
239
            $getAdCodeLatLng = $this->getNameAdCodeLatLng($location['state'], $location['city'], $location['area']);
240
            // 省份
241
            $res['province'] = $getAdCodeLatLng['province'];
242
            // 城市
243
            $res['city'] = $getAdCodeLatLng['city'];
244
            // 地区
245
            $res['district'] = $getAdCodeLatLng['district'];
246
            $locationData[$ip] = $location;
247
        }
248
        return $res;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $res does not seem to be defined for all execution paths leading up to this point.
Loading history...
249
    }
250
251
    /**
252
     * 解析省市区县
253
     * @param $location
254
     * @return array
255
     * @example '江苏省苏州市吴江市' , '江苏省苏州市吴中区' , '江苏省苏州市昆山市' , '黑龙江省鸡西市' , '广西桂林市' , '陕西省西安市户县' , '河南省开封市通许县' ,'内蒙古呼伦贝尔市海拉尔区','甘肃省白银市平川区','孟加拉','上海市' , '北京市朝阳区' ,'美国' ,'香港' ,  俄罗斯' ,'IANA'
256
     */
257
    private function parseLocation($location)
258
    {
259
        $state = $city = $area = $this->unknown;
260
        if (preg_match('/^(.+省)?(新疆|内蒙古|宁夏|西藏|广西|香港|澳门)?(.+市)?(.+市)?(.+(县|区))?/', $location, $preg)) {
261
            if (count($preg) == 4) {        //匹配 "浙江省杭州市"
262
                $state = $preg[1] ? $preg[1] : ($preg[2] ? $preg[2] : $preg[3]);
263
                $city = $preg[3];
264
            } else if (count($preg) == 7) { //匹配 "浙江省杭州市江干区"
265
                $state = $preg[1] ? $preg[1] : ($preg[2] ? $preg[2] : $preg[3]);
266
                $city = $preg[3];
267
                $area = $preg[5];
268
            } else if (count($preg) == 3) { //匹配 "香港"
269
                $state = $preg[1] ? $preg[1] : $preg[2];
270
                $city = $state;
271
            } else if (count($preg) == 2) {  //匹配 "浙江省"
272
                $state = $preg[1] ? $preg[1] : $this->unknown;
273
            }
274
        }
275
        return [$state, $city, $area];
276
    }
277
278
    /**
279
     * 返回读取的长整型数
280
     * @return mixed
281
     */
282
    private function getLong()
283
    {
284
        //将读取的little-endian编码的4个字节转化为长整型数
285
        $result = unpack('Vlong', fread($this->fp, 4));
286
        return $result['long'];
287
    }
288
289
    /**
290
     * 返回读取的3个字节的长整型数
291
     * @return mixed
292
     */
293
    private function getLong3()
294
    {
295
        //将读取的little-endian编码的3个字节转化为长整型数
296
        $result = unpack('Vlong', fread($this->fp, 3) . chr(0));
297
        return $result['long'];
298
    }
299
300
    /**
301
     * 返回压缩后可进行比较的IP地址
302
     * @param $ip
303
     * @return false|string
304
     */
305
    private function packIp($ip)
306
    {
307
        // 将IP地址转化为长整型数,如果在PHP5中,IP地址错误,则返回False,
308
        // 这时intval将Flase转化为整数-1,之后压缩成big-endian编码的字符串
309
        return pack('N', intval(ip2long($ip)));
310
    }
311
312
    /**
313
     * 返回读取的字符串
314
     *
315
     * @access private
316
     * @param string $data
317
     * @return string
318
     */
319
    private function getString($data = "")
320
    {
321
        $char = fread($this->fp, 1);
322
        while (ord($char) > 0) {        // 字符串按照C格式保存,以\0结束
323
            $data .= $char;             // 将读取的字符连接到给定字符串之后
324
            $char = fread($this->fp, 1);
325
        }
326
        return $data;
327
    }
328
329
    /**
330
     * 返回地区信息
331
     * @return string
332
     */
333
    private function getExtendString()
334
    {
335
        $byte = fread($this->fp, 1);    // 标志字节
336
        switch (ord($byte)) {
337
            case 0:                     // 没有区域信息
338
                $area = "";
339
                break;
340
            case 1:
341
            case 2:                     // 标志字节为1或2,表示区域信息被重定向
342
                fseek($this->fp, $this->getLong3());
343
                $area = $this->getString();
344
                break;
345
            default:                    // 否则,表示区域信息没有被重定向
346
                $area = $this->getString($byte);
347
                break;
348
        }
349
        return $area;
350
    }
351
352
    /**
353
     * 析构函数,用于在页面执行结束后自动关闭打开的文件。
354
     */
355
    public function __destruct()
356
    {
357
        if ($this->fp) {
358
            fclose($this->fp);
359
        }
360
        $this->fp = 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like 0 of type integer is incompatible with the declared type resource of property $fp.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
361
    }
362
363
    /**
364
     * ipv4转换ipv6
365
     * @param $ip
366
     * @return bool|false|string|string[]|null
367
     */
368
    protected function getNormalizedIP($ip)
369
    {
370
        if (!is_string($ip)) {
371
            return '';
372
        }
373
        if (preg_match('%^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$%', $ip, $match)) {
374
            $IPParts = array();
375
            for ($i = 1; $i <= 4; $i++) {
376
                $IPPart = (int)$match[$i];
377
                if ($IPPart > 255) {
378
                    return '';
379
                }
380
                $IPParts[$i] = str_pad(decHex($IPPart), 2, '0', STR_PAD_LEFT);
381
            }
382
            return '0000:0000:0000:0000:0000:ffff:' . $IPParts[1] . $IPParts[2] . ':' . $IPParts[3] . $IPParts[4];
383
        }
384
        return '';
385
    }
386
387
    /**
388
     * 解析CODE
389
     * @param $province_name
390
     * @param $city_name
391
     * @param $district_name
392
     * @return array
393
     */
394
    private function getNameAdCodeLatLng($province_name, $city_name, $district_name)
395
    {
396
        // 名称
397
        $province['name'] = $province_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$province was never initialized. Although not strictly required by PHP, it is generally a good practice to add $province = array(); before regardless.
Loading history...
398
        $city['name'] = $city_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$city was never initialized. Although not strictly required by PHP, it is generally a good practice to add $city = array(); before regardless.
Loading history...
399
        $district['name'] = $district_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$district was never initialized. Although not strictly required by PHP, it is generally a good practice to add $district = array(); before regardless.
Loading history...
400
        // adcode
401
        $province['adcode'] = '';
402
        $city['adcode'] = '';
403
        $district['adcode'] = '';
404
        // lat
405
        $province['lat'] = '';
406
        $city['lat'] = '';
407
        $district['lat'] = '';
408
        // lng
409
        $province['lng'] = '';
410
        $city['lng'] = '';
411
        $district['lng'] = '';
412
413
        if (!empty($province_name)) {
414
            $json_province = json_decode(file_get_contents(__DIR__ . '/bin/province.json'), true);
415
            foreach ($json_province['rows'] as $key => $value) {
416
                if ($value['name'] == $province_name) {
417
                    $province['name'] = $value['name'];
418
                    $province['adcode'] = $value['adcode'];
419
                    $province['lat'] = $value['lat'];
420
                    $province['lng'] = $value['lng'];
421
                }
422
            }
423
        }
424
        if (!empty($city_name)) {
425
            $json_city = json_decode(file_get_contents(__DIR__ . '/bin/city.json'), true);
426
            foreach ($json_city['rows'] as $key => $value) {
427
                if ($value['name'] == $city_name) {
428
                    $city['name'] = $value['name'];
429
                    $city['adcode'] = $value['adcode'];
430
                    $city['lat'] = $value['lat'];
431
                    $city['lng'] = $value['lng'];
432
                }
433
            }
434
        }
435
        if (!empty($district_name)) {
436
            $json_district = json_decode(file_get_contents(__DIR__ . '/bin/district.json'), true);
437
            foreach ($json_district['rows'] as $key => $value) {
438
                if ($value['name'] == $district_name) {
439
                    $district['name'] = $value['name'];
440
                    $district['adcode'] = $value['adcode'];
441
                    $district['lat'] = $value['lat'];
442
                    $district['lng'] = $value['lng'];
443
                }
444
            }
445
        }
446
        return [
447
            'province' => $province,
448
            'city' => $city,
449
            'district' => $district
450
        ];
451
    }
452
}
453