fix(phone-image): 修复XSS注入、正则兼容性、render锁稳定性和缓存清理

T1: XSS修复 - PHP模板注入改用json_encode,poster URL转义处理
T2: fontSize NaN修复 - parseInt统一处理,lookbehind正则替换为兼容方案
T3: render锁稳定性 - insertPageBreak/removePageBreak添加15次重试上限,
     fontsReady添加catch处理,_pending递归添加错误捕获
T4: 缓存清理 - init()清空缓存,render()超过3倍blocks数自动清理
This commit is contained in:
augushong
2026-05-07 21:29:45 +08:00
parent e657e37dd4
commit 9aacfab11d
2 changed files with 53 additions and 14 deletions

View File

@@ -92,6 +92,9 @@ var PhoneImageEngine = (function () {
$.extend(config, userConfig);
}
// 清空缓存,避免旧数据干扰
convertedBlockCache = {};
// 内容流事件委托(只绑定一次)
$(document).off('click', '.break-inserter-btn');
$(document).off('click', '.remove-break-btn');
@@ -142,6 +145,12 @@ var PhoneImageEngine = (function () {
var cleanHtml = preprocessContent(postData.content_html);
var blocks = parseHtmlToBlocks(cleanHtml);
// 缓存清理:如果缓存条目过多则清空,防止内存膨胀
var cacheKeys = Object.keys(convertedBlockCache);
if (cacheKeys.length > blocks.length * 3) {
convertedBlockCache = {};
}
// 先渲染内容流(DOM) - 用于实测高度
renderContentFlow(blocks);
@@ -181,7 +190,9 @@ var PhoneImageEngine = (function () {
// 如果渲染期间有新的渲染请求排队,自动触发
if (render._pending) {
render._pending = false;
render();
render().catch(function () {
// 静默处理递归渲染失败
});
}
deferred.resolve(pages);
}).catch(function (err) {
@@ -553,13 +564,25 @@ var PhoneImageEngine = (function () {
return [block];
}
// 按句子拆分:句号、问号、叹号、换行
var sentences = text.split(/(?<=[。!?\n])/);
// 按句子拆分:句号、问号、叹号、换行(兼容不支持 lookbehind 的浏览器)
var effectiveFontSize = parseInt(config.fontSize, 10) || 14;
var parts = text.split(/([。!?\n])/);
var sentences = [];
var current = '';
for (var si = 0; si < parts.length; si++) {
current += parts[si];
if (/[。!?\n]/.test(parts[si]) && current.length > 0) {
sentences.push(current);
current = '';
}
}
if (current) sentences.push(current);
if (sentences.length <= 1) {
// 无法按句子拆分,按固定字符数拆分
sentences = [];
var chunkSize = Math.floor(getContentWidth() / config.fontSize) *
Math.floor(pageHeight / (config.fontSize * 1.8));
var chunkSize = Math.floor(getContentWidth() / effectiveFontSize) *
Math.floor(pageHeight / (effectiveFontSize * 1.8));
if (chunkSize < 10) chunkSize = 10;
for (var ci = 0; ci < text.length; ci += chunkSize) {
sentences.push(text.substring(ci, ci + chunkSize));
@@ -570,7 +593,7 @@ var PhoneImageEngine = (function () {
for (var j = 0; j < sentences.length; j++) {
var s = sentences[j].trim();
if (!s) continue;
var h = estimateTextHeight(s, config.fontSize);
var h = estimateTextHeight(s, effectiveFontSize);
result.push({
type: wrapperTag === 'blockquote' ? 'blockquote' : 'p',
html: '<' + wrapperTag + '>' + s + '</' + wrapperTag + '>',
@@ -610,7 +633,7 @@ var PhoneImageEngine = (function () {
html += '" style="width:' + sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
if (hasCover) {
html += '<img class="cover-image" src="' + postData.poster + '" alt="">';
html += '<img class="cover-image" src="' + escapeHtml(postData.poster) + '" alt="">';
html += '<div class="cover-title">' + escapeHtml(postData.title) + '</div>';
if (postData.desc) {
html += '<div class="cover-subtitle">' + escapeHtml(postData.desc) + '</div>';
@@ -825,7 +848,9 @@ var PhoneImageEngine = (function () {
? document.fonts.ready
: $.Deferred().resolve().promise();
fontsReady.then(function () {
fontsReady.catch(function () {
// 字体加载失败不影响截图,继续执行
}).then(function () {
requestAnimationFrame(function () {
// html2canvas会跳过visibility:hidden的元素临时切换为可见
$staging.css({ visibility: 'visible' });
@@ -1163,9 +1188,16 @@ var PhoneImageEngine = (function () {
function insertPageBreak(blockIndex) {
// 如果正在渲染,延迟重试
if (render._locked) {
insertPageBreak._retryCount = (insertPageBreak._retryCount || 0) + 1;
if (insertPageBreak._retryCount >= 15) {
insertPageBreak._retryCount = 0;
layer.msg('操作超时,请重试');
return;
}
setTimeout(function () { insertPageBreak(blockIndex); }, 200);
return;
}
insertPageBreak._retryCount = 0;
var cleanHtml = preprocessContent(postData.content_html);
var $temp = $('<div>').html(cleanHtml);
@@ -1191,9 +1223,16 @@ var PhoneImageEngine = (function () {
function removePageBreak(blockIndex) {
// 如果正在渲染,延迟重试
if (render._locked) {
removePageBreak._retryCount = (removePageBreak._retryCount || 0) + 1;
if (removePageBreak._retryCount >= 15) {
removePageBreak._retryCount = 0;
layer.msg('操作超时,请重试');
return;
}
setTimeout(function () { removePageBreak(blockIndex); }, 200);
return;
}
removePageBreak._retryCount = 0;
// 统计这是第几个 page-break
var cleanHtml = preprocessContent(postData.content_html);

View File

@@ -187,13 +187,13 @@
var postData = {
postId: {$post.id},
title: '{$post.title|raw}',
desc: '{$post.desc|default=""}',
coverText: '{$post->getData("cover_text")|default=""}',
title: <?php echo json_encode($post->title ?? ''); ?>,
desc: <?php echo json_encode($post->desc ?? ''); ?>,
coverText: <?php echo json_encode($post->getData('cover_text') ?? ''); ?>,
contentHtml: $('#post-content-html').html(),
poster: '{$post.poster|default=""}',
authorName: '{$post.author_name|default=""}',
createTime: '{$post.create_time_text|default=""}',
poster: <?php echo json_encode($post->poster ?? ''); ?>,
authorName: <?php echo json_encode($post->author_name ?? ''); ?>,
createTime: <?php echo json_encode($post->create_time_text ?? ''); ?>,
categoryName: ''
};