first commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\common\service\qywx\QywxPromotionWidgetService;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
function widgetAssert(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function expectInvalidWidget(array $overrides, string $message): void
|
||||
{
|
||||
try {
|
||||
QywxPromotionWidgetService::fromInput($overrides + QywxPromotionWidgetService::defaults());
|
||||
} catch (InvalidArgumentException) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$defaults = QywxPromotionWidgetService::defaults();
|
||||
widgetAssert($defaults === [
|
||||
'v' => 1,
|
||||
'enabled' => false,
|
||||
'template' => 'bubble',
|
||||
'position' => 'bottom-right',
|
||||
'title' => '专属顾问在线',
|
||||
'subtitle' => '点击添加企业微信,获取一对一服务',
|
||||
'button_text' => '立即咨询',
|
||||
'primary_color' => '#139A8C',
|
||||
'bottom_offset' => 28,
|
||||
'show_mobile' => true,
|
||||
], '默认浮窗配置与公开契约不一致');
|
||||
|
||||
$normalised = QywxPromotionWidgetService::fromInput([
|
||||
'enabled' => '1',
|
||||
'template' => 'card',
|
||||
'position' => 'bottom-left',
|
||||
'title' => " 在线\n顾问 ",
|
||||
'subtitle' => '',
|
||||
'button_text' => '去咨询',
|
||||
'primary_color' => '#a1b2c3',
|
||||
'bottom_offset' => '64',
|
||||
'show_mobile' => '0',
|
||||
]);
|
||||
widgetAssert($normalised['v'] === 1, '缺省输入未补齐配置版本');
|
||||
widgetAssert($normalised['enabled'] === true, '启用状态规范化失败');
|
||||
widgetAssert($normalised['title'] === '在线 顾问', '文案空白规范化失败');
|
||||
widgetAssert($normalised['primary_color'] === '#A1B2C3', '主题色未规范为大写');
|
||||
widgetAssert($normalised['bottom_offset'] === 64, '底部距离规范化失败');
|
||||
widgetAssert($normalised['show_mobile'] === false, '移动端开关规范化失败');
|
||||
widgetAssert(
|
||||
QywxPromotionWidgetService::decode(QywxPromotionWidgetService::encode($normalised)) === $normalised,
|
||||
'浮窗配置编解码不能稳定往返'
|
||||
);
|
||||
|
||||
expectInvalidWidget(['v' => 2], '未知版本没有被拒绝');
|
||||
expectInvalidWidget(['template' => 'html'], '未知模板没有被拒绝');
|
||||
expectInvalidWidget(['position' => 'top-right'], '未知位置没有被拒绝');
|
||||
expectInvalidWidget(['title' => ''], '空标题没有被拒绝');
|
||||
expectInvalidWidget(['title' => str_repeat('中', 25)], '超长标题没有被拒绝');
|
||||
expectInvalidWidget(['subtitle' => str_repeat('中', 49)], '超长副标题没有被拒绝');
|
||||
expectInvalidWidget(['button_text' => str_repeat('中', 13)], '超长按钮文案没有被拒绝');
|
||||
expectInvalidWidget(['primary_color' => 'red;background:url(x)'], 'CSS 注入色值没有被拒绝');
|
||||
expectInvalidWidget(['bottom_offset' => 15], '过小底部距离没有被拒绝');
|
||||
expectInvalidWidget(['bottom_offset' => 161], '过大底部距离没有被拒绝');
|
||||
expectInvalidWidget(['show_mobile' => 'yes'], '非法布尔值没有被拒绝');
|
||||
expectInvalidWidget(['template' => null], '显式 null 模板没有被拒绝');
|
||||
|
||||
$decodedInvalid = QywxPromotionWidgetService::decode('{broken');
|
||||
widgetAssert($decodedInvalid === $defaults && $decodedInvalid['enabled'] === false, '损坏 JSON 未 fail-closed');
|
||||
$decodedIncomplete = QywxPromotionWidgetService::decode('{"enabled":true}');
|
||||
widgetAssert($decodedIncomplete === $defaults && $decodedIncomplete['enabled'] === false, '字段缺失配置未 fail-closed');
|
||||
$decodedUnknown = QywxPromotionWidgetService::decode('{"v":2,"enabled":true}');
|
||||
widgetAssert($decodedUnknown === $defaults && $decodedUnknown['enabled'] === false, '未知版本未 fail-closed');
|
||||
$decodedIllegal = QywxPromotionWidgetService::decode('{"v":1,"enabled":true,"template":"raw-html"}');
|
||||
widgetAssert($decodedIllegal === $defaults && $decodedIllegal['enabled'] === false, '非法持久化配置未 fail-closed');
|
||||
|
||||
$xssConfig = QywxPromotionWidgetService::fromInput([
|
||||
'v' => 1,
|
||||
'enabled' => true,
|
||||
'template' => 'message',
|
||||
'position' => 'bottom-right',
|
||||
'title' => '<img onerror=x>',
|
||||
'subtitle' => '</script>',
|
||||
'button_text' => '咨询',
|
||||
'primary_color' => '#139A8C',
|
||||
'bottom_offset' => 28,
|
||||
'show_mobile' => true,
|
||||
]);
|
||||
$encoded = QywxPromotionWidgetService::encode($xssConfig);
|
||||
widgetAssert(!str_contains($encoded, '<img') && str_contains($encoded, '\\u003Cimg'), '持久化 JSON 未使用 HEX 转义');
|
||||
|
||||
$key = str_repeat('a', 32);
|
||||
$script = QywxPromotionWidgetService::renderScript(
|
||||
$key,
|
||||
'/api/qywx-promotion/go/' . $key,
|
||||
$xssConfig,
|
||||
true
|
||||
);
|
||||
widgetAssert(!str_contains($script, '<img onerror=x>'), 'XSS 文案以原始标签进入公开脚本');
|
||||
widgetAssert(!str_contains($script, 'innerHTML'), '公开脚本不得使用 innerHTML');
|
||||
widgetAssert(str_contains($script, 'node.textContent=value'), '公开脚本文案未通过 textContent 写入');
|
||||
widgetAssert(str_contains($script, 'data-wecom-promotion'), '旧 data-wecom-promotion 触发方式丢失');
|
||||
widgetAssert(str_contains($script, '.wecom-promotion-link[data-pool'), '旧 data-pool 触发方式丢失');
|
||||
widgetAssert(str_contains($script, 'w.WecomPromotion=w.WecomPromotion||{}'), '全局 WecomPromotion 注册表丢失');
|
||||
widgetAssert(str_contains($script, 'open:openPromotion'), '全局 open 方法丢失');
|
||||
widgetAssert(str_contains($script, 'show:show') && str_contains($script, 'hide:hide') && str_contains($script, 'destroy:destroy'), '浮窗生命周期方法不完整');
|
||||
widgetAssert(str_contains($script, 'location.origin') && str_contains($script, 'location.pathname'), '来源地址未限制为 origin + pathname');
|
||||
widgetAssert(!str_contains($script, 'location.href'), '公开脚本仍发送完整 location.href');
|
||||
widgetAssert(str_contains($script, 'attachShadow'), '公开脚本未隔离浮窗样式');
|
||||
widgetAssert(str_contains($script, 'd.currentScript') && str_contains($script, 'new w.URL(value,node.src)'), '跳转地址未从安装脚本来源解析');
|
||||
widgetAssert(str_contains($script, "style.setAttribute('nonce',nonce)"), '公开脚本未向动态样式传递 CSP nonce');
|
||||
widgetAssert(str_contains($script, 'event.composedPath'), '公开脚本未兼容 Shadow DOM 内的手动触发元素');
|
||||
widgetAssert(!str_contains($script, 'root.style.'), '公开脚本仍依赖会被严格 CSP 拦截的元素内联样式');
|
||||
widgetAssert(str_contains($script, 'safe-area-inset-bottom'), '公开脚本未适配移动端安全区');
|
||||
foreach (['bubble', 'pill', 'card', 'message', 'edge', 'bar'] as $template) {
|
||||
widgetAssert(str_contains($script, '.wcp-' . $template), '公开脚本缺少模板:' . $template);
|
||||
}
|
||||
|
||||
$disabledScript = QywxPromotionWidgetService::renderScript($key, 'https://example.test/go', $xssConfig, false);
|
||||
widgetAssert(str_contains($disabledScript, '"enabled":false'), '停用方案仍会自动挂载浮窗');
|
||||
widgetAssert(str_contains($disabledScript, 'open:openPromotion'), '停用方案脚本没有保留手动 open 兼容接口');
|
||||
|
||||
echo "QYWX_PROMOTION_WIDGET_SERVICE_OK\n";
|
||||
Reference in New Issue
Block a user