最近后台的评论列表没法看了。一屏十几个,全是同一个模子:
1,或者直接就是 example.comtesting@example.com555http://www.example.comIP 还都不一样,209.50.163.107、45.3.35.45、65.111.20.170 换着来。典型的脚本批量灌。
让我想不通的是:我这个主题本来就有评论过滤,写在 functions.php 里,还是当年从网上抄的。为什么一条都没拦住?
原来也写过文章描述过:限制wordpress评论文章字数(垃圾评论)的方法
原样贴出来,就这么几行:
add_filter( 'preprocess_comment', 'wpb_preprocess_comment' );
function wpb_preprocess_comment( $comment ) {
if ( strlen( $comment['comment_content'] ) > 350 ) {
wp_die( '评论内容过长,请精简到350字以内' );
}
$has_http = stristr( $comment['comment_content'], "http" ) !== false;
$has_https = stristr( $comment['comment_content'], "https" ) !== false;
$has_www = stristr( $comment['comment_content'], "www" ) !== false;
if ( $has_http || $has_https || $has_www ) {
wp_die( '评论内容包含不适当的链接信息,请移除后重新提交' );
}
return $comment;
}
逻辑很朴素:正文里出现 http 或者 www,就拒绝。看着没毛病。
但垃圾评论根本不往正文里写链接。它把链接塞在「网址」那一栏,正文只写三个字符「555」。
我盯着这段代码看了半天才反应过来——我检查的是 comment_content,垃圾填的是 comment_author_url,压根不是同一个字段。
三个 stristr 全都在查正文,网址栏从头到尾没人管。
WordPress 存一条评论的流程是这样的:
访客点提交
→ 各字段打包成 $comment 数组
→ 触发 preprocess_comment 钩子 ← 只能在这里拦
→ 写入数据库
→ 前台显示
有几个教程会让人用 comment_post,但那个钩子是评论已经进库之后才触发的。等于门已经关了才去抓人,抓到也只能删,属于先污染后治理。
preprocess_comment 是唯一能在落库前拦住的位置。它拿到的参数是一个数组,里面装着这次提交的全部字段:
| 键名 | 内容 |
|---|---|
| comment_author | 昵称 |
| comment_author_email | 邮箱 |
| comment_author_url | 网址 ← 灌水的重灾区 |
| comment_content | 评论正文 |
| comment_post_ID | 评论的是哪篇文章 |
核心思路:不光查正文,把网址栏、长度、邮箱一起检查。完整函数是这样:
add_filter( 'preprocess_comment', 'wpb_preprocess_comment' );
function wpb_preprocess_comment( $comment ) {
// 管理员自己不受限制,方便回复时贴链接
if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) {
return $comment;
}
$content = isset( $comment['comment_content'] ) ? (string) $comment['comment_content'] : '';
$url = isset( $comment['comment_author_url'] ) ? trim( (string) $comment['comment_author_url'] ) : '';
$email = isset( $comment['comment_author_email'] ) ? (string) $comment['comment_author_email'] : '';
// 闸门 1:网址栏不许填
if ( '' !== $url && 'http://' !== $url && 'https://' !== $url ) {
wpb_comment_die( '请不要在「网址」一栏填写链接。' );
}
// 闸门 2:正文里不许出现网址
if ( preg_match( '/https?:\/\/|www\./i', $content ) ) {
wpb_comment_die( '评论内容中不能包含网址链接。' );
}
// 闸门 3:正文太短
$plain = trim( wp_strip_all_tags( $content ) );
if ( mb_strlen( $plain, 'UTF-8' ) < 5 ) {
wpb_comment_die( '内容太短,请至少写下 5 个字。' );
}
// 闸门 4:正文过长(原来的规则)
if ( strlen( $content ) > 350 ) {
wpb_comment_die( '内容过长,请精简到 350 字以内。' );
}
// 闸门 5:占位邮箱
if ( '' !== $email && preg_match( '/@(example|test|localhost|sample)\./i', $email ) ) {
wpb_comment_die( '请填写真实的邮箱地址。' );
}
return $comment;
}
拒绝的时候别只 wp_die( '失败' ) 一行,访客看到白屏会以为网站坏了。给它一个能返回的提示:
function wpb_comment_die( $msg ) {
wp_die(
'<p>' . esc_html( $msg ) . '</p>'
. '<p><a href="javascript:history.back()">← 返回上一页修改</a></p>',
'评论未通过校验',
array( 'response' => 403 )
);
}
第三个参数 'response' => 403 是有讲究的。默认 wp_die 返回 500,那是「服务器内部错误」的意思;403 才是「禁止访问」。有些自动提交脚本认这个状态码,看到 403 就知道是被规则挡了,不会傻乎乎地重试。
第一版我写的是 if ( ! empty( $url ) ),结果误伤了一批正常访客。
原因是浏览器的表单自动填充。很多人(包括我自己测试时)明明没填网址,但浏览器会预塞一个 http:// 进去,值不为空,于是被判成垃圾。
所以判断要放过三个值:空字符串、http://、https://。这三个都算「没填」。
一个汉字在 UTF-8 里占 3 个字节。
strlen( '你好' ) // 6
mb_strlen( '你好', 'UTF-8' ) // 2
如果用 strlen 去判断「少于 5」,两个汉字就算合格了,垃圾照样能过。只有 mb_strlen 才是真的在数字。
(下面那条「超过 350」我保留了原来的 strlen,因为那是字节语义,350 字节约等于 116 个汉字,跟原来的行为一致,没动。)
这个不是理论,是我自己翻车了。
新代码沿用了旧函数的名字 wpb_preprocess_comment,好处是 add_filter 那行不用改。但如果你只把新代码贴上去、旧函数没删干净,PHP 会直接抛:
Fatal error: Cannot redeclare wpb_preprocess_comment()
整个网站瞬间白屏,后台也进不去。我当时的报错在第 96 行——不过那是另一次事故,等下说。
所以替换之前,一定先把 functions.php 备份一份,再把旧函数整段删掉,不要只删一半。
同一天我还踩了另一个坑,值得单独讲,因为太反直觉了。
我在改一个截取摘要的函数时,判断函数从哪一行结束,写的是「往下找第一行只有 } 的」。结果原函数里有个嵌套的 if { },脚本在内层那个 } 就停了,把旧函数的尾巴留在了原地,多出一个 }。
报错就一句:Unmatched '}' in functions.php on line 96。整站 500。
后来才明白,找函数结尾不能数第一个右括号,要数括号的层级:遇到 { 加一,遇到 } 减一,减到 0 才是函数真正的结尾。用正则或者行匹配去找,遇到嵌套必错。
上面这些做完,垃圾确实进不来了。但想了几天,我把评论功能整个关掉了。
原因很实在:我这是个手机卡套餐站,一千多篇文章讲的是「19 元 130G 怎么办理」。读者看完就去办卡了,不会有人在这儿讨论「这个套餐值不值」。建站到现在,真实评论一只手数得过来,剩下全在灌水。
既然如此,留着评论区图什么呢?维护过滤规则要成本,规则还可能被新的脚本绕过去。真要有人联系我,页脚有 QQ 群和联系方式,比评论区靠谱。
所以最后的选择是:后台「设置 → 讨论」里取消勾选「允许他人在新文章上发表评论」,一劳永逸。
如果你的站评论有真实互动、舍不得关,那就把上面那套五道闸挂上去,同时务必在同一个设置页勾上「评论必须经人工批准」。别让它自动发布——自动发布等于把前台交给脚本。