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

View File

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