IpLocation.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. <?php
  2. namespace Sakura\API;
  3. /**
  4. * 获取IP地理位置
  5. */
  6. class IpLocation
  7. {
  8. //要查询的IP地址
  9. private $ip;
  10. //国家
  11. private $country;
  12. //地区
  13. private $region;
  14. //城市
  15. private $city;
  16. public function __construct(string $ip)
  17. {
  18. $this->ip = $ip;
  19. }
  20. /**
  21. * 通过Sakurario IP地址解析接口获取地理位置
  22. *
  23. * @return boolean 成功返回true,失败返回false
  24. */
  25. private function getIpLocationBySakurairo()
  26. {
  27. if (empty($this->ip)) {
  28. return false;
  29. }
  30. $url = "https://api.nmxc.ltd/ip/$this->ip";
  31. $response = wp_remote_get($url);
  32. // 检查响应
  33. if (is_wp_error($response)) {
  34. $errorMessage = $response->get_error_message();
  35. trigger_error('通过Sakurairo获取IP地理位置失败:' . $errorMessage, E_USER_WARNING);
  36. return false;
  37. } else {
  38. // 处理响应数据
  39. $body = json_decode(wp_remote_retrieve_body($response), true);
  40. if (!empty($body)) {
  41. if (isset($body['status']) && $body['status'] == 500) {
  42. $message = isset($body['status'])? $body['status'] : '';
  43. trigger_error("通过Sakurairo获取IP地理位置失败:$message", E_USER_WARNING);
  44. return false;
  45. } else {
  46. $data = $body['data'];
  47. $this->country = $data['country'] ? $data['country'] : '';
  48. $this->region = $data['region'] ? $data['region'] : '';
  49. $this->city = $data['city'] ? $data['city'] : '';
  50. return true;
  51. }
  52. } else {
  53. trigger_error('通过Sakurairo获取IP地理位置失败:返回的数据不是json格式', E_USER_WARNING);
  54. return false;
  55. }
  56. }
  57. }
  58. /**
  59. * 通过IP-API接口获取IP地理位置
  60. * 接口文档:https://ip-api.com/docs/api:json
  61. *
  62. * @return boolean 成功返回true,失败返回false
  63. */
  64. private function getIpLocationByIpApi ()
  65. {
  66. if (empty($this->ip)) {
  67. return false;
  68. }
  69. // 检查速率限制
  70. $isLimit = get_transient('ip_location_rate_limit');
  71. if ($isLimit) {
  72. trigger_error('通过IP-API获取IP地理位置:获取失败,超过接口速率限制', E_USER_WARNING);
  73. return false;
  74. }
  75. // IP-API支持的语言
  76. $languages = array(
  77. 'zh_CN' => 'zh-CN',
  78. 'en' => 'en',
  79. 'de' => 'de',
  80. 'es' => 'es',
  81. 'pt_BR' => 'pt-BR',
  82. 'ja' => 'ja',
  83. 'fr' => 'fr',
  84. 'ru' => 'ru'
  85. );
  86. // 获取WordPress语言用于本地化
  87. $lang = get_locale();
  88. $lang = isset($languages[$lang])? $languages[$lang] : 'en';
  89. // 定义需要获取哪些信息,用法见接口文档
  90. $fields = '49177';
  91. $url = "http://ip-api.com/json/$this->ip?fields=$fields&lang=$lang";
  92. $response = wp_remote_get($url);
  93. // 检查响应
  94. if (is_wp_error($response)) {
  95. $errorMessage = $response->get_error_message();
  96. trigger_error('通过IP-API获取IP地理位置失败:' . $errorMessage, E_USER_WARNING);
  97. return false;
  98. } else {
  99. $headers = wp_remote_retrieve_headers($response);
  100. // 请求剩余次数
  101. $remainingAmount = $headers['X-Rl'];
  102. // 次数重置剩余时间
  103. $resetTime = $headers['X-Ttl'];
  104. // 防止超过速率限制
  105. if ($remainingAmount <= 2) {
  106. set_transient('ip_location_rate_limit', 'is_limit', $resetTime);
  107. }
  108. // 处理响应数据
  109. $data = json_decode(wp_remote_retrieve_body($response), true);
  110. if (!empty($data)) {
  111. if ($data['status'] == 'success') {
  112. $this->country = $data['country'] ? $data['country'] : '';
  113. $this->region = $data['regionName'] ? $data['regionName'] : '';
  114. $this->city = $data['city'] ? $data['city'] : '';
  115. return true;
  116. } else {
  117. $message = $data['message'];
  118. trigger_error("通过IP-API获取IP地理位置失败:$message", E_USER_WARNING);
  119. return false;
  120. }
  121. } else {
  122. trigger_error('通过IP-API获取IP地理位置失败:返回的数据不是json格式', E_USER_WARNING);
  123. return false;
  124. }
  125. }
  126. }
  127. /**
  128. * 输出地理位置信息
  129. *
  130. * @return array 地理位置信息数组
  131. */
  132. private function outputLocation()
  133. {
  134. $location = array(
  135. 'country' => $this->country,
  136. 'region' => $this->region,
  137. 'city' => $this->city
  138. );
  139. return $location;
  140. }
  141. /**
  142. * 检查IP地理位置信息的字段是否都是完整的
  143. *
  144. * @param array $data 地理位置信息数组
  145. * @return boolean true,完整;false,不完整
  146. */
  147. private function checkCompleteness(array $data)
  148. {
  149. $dataFilter = array_filter($data);
  150. if (count($dataFilter) === count($data)) {
  151. return true;
  152. } else {
  153. return false;
  154. }
  155. }
  156. /**
  157. * 检查IP地址的合法性(排除空地址、非法地址以及私有地址和保留地址)
  158. *
  159. * @param string $ip 待检查的IP地址
  160. * @return boolean true,合法;false,非法
  161. */
  162. public static function checkIpValid(string $ip)
  163. {
  164. if (empty($ip)) {
  165. return false;
  166. }
  167. // 检查是否是合法的 IPv4 或 IPv6 地址
  168. if (!filter_var($ip, FILTER_VALIDATE_IP)) {
  169. return false;
  170. }
  171. // 检查是否是保留地址
  172. if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
  173. return true;
  174. } else {
  175. return false;
  176. }
  177. }
  178. /**
  179. * 获取IP地址的地理位置信息
  180. *
  181. * @return mixed 成功时返回地理位置信息数组array('country' => '国家','region' => '地区(省份)','city' => '城市');失败时返回false
  182. */
  183. public function getLocation()
  184. {
  185. // 检查IP地址的合法性
  186. if (!static::checkIpValid($this->ip)) {
  187. trigger_error('获取IP地理位置失败:不是有效的IP地址', E_USER_WARNING);
  188. return false;
  189. }
  190. $server = iro_opt('location_server');
  191. switch ($server) {
  192. case 'sakurairo':
  193. $this->getIpLocationBySakurairo();
  194. break;
  195. case 'ip-api':
  196. $this->getIpLocationByIpApi();
  197. break;
  198. case 'all':
  199. $isSakurairo = $this->getIpLocationBySakurairo();
  200. if (!$isSakurairo || !$this->checkCompleteness($this->outputLocation())) {
  201. $this->getIpLocationByIpApi();
  202. }
  203. break;
  204. default:
  205. break;
  206. }
  207. $data = $this->outputLocation();
  208. if ($this->checkCompleteness($data)) {
  209. return $data;
  210. } else {
  211. trigger_error('获取IP地理位置失败', E_USER_WARNING);
  212. return false;
  213. }
  214. }
  215. }
  216. /**
  217. * IP地理位置解析输出
  218. */
  219. class IpLocationParse
  220. {
  221. //国家
  222. public $country;
  223. //地区
  224. public $region;
  225. //城市
  226. public $city;
  227. public function __construct(array $data)
  228. {
  229. $this->country = $data['country'];
  230. $this->region = $data['region'];
  231. $this->city = $data['city'];
  232. }
  233. /**
  234. * 通过HTML格式输出IP地理位置信息
  235. *
  236. * @return string HTML格式地理位置信息
  237. */
  238. public function getLocationHtml()
  239. {
  240. $html = '';
  241. $html.= '<div class="ip-location">';
  242. $html.= '<div class="ip-location-country">'.$this->country.'</div>';
  243. $html.= '<div class="ip-location-region">'.$this->region.'</div>';
  244. $html.= '<div class="ip-location-city">'.$this->city.'</div>';
  245. $html.= '</div>';
  246. return $html;
  247. }
  248. /**
  249. * 获取简洁的IP地址地理信息
  250. *
  251. * @return string “国家 地区(省份) 城市”
  252. */
  253. public function getLocationConcision()
  254. {
  255. return $this->country.' '.$this->region.' '.$this->city;
  256. }
  257. /**
  258. * 通过评论ID获取IP地理位置信息,当数据库里不存在IP地理位置信息时会自动请求接口获取
  259. *
  260. * @param int $comment_id 评论ID
  261. * @return string 成功时返回IP地理位置信息:“国家 地区(省份) 城市”;失败时返回“Unknown”或“Reserved Address”或“Empty Address”
  262. */
  263. public static function getIpLocationByCommentId(int $commentId)
  264. {
  265. $ipLocation = get_comment_meta($commentId, 'iro_ip_location', true);
  266. if ($ipLocation) {
  267. $location = new IpLocationParse($ipLocation);
  268. return $location->getLocationConcision();
  269. } else {
  270. // 解析IP地址地理位置
  271. $commentIp = get_comment_author_IP($commentId);
  272. if (!empty($commentIp)) {
  273. if (IPLocation::checkIpValid($commentIp)) {
  274. $ipLocation = new IPLocation($commentIp);
  275. $location = $ipLocation->getLocation();
  276. // 记录IP地理位置信息
  277. if ($location) {
  278. if (iro_opt('save_location')) add_comment_meta($commentId, 'iro_ip_location', $location);
  279. $locationParse = new IpLocationParse($location);
  280. return $locationParse->getLocationConcision();
  281. } else {
  282. return __('Unknown');
  283. }
  284. } else {
  285. return __('Reserved Address');
  286. }
  287. } else {
  288. return __('Empty Address');
  289. }
  290. }
  291. }
  292. /**
  293. * 获取单个IP地址地理位置信息
  294. *
  295. * @param string $ip IP地址
  296. * @return string 成功时返回IP地理位置信息:“国家 地区(省份) 城市”;失败时返回“Unknown”或“Reserved Address”或“Empty Address”
  297. */
  298. public static function getIpLocationByIp(string $ip)
  299. {
  300. if (empty($ip)) {
  301. return __('Empty Address');
  302. }
  303. if (IPLocation::checkIpValid($ip)) {
  304. $ipLocation = new IPLocation($ip);
  305. $location = $ipLocation->getLocation();
  306. if ($location) {
  307. $locationParse = new IpLocationParse($location);
  308. return $locationParse->getLocationConcision();
  309. } else {
  310. return __('Unknown');
  311. }
  312. } else {
  313. return __('Reserved Address');
  314. }
  315. }
  316. }