Unlimited Elements 2.0.9 插件 Pro 标签异常排查记录

Unlimited Elements 2.0.9 Pro 标签异常排查全记录

先说结论:这次折腾了一天,最后发现的问题简单得让人无语。UE 插件升级到 2.0.9 之后,小部件标签从 “Web” 变成了 “Free”,查到最后居然是压缩包漏打包了一个目录。

下面把整个排查过程原原本本记下来。给自己留个档,也给遇到同样问题的朋友一个参考。

发现症状

站上用的是 Unlimited Elements for Elementor(下面简称 UE),Pro 版本,Freemius 授权一直正常。2.0.6 时代,Elementor 编辑器里所有 Pro 小部件的标签都显示 “Web”,一切正常。

然后我手欠,点了升级,2.0.6 直接升到 2.0.9。

再进编辑器,所有 Pro 小部件的标签变成了 “Free” 或 “Pro”。当时挺懵的:授权明明没到期。去 Freemius 后台确认了一下,授权状态是 Active,没问题。

那问题就出在插件自身了。

先看 Changelog:发现一次安全更新

照例先看 changelog。2.0.6 → 2.0.7 → 2.0.8 → 2.0.9,每个版本改了什么?

注意到 2.0.7 的更新说明里写着这是安全更新。去 Patchstack 查了一下,CVE-2026-27041,CVSS 9.9 分,任意文件上传漏洞,受影响版本 ≤ 2.0.6,2.0.7 修复。

我的第一反应:肯定是这个安全补丁动到了 Pro 判断逻辑。

下源码对比:发现判断错了

直接把 2.0.6 和 2.0.7 的完整源码拉下来,diff -rq 扫了一遍。29 个文件有差异。

逐个翻:

  • provider_helper.class.php — 加了权限检查
  • assets_work.class.php — 加强了文件上传验证
  • functions_wordpress.class.php — 修了 SQL 注入
  • ……

都是安全相关改动,合理。可翻到跟标签显示有关的文件——browser.class.phpweb_api.class.phpglobals.class.php——挨个比对,2.0.6 和 2.0.7 之间这几个文件完全一样,一个字节都没变

也就是说,安全补丁跟标签显示逻辑毫无关系。我的推测是错的。

开始瞎猜:版本号?隐藏开关?

安全补丁这条线断了,我开始各种乱猜。

猜测一:API 服务器根据版本号返回不同的数据?

UE 有个远程 API,插件靠它跟服务器通信拿小部件元数据。我琢磨着,会不会服务器端发现版本号变了,就返回不同的标签数据?毕竟 2.0.9 是新版本,万一远程 API 做了兼容性检查呢。

验证很简单——改版本号。版本号常量定义在 includes.php 里:


// unlimited-elements-for-elementor-premium/includes.php
define("UNLIMITED_ELEMENTS_VERSION", "2.0.9");

我在 wp-config.php 里加了这样一行去覆盖:


// 加到 wp-config.php - 尝试覆盖版本号欺骗 API 服务器
define("UNLIMITED_ELEMENTS_VERSION", "2.0.6");

结果,标签该是 “Free” 还是 “Free”。

猜测二:是不是有隐藏的测试开关?

翻代码时注意到 unitecreator_globals.class.php 里有一段逻辑:


if (defined("UC_TEST_FREE_VERSION"))
    self::$isProVersion = false;

有个 UC_TEST_FREE_VERSION 常量,定义了就强制把 Pro 降成 Free。我怀疑新版本里是不是某处悄悄 define 了这个常量。于是在 wp-config.php 里加:


// 加到 wp-config.php - 确保这个常量没被定义
// 也试过改成 true 看有啥效果
define("UC_TEST_FREE_VERSION", false);

结果一样——跟这个常量毫无关系。

上检测代码:逐个版本对比

猜来猜去不是办法,上检测手段。先搞清楚标签显示到底由哪个函数决定。

顺藤摸瓜:

  1. Elementor 面板的标签 → browser.class.php:isWebAddonFree() 第 529 行
  2. ↓ 调用
  3. provider_web_api.class.php:isProductActive()
  4. ↓ 调用
  5. HelperProviderUC::isActivatedByFreemius()
  6. ↓ 调用
  7. Freemius SDK 的 $uefe_fs->is_paying()

我在主题的 functions.php 里加了段检测代码,把每一步的返回值都打出来:


// 加到主题 functions.php - 调试检测代码
add_action('init', function() {
    if (!class_exists('UniteCreatorWebAPI') || !class_exists('HelperProviderUC')) {
        return;
    }

    global $uefe_fs;

    $webApi = new UniteCreatorWebAPI();
    $webApi->setProduct('unlimitedelements');

    $debug = [
        'isProductActive'      => $webApi->isProductActive() ? 'true' : 'false',
        'isFreemiusActive'     => HelperProviderUC::isActivatedByFreemius() ? 'true' : 'false',
        'UE Version'           => UNLIMITED_ELEMENTS_VERSION,
        'Freemius version'     => $uefe_fs->get_plugin_version(),
        'is_paying'            => $uefe_fs->is_paying() ? 'true' : 'false',
        'GlobalsUC::$isProVersion' => (GlobalsUC::$isProVersion ?? false) ? 'true' : 'false',
        'pro_path_exists'      => file_exists(GlobalsUC::$pathPro ?? '') ? 'true' : 'false',
    ];

    error_log('[UE_DEBUG] ' . print_r($debug, true));
});

wp-config.php 里把调试日志打开:


// 加到 wp-config.php - 开启调试日志
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

刷新页面,去看 /wp-content/debug.log


[UE_DEBUG] Array
(
    [isProductActive] => false
    [isFreemiusActive] => true
    [UE Version] => 2.0.9
    [Freemius version] => 2.6.2
    [is_paying] => true
    [GlobalsUC::$isProVersion] => false
    [pro_path_exists] => false
)

有意思了。is_paying = trueisProductActive = false

再看 provider_web_api.class.phpisProductActive() 的源码,一下就明白了:


public function isProductActive() {
    if(GlobalsUC::$isProVersion == false)
        return(false);  // <-- 在这里就返回了!

    if($this->isFreemiusActive() == false)
        return(false);

    return(true);
}

函数第一行就先检查 GlobalsUC::$isProVersion,是 false 就直接 return false,根本走不到 Freemius 检查那一步。怪不得 is_paying = trueisProductActive = false——Freemius 压根没被问到!

二分法锁定问题版本

检测路径清楚了,接下来用二分法找出是哪个版本引入的问题。

检测代码保留,反复切换插件版本:2.0.6 → 2.0.7 → 2.0.8 → 2.0.9,每次都看 debug.log:


// === 2.0.6 ===
[UE_DEBUG] isProductActive: true, isFreemiusActive: true, is_paying: true, $isProVersion: true

// === 2.0.7 ===
[UE_DEBUG] isProductActive: true, isFreemiusActive: true, is_paying: true, $isProVersion: true

// === 2.0.8 ===
[UE_DEBUG] isProductActive: true, isFreemiusActive: true, is_paying: true, $isProVersion: true

// === 2.0.9 ===
[UE_DEBUG] isProductActive: false, isFreemiusActive: true, is_paying: true, $isProVersion: false

2.0.8 正常,2.0.9 的 $isProVersion 变成了 false。这个变量在 unitecreator_globals.class.php 里设置:


// unitecreator_globals.class.php 第 244 行
self::$pathPro = self::$pathPlugin . "pro/";

if(file_exists(self::$pathPro))
    self::$isProVersion = true;

if(defined("UC_TEST_FREE_VERSION"))
    self::$isProVersion = false;

逻辑简单得很——看插件目录下有没有 pro/ 目录。有就是 Pro 版,没有就是 Free 版。

而检测代码早就告诉我 pro_path_exists = false 了。也就是说2.0.9 安装目录下根本没有 pro/ 这个文件夹

对比压缩包:找到真凶

为了确认,把 2.0.8 和 2.0.9 的 zip 都下下来对比目录结构:


$ diff -rq unlimited-elements-2.0.8/ unlimited-elements-2.0.9/
只在 2.0.8 中找到: pro/childparams_pro.class.php
只在 2.0.8 中找到: pro/globals_pro.class.php
只在 2.0.8 中找到: pro/includes_pro.php
只在 2.0.8 中找到: pro/provider_settings_multisource_pro.class.php
只在 2.0.8 中找到: pro/template_engine_pro.class.php
...(还有其他 15 个文件差异)

2.0.9 的压缩包里整个 pro/ 目录消失了,上面这 5 个文件一个都没打包进去。

这能怪谁呢,八成是发布 2.0.9 的时候打包脚本出了问题,把 pro/ 目录漏掉了。2.0.7、2.0.8 都好好的,偏偏 2.0.9 少了这个目录。

完整的根因链路

把整个连锁反应捋一遍:

  1. 2.0.9 zip 打包时 pro/ 目录被遗漏
  2. file_exists(self::$pathPro)false(目录不存在)
  3. GlobalsUC::$isProVersion 保持默认值 false
  4. isProductActive() 第 1 行检查 if($isProVersion == false) return(false)直接返回 false
  5. Freemius 的 is_paying() 虽然返回 true,但根本没被执行到
  6. isWebAddonFree() 收到 false → 所有小部件显示 “Free” 而非 “Web”

讽刺不?一个上传安全漏洞修得严严实实,最后栽在一个打包漏文件的问题上。

修复方法

修复简单得不能再简单——从 2.0.8 目录把 pro/ 复制过来:


cd /path/to/wp-content/plugins/unlimited-elements-for-elementor-premium/
cp -r /path/to/backup/unlimited-elements-2.0.8/pro/ ./pro/

刷新,一切恢复正常。所有小部件又老老实实显示 “Web” 标签了。

后记

这次排查最大的感触是——你以为的问题往往不是真正的问题

  • 一开始以为是 Freemius 授权问题 → 不是
  • 后来以为是安全补丁改了代码 → 也不是
  • 再猜版本号检测 → 还是不对
  • 最后发现是打包漏了个目录

如果一开始就直接对比各版本的 zip 文件列表,而不是在代码逻辑里绕来绕去,能省下一大半时间。但反过来想,要不是一层层追踪代码逻辑、加调试输出、逐个版本对比验证,也不会对 UE 的授权判断机制理解得这么透彻。

另外,file_exists() 判断 Pro 版本这个设计本身就挺脆弱的——文件系统状态和授权状态一旦不一致,就会出现这种诡异的症状。当然,如果打包没出问题,这本来也不会有事。

给遇到同样问题的朋友一个建议:Unlimited Elements 升到 2.0.9 后发现 Pro 小部件不认了,先去看插件目录下有没有 pro/ 这个文件夹。八成就是少了它。


排查日期:2026-05-14 | 首发于 tudoudaily.com

发表回复

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

或许还会想看: