惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Palo Alto Networks Blog
AWS News Blog
AWS News Blog
Latest news
Latest news
Microsoft Security Blog
Microsoft Security Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Vulnerabilities – Threatpost
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
C
CERT Recently Published Vulnerability Notes
T
Tenable Blog
雷峰网
雷峰网
K
Kaspersky official blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
L
Lohrmann on Cybersecurity
T
The Exploit Database - CXSecurity.com
T
Tor Project blog
A
Arctic Wolf
Project Zero
Project Zero
F
Fortinet All Blogs
T
Threatpost
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
SegmentFault 最新的问题
P
Privacy & Cybersecurity Law Blog
D
DataBreaches.Net
量子位
Cisco Talos Blog
Cisco Talos Blog
博客园 - 【当耐特】
V
Visual Studio Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园 - 叶小钗
I
Intezer
G
GRAHAM CLULEY
Google Online Security Blog
Google Online Security Blog
T
The Blog of Author Tim Ferriss
T
Troy Hunt's Blog
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
C
Cyber Attacks, Cyber Crime and Cyber Security
Hacker News: Ask HN
Hacker News: Ask HN
M
MIT News - Artificial intelligence
N
News | PayPal Newsroom
Cyberwarzone
Cyberwarzone
V
V2EX
腾讯CDC
Webroot Blog
Webroot Blog
MongoDB | Blog
MongoDB | Blog

李锋镝的博客

LiteLLM 本地代理搭建 Claude-HUD 使用文档 Kratos+ —— Kratos 主题二次开发记录 译文:如何将单体应用拆解为微服务 codebase-memory-mcp 极简完整使用指南 Claude Haiku 4.5、Claude Sonnet 4.6、Claude Opus 4.7 区别以及各自的新特性 SchedulingConfigurer详解 踩坑60+次后,我终于搞懂 Claude Skill 怎么写才会真的触发 Everything Claude Code 详细使用文档 配置Jackson使用字段而不是getter/setter来序列化和反序列化 这个域名注册整整十年了,十年时间,真快啊 Claude Code全维度实战指南:从入门到精通,解锁AI编程新范式 Apollo配置中心中的protalDB的作用是什么 org.apache.ibatis.plugin.Interceptor类详细介绍及使用 岁末 Excel2016右键新建工作表,打开时提示“因为文件格式或文件扩展名无效。请确定文件未损坏,并且文件扩展名与文件的格式匹配。”的解决办法 wordpress增加说说功能 Java 为什么有这么多 “O”? 别再背线程池的七大参数了,现在面试官都这么问 2024年11月1号 农历十月初一 我的第一个WordPress插件:Dylan Custom Plugin上线了 推荐一款比较养眼的Xshell配色方案 hnswlib installation failed 开工啦~ 阳了... MybatisCodeHelperPro激活 @Async注解的坑 新买的笔记本发货啦…… 这个中秋节感觉过的好累啊 IDEA下载源码报:Cannot connect to the Maven process. Try again later. RocketMQ的push消费方式实现详解 看病难~取药难~~ 笑死、腹肌……根本不可能有腹肌的~~ 居家办公了~ C# 11 的这个新特性,我愿称之最强! IntelliJ IDEA 2020.3.x永久白嫖(Windows/Mac) IDEA无限试用方法【2020.3最新亲测有效】 SpringBoot使用注解的方式构建Elasticsearch查询语句,实现多条件的复杂查询 UUID太长怎么办?快来试试NanoId 忽然发现,在校大学生可以免费领一年有道云笔记会员~ 醒醒~补个税了 使用itext和freemarker来根据Html模板生成PDF文件,加水印、印章 来来来,用python画一个冰墩墩儿 SpringBoot整合GraphQL入门教程 办理居住证困难重重啊! 妈呀,昨天晚上睡觉做了一晚上的梦,可累死我了 感觉Typecho很简洁啊…… WordPress的自动更新好烦啊 BeanCopier工具类(性能优化工具类) 内存屏障浅析 居住证可算是申请通过了…… 试了下壁挂炉供暖 关于重阳节 居住证签注... Spring Boot 2.x使用PostgreSQL数据库 十一节后开工头一天,修了个耳机…… 玉楼春·尊前拟把归期说 nginx反向代理配置去除前缀 1931→2021! Navicat Premium数据库账号密码解密 百度的索引量数据现在有点儿太夸张了吧? 哇塞~这个小姐姐实在太惊艳了…… 关于8月29号下午博客故障的一些记录 如何形象的描述反应式编程中的背压(Backpressure)机制? 基于Java8的Either类 海琴烟~~~ JMX监控权限认证配置 IntelliJ IDEA 2019.3.3 永久激活 破解[Windows] SpringBoot整合Elasticsearch详细步骤以及代码示例(附源码) SpringBoot整合Elasticsearch游标查询(scroll) GitLab创建新项目,初次提交命令和流程 吐槽下Google浏览器~ 从零搭建Spring Cloud Gateway网关(一) 从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志 博客有logo啦 你的答案 数据库事务的一点简单总结 Spring Boot发展史(Spring Boot介绍) 【漫画】戏说外行对程序员的误会有多深 【收藏】从面试官角度观察到的程序员技能瓶颈,同时给出突破瓶颈的建议 Git中.gitignore文件不起作用的解决办法 背影 - 朱自清 jmap命令(jdk1.8) 彻底搞懂mysql日志系统binlog,redolog,undolog 出院了~~~ linux中ftp查看不到文件列表的问题 PHP版本怎么更新啊…… 成语接龙 StarUML4.0破解文件 Java SPI详解 WordPress评论框增加自定义表情 因在公司上不正经网站,我没过试用期… 优化了MYSQL大量写入问题,老板奖励了1000块给我 不慌不忙的坚强(林徽因39段最美文字!) ConcurrentHashMap常用方法源码解析(jdk1.8) 我是如何失去团队掌控的? HBASE填坑日志 关闭apache httpclient4.5 DEBUG日志 使用OpenShift搭建k8s集群 SonarQube Scanner的配置与使用简介
WordPress评论添加UserAgent以及地理位置信息
李锋镝 · 2026-02-23 · via 李锋镝的博客

看到大家的博客评论下面都带有UserAgent信息,还有地理位置信息,一直都很羡慕,奈何网上苦寻插件不见。要么就是插件年久失修,要么就是直接主题集成,无一我想要的。昨天又想起这事,心痒难耐,遂决定自己敲一个出来(其实是AI代笔)。

首先看了下WordPress的评论表,发现UserAgent和用户IP都存的有,这就很方便了,至少数据不需要自己另外新建表保存了。

解析UA信息

解析UserAgent其实很简单,代码如下:

function parse_user_agent( $u_agent = null ) {
    if( $u_agent === null && isset($_SERVER['HTTP_USER_AGENT']) ) {
        $u_agent = $_SERVER['HTTP_USER_AGENT'];
    }

    if( $u_agent === null ) {
        throw new \InvalidArgumentException('parse_user_agent requires a user agent');
    }

    $platform = null;
    $browser  = null;
    $version  = null;

    $empty = array( 'platform' => $platform, 'browser' => $browser, 'version' => $version );

    if( !$u_agent ) {
        return $empty;
    }

    if( preg_match('/\((.*?)\)/m', $u_agent, $parent_matches) ) {
        preg_match_all('/(?P<platform>BB\d+;|Android|CrOS|Tizen|iPhone|iPad|iPod|Linux|(Open|Net|Free)BSD|Macintosh|Windows(\ Phone)?|Silk|linux-gnu|BlackBerry|PlayBook|X11|(New\ )?Nintendo\ (WiiU?|3?DS|Switch)|Xbox(\ One)?)
                (?:\ [^;]*)?
                (?:;|$)/imx', $parent_matches[1], $result);

        $priority = array( 'Xbox One', 'Xbox', 'Windows Phone', 'Tizen', 'Android', 'FreeBSD', 'NetBSD', 'OpenBSD', 'CrOS', 'X11' );

        $result['platform'] = array_unique($result['platform']);
        if( count($result['platform']) > 1 ) {
            if( $keys = array_intersect($priority, $result['platform']) ) {
                $platform = reset($keys);
            } else {
                $platform = $result['platform'][0];
            }
        } elseif( isset($result['platform'][0]) ) {
            $platform = $result['platform'][0];
        }
    }

    if( $platform == 'linux-gnu' || $platform == 'X11' ) {
        $platform = 'Linux';
    } elseif( $platform == 'CrOS' ) {
        $platform = 'Chrome OS';
    }

    preg_match_all('%(?P<browser>Camino|Kindle(\ Fire)?|Firefox|Iceweasel|IceCat|Safari|MSIE|Trident|AppleWebKit|
                TizenBrowser|(?:Headless)?Chrome|YaBrowser|Vivaldi|IEMobile|Opera|OPR|Silk|Midori|Edge|Edg|CriOS|UCBrowser|Puffin|OculusBrowser|SamsungBrowser|
                Baiduspider|Googlebot|YandexBot|bingbot|Lynx|Version|Wget|curl|
                Valve\ Steam\ Tenfoot|
                NintendoBrowser|PLAYSTATION\ (\d|Vita)+)
                (?:\)?;?)
                (?:(?:[:/ ])(?P<version>[0-9A-Z.]+)|/(?:[A-Z]*))%ix',
        $u_agent, $result);

    // If nothing matched, return null (to avoid undefined index errors)
    if( !isset($result['browser'][0]) || !isset($result['version'][0]) ) {
        if( preg_match('%^(?!Mozilla)(?P<browser>[A-Z0-9\-]+)(/(?P<version>[0-9A-Z.]+))?%ix', $u_agent, $result) ) {
            return array( 'platform' => $platform ?: null, 'browser' => $result['browser'], 'version' => isset($result['version']) ? $result['version'] ?: null : null );
        }

        return $empty;
    }

    if( preg_match('/rv:(?P<version>[0-9A-Z.]+)/i', $u_agent, $rv_result) ) {
        $rv_result = $rv_result['version'];
    }

    $browser = $result['browser'][0];
    $version = $result['version'][0];

    $lowerBrowser = array_map('strtolower', $result['browser']);

    $find = function ( $search, &$key = null, &$value = null ) use ( $lowerBrowser ) {
        $search = (array)$search;

        foreach( $search as $val ) {
            $xkey = array_search(strtolower($val), $lowerBrowser);
            if( $xkey !== false ) {
                $value = $val;
                $key   = $xkey;

                return true;
            }
        }

        return false;
    };

    $findT = function ( array $search, &$key = null, &$value = null ) use ( $find ) {
        $value2 = null;
        if( $find(array_keys($search), $key, $value2) ) {
            $value = $search[$value2];

            return true;
        }

        return false;
    };

    $key = 0;
    $val = '';
    if( $findT(array( 'OPR' => 'Opera', 'UCBrowser' => 'UC Browser', 'YaBrowser' => 'Yandex', 'Iceweasel' => 'Firefox', 'Icecat' => 'Firefox', 'CriOS' => 'Chrome', 'Edg' => 'Edge' ), $key, $browser) ) {
        $version = $result['version'][$key];
    }elseif( $find('Playstation Vita', $key, $platform) ) {
        $platform = 'PlayStation Vita';
        $browser  = 'Browser';
    } elseif( $find(array( 'Kindle Fire', 'Silk' ), $key, $val) ) {
        $browser  = $val == 'Silk' ? 'Silk' : 'Kindle';
        $platform = 'Kindle Fire';
        if( !($version = $result['version'][$key]) || !is_numeric($version[0]) ) {
            $version = $result['version'][array_search('Version', $result['browser'])];
        }
    } elseif( $find('NintendoBrowser', $key) || $platform == 'Nintendo 3DS' ) {
        $browser = 'NintendoBrowser';
        $version = $result['version'][$key];
    } elseif( $find('Kindle', $key, $platform) ) {
        $browser = $result['browser'][$key];
        $version = $result['version'][$key];
    } elseif( $find('Opera', $key, $browser) ) {
        $find('Version', $key);
        $version = $result['version'][$key];
    } elseif( $find('Puffin', $key, $browser) ) {
        $version = $result['version'][$key];
        if( strlen($version) > 3 ) {
            $part = substr($version, -2);
            if( ctype_upper($part) ) {
                $version = substr($version, 0, -2);

                $flags = array( 'IP' => 'iPhone', 'IT' => 'iPad', 'AP' => 'Android', 'AT' => 'Android', 'WP' => 'Windows Phone', 'WT' => 'Windows' );
                if( isset($flags[$part]) ) {
                    $platform = $flags[$part];
                }
            }
        }
    } elseif( $find(array( 'IEMobile', 'Edge', 'Midori', 'Vivaldi', 'OculusBrowser', 'SamsungBrowser', 'Valve Steam Tenfoot', 'Chrome', 'HeadlessChrome' ), $key, $browser) ) {
        $version = $result['version'][$key];
    } elseif( $rv_result && $find('Trident') ) {
        $browser = 'MSIE';
        $version = $rv_result;
    } elseif( $browser == 'AppleWebKit' ) {
        if( $platform == 'Android' ) {
            $browser = 'Android Browser';
        } elseif( strpos($platform, 'BB') === 0 ) {
            $browser  = 'BlackBerry Browser';
            $platform = 'BlackBerry';
        } elseif( $platform == 'BlackBerry' || $platform == 'PlayBook' ) {
            $browser = 'BlackBerry Browser';
        } else {
            $find('Safari', $key, $browser) || $find('TizenBrowser', $key, $browser);
        }

        $find('Version', $key);
        $version = $result['version'][$key];
    } elseif( $pKey = preg_grep('/playstation \d/i', $result['browser']) ) {
        $pKey = reset($pKey);

        $platform = 'PlayStation ' . preg_replace('/\D/', '', $pKey);
        $browser  = 'NetFront';
    }

    return array( 'platform' => $platform ?: null, 'browser' => $browser ?: null, 'version' => $version ?: null );
}

解析IP

另外一个难题是根据用户IP解析地址位置信息,网上大多都是调用Api接口或者IP库来解析的。

首先说说Api,大部分Api都有请求频率的限制,太频繁容易被限制,要么就是付费接口,那更不行,那Api这条路子基本行不通了。

第二条路子就是IP库,这个办法挺好的,但是需要经常更新IP库,不然时间久了位置信息可能不准了,不准的话,还不如不要,咱要做就要做好不是。这样的话,还得写个定时任务去更新IP库,挺麻烦的,我就想要一个轻量级的小功能而已,几行代码实现就行。

后来灵机一动,想起了自己在用的另外一款插件WP Statistics,这个插件自带的就有IP库,而且它自己还会定时去更新IP库,简直完美,那要怎么用它呢?这个简单,看它代码就行了。

经过研究,找到了GeolocationFactory这个类,里面有个函数叫getLocation,传入IP就能返回位置信息,完美!

$location_data = WP_Statistics\Service\Geolocation\GeolocationFactory::getLocation($comment_ip);
$country = $location_data['country'] ?? '未知';
$region = $location_data['region'] ?? '未知';
$city = $location_data['city'] ?? '未知';

渲染页面

剩下的就是将解析的信息渲染到页面上,WordPress提供有现成的钩子,调用即可。

function wpcdi_add_info_after_comment_content($comment_text, $comment) {
    // 仅前台显示,且评论已通过审核
    if (is_admin() || $comment->comment_approved != 1) {
        return $comment_text;
    }
    $location_icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20" fill="#e9ecef" stroke="#495057" stroke-width="2">
          <path d="M12 3c-2.5 0-4.5 2-4.5 4.5c0 2.5 2 5.5 4.5 9c2.5-3.5 4.5-6.5 4.5-9C16.5 5 14.5 3 12 3z"/>
          <circle cx="12" cy="7.5" r="1.5" fill="#495057"/>
        </svg>';

    // 获取评论者IP、UA并过滤
    $comment_ip = sanitize_text_field($comment->comment_author_IP);
    $user_agent = sanitize_text_field($comment->comment_agent);

    // 获取设备/地理信息
    $info = wpcdi_get_comment_info($comment_ip, $user_agent);

    $browser_icon = wpcdi_get_svg_icon('browser', $info['browser']);
    $os_icon = wpcdi_get_svg_icon('os', $info['os']);

    $info_html = '<div class="wpcdi-comment-info" style="padding-top:10px; font-size: 12px; color: #495057; line-height: 1.5; display: flex; align-items: center; gap: 5px;">
    <span style="display: inline-flex; align-items: center;">' . $os_icon . '<span>' . esc_html($info['os']) . '</span></span>
    <span style="display: inline-flex; align-items: center;">' . $browser_icon . '<span>' . esc_html($info['browser']) . '</span></span><span style="display: inline-flex; align-items: center;">' . $location_icon . '<span>'  . esc_html($info['location']) . '</span></span></div>';
    // 追加到评论内容后方
    return $comment_text . $info_html;
}
// 绑定评论内容钩子,优先级10,参数2
add_filter('comment_text', 'wpcdi_add_info_after_comment_content', 10, 2);

大家可以评论一下看看效果,或者有什么好的点子也可以说一下。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/article/cha-jian/4606