WP Armour Honeypot 与 Elementor Popup 兼容性问题排查

现象

网站上启用了 WP Armour Honeypot 防垃圾插件,普通页面上的表单工作正常。但所有通过 Elementor Popup 提交的表单,无论填写什么内容,提交后都返回 “Spamming or your Javascript is disabled !!” 错误。

排查过程

第一步:确认插件正常工作

首先确认 WP Armour 在普通页面上是否正常。网站运行了 WP Armour Honeypot 一段时间,非 Popup 页面上的各类表单从未出现过误判,说明插件核心功能在常规场景下工作正常。

第二步:定位到 Popup

问题只出现在 Elementor Popup 中加载的表单上。常见的 Popup 有两种加载方式:

  1. 页面内嵌 — Popup 内容直接在页面 HTML 中,用 CSS 隐藏,通过 JS 控制显示
  2. 动态加载 (AJAX) — Popup 内容通过 AJAX 请求获取,渲染到 DOM 中

我们的网站使用的是 AJAX 动态加载模式。这意味着 Popup 内的表单在页面加载时并不存在于 DOM 中。

第三步:分析 WP Armour 的注入机制

查看 WP Armour 的 JavaScript 代码(wpa.jswpa_vanilla.js),发现它的工作流程是这样的:


页面加载 → 监听用户交互事件 (mousemove, keydown, scroll 等)
        → 用户交互触发 → 运行 wpa_add_honeypot_field()
        → 2 秒后备超时 → 同样运行 wpa_add_honeypot_field()
        → wpa_injected = true (锁定,不再重复注入)

wpa_add_honeypot_field() 的工作方式是在 DOM 中查找所有匹配选择器的表单,然后追加蜜罐字段:


function wpa_add_honeypot_field() {
    var allFormSelectors = [
        'form.wpcf7-form, .wpcf7 form',
        'form.elementor-form',
        'form.frm-fluent-form',
        // ... 更多选择器
    ];
    // 一次性查询所有现有表单并注入
    document.querySelectorAll(allFormSelectors.join(', ')).forEach(...)
}

关键问题就在这里:querySelectorAll 只查询当前 DOM 中已存在的表单。AJAX 加载 Popup 时,表单被添加到 DOM 中,但 wpa_add_honeypot_field() 已经运行完毕,且 wpa_injected 已经锁定,不会再运行第二次。

第四步:确认蜜罐字段缺失

在浏览器开发者工具中检查 Elementor Popup 中的表单,对比普通页面的表单:

普通页面表单(正常)


<form class="elementor-form">
<!-- 正常表单字段 -->
  <input type="text" name="name">
  <input type="email" name="email">
  <!-- 蜜罐字段 -->
  <div id="altEmail_container" class="altEmail_container">
    <label for="alt_s">Alternative:</label>
    <input type="text" id="alt_s" name="alt_s">
  </div>
  <span class="wpa_hidden_field" style="display:none;height:0;width:0;">
    <label>WPA <input type="text" name="wpa_field_xyz" value="12345"></label>
  </span>
</form>

Popup 中的表单(异常)


<form class="elementor-form">
  <!-- 只有正常表单字段,没有蜜罐 -->
  <input type="text" name="name">
  <input type="email" name="email">
  <!-- ❌ wpa_hidden_field 和 altEmail_container 完全不存在 -->
</form>

第五步:验证 PHP 端的检测逻辑

然后看了 PHP 端如何判断是否为垃圾信息:


function wpa_check_is_spam($form_data) {
    if (
        (isset($form_data[$GLOBALS['wpa_field_name']])) &&  // ① 蜜罐隐藏字段必须在提交数据中
        (isset($form_data['alt_s'])) &&                       // ② alt_s 字段必须在提交数据中
        (empty($form_data['alt_s']))                          // ③ alt_s 必须为空
    ) {
        return false; // ✅ 非垃圾
    } else {
        return true;  // ❌ 判定为垃圾
    }
}

三条检查全通过才算正常提交。因为 Popup 表单没有注入蜜罐字段,提交的数据中自然没有 wpa_field_namealt_swpa_check_is_spam() 返回 true,”Spamming or your Javascript is disabled !!” 的错误就被抛出来了。

根因

WP Armour Honeypot 的 JS 注入逻辑设计为”一次性”的:

  • 只在页面加载时(或用户首次交互时)运行一次
  • wpa_injected 开关防止重复注入
  • 没有监听 DOM 变化的机制

Elementor Popup 的 AJAX 动态加载模式,恰好撞上了这个设计盲区。不是 WP Armour 有问题,也不是 Elementor 有问题,而是两者没有意识到对方的存在。

修复方法

修复思路很简单:监听 Elementor Popup 打开事件,在 Popup 打开时对弹窗内的表单重新注入蜜罐字段

Elementor 提供了 elementor/popup/show 事件,通过 elementorFrontend.hooks.addAction 可以注册回调:


elementorFrontend.hooks.addAction('elementor/popup/show', function(popupId, popupData, popupElement) {
    // 获取弹窗容器
    var popupEl = popupElement || document.querySelector('.elementor-' + popupId);
    if (!popupEl) return;

    // 在弹窗内查找所有表单
    var forms = popupEl.querySelectorAll([
        'form.elementor-form',
        'form.wpcf7-form',
        'form.frm-fluent-form',
        // ...
    ].join(', '));

    forms.forEach(function(form) {
        // 如果已经存在蜜罐字段则跳过,防止重复注入
        if (form.querySelector('.wpa_hidden_field')) return;
        form.insertAdjacentHTML('beforeend', wpa_hidden_field);
    });
});

完整的修复方案包括三个部分:

  1. Elementor 事件钩子 — 监听 elementor/popup/show,在弹窗打开时注入
  2. 重复注入防护 — 检查 .wpa_hidden_field 是否已存在,支持弹窗多次打开而不重复注入
  3. MutationObserver 后备方案 — 对于非 Elementor 的动态加载场景(如 Beaver Builder、自定义 AJAX 表单),也能捕获

完整的可部署代码:


(function() {
    function init() {
        if (typeof wpa_add_honeypot_field !== 'function' || typeof wpa_hidden_field === 'undefined') {
            setTimeout(init, 500);
            return;
        }

        if (typeof elementorFrontend !== 'undefined' && elementorFrontend.hooks) {
            elementorFrontend.hooks.addAction('elementor/popup/show', function(popupId, popupData, popupElement) {
                var popupEl = popupElement || document.querySelector('.elementor-' + popupId);
                if (!popupEl) return;

                var formSelectors = [
                    'form.elementor-form',
                    'form.wpcf7-form', '.wpcf7 form',
                    'form.wpforms-form',
                    '.gform_wrapper form',
                    '.frm_forms form',
                    'form.frm-fluent-form',
                    '.ff_conv_app',
                    'form.wpa_form', '.wpa_form form',
                    'form#commentform', 'form.comment-form',
                    'form#loginform',
                    'form.register',
                    'form.cart'
                ];

                var forms = popupEl.querySelectorAll(formSelectors.join(', '));
                forms.forEach(function(form) {
                    if (form.querySelector('.wpa_hidden_field')) return;
                    form.insertAdjacentHTML('beforeend', wpa_hidden_field);
                });

                var initiators = popupEl.querySelectorAll('input.wpa_initiator');
                initiators.forEach(function(initiator) {
                    var form = initiator.closest('form');
                    if (form && !form.querySelector('.wpa_hidden_field')) {
                        initiator.insertAdjacentHTML('afterend', wpa_hidden_field);
                    }
                });
            });
        }

        if (!window._wpaObserver) {
            window._wpaObserver = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    mutation.addedNodes.forEach(function(node) {
                        if (node.nodeType !== 1) return;
                        var forms = node.matches && node.matches('form') ? [node] : node.querySelectorAll ? node.querySelectorAll('form') : [];
                        forms.forEach(function(form) {
                            if (!form.querySelector('.wpa_hidden_field')) {
                                form.insertAdjacentHTML('beforeend', wpa_hidden_field);
                            }
                        });
                    });
                });
            });
            window._wpaObserver.observe(document.body, { childList: true, subtree: true });
        }
    }

    init();
})();

安装方式:将代码添加到主题的 functions.php 中,或者使用自定义 JS 插件(如 WPCode):


add_action('wp_footer', function() { ?>
<script>
/* 粘贴上面的 JS 代码 */
</script>
<?php }, 999);

总结

这个问题的本质是一个典型的时序问题:WP Armour 假设所有表单都在页面加载时存在,而 Elementor Popup 打破了这一假设。

从这次的排查中学到几点:

  1. 安全插件的 JS 注入逻辑需要考虑动态内容。静态 DOM 查询在单页应用(SPA)和动态加载场景下会有盲区。MutationObserver 是处理这类问题的基础设施级方案。
  2. Elementor Popup 的事件系统很完善elementor/popup/show 事件是处理 Popup 相关逻辑的入口点,比监听 DOM 变化更精确、更高性能。
  3. 给插件提建议时,带上代码。与其让作者自己理解问题再写代码,不如直接给他一个可合并的修复方案。WP Armour 的开发成本不高——让 wpa_add_honeypot_field() 支持幂等调用(已注入的表单自动跳过),再加上 Elementor 事件监听,就能解决这个问题。
发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

或许还会想看: