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.php、web_api.class.php、globals.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);
结果一样——跟这个常量毫无关系。
上检测代码:逐个版本对比
猜来猜去不是办法,上检测手段。先搞清楚标签显示到底由哪个函数决定。
顺藤摸瓜:
- Elementor 面板的标签 →
browser.class.php:isWebAddonFree()第 529 行 - ↓ 调用
provider_web_api.class.php:isProductActive()- ↓ 调用
HelperProviderUC::isActivatedByFreemius()- ↓ 调用
- 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 = true 但 isProductActive = false。
再看 provider_web_api.class.php 里 isProductActive() 的源码,一下就明白了:
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 = true 却 isProductActive = 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 少了这个目录。
完整的根因链路
把整个连锁反应捋一遍:
- 2.0.9 zip 打包时 pro/ 目录被遗漏了
file_exists(self::$pathPro)→ false(目录不存在)GlobalsUC::$isProVersion保持默认值 falseisProductActive()第 1 行检查if($isProVersion == false) return(false)→ 直接返回 false- Freemius 的
is_paying()虽然返回 true,但根本没被执行到 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