fix: 保留博客HTML并补齐预览菜单

This commit is contained in:
sunlei 2026-07-01 18:13:05 +08:00
parent 616346ccbc
commit 8c3a1d793c
6 changed files with 73 additions and 9 deletions

View File

@ -11,6 +11,8 @@ VALUES
(2041700000000120302, 2041700000000100301, 'BlogArticleEdit', NULL, NULL, NULL, 'Blog:Article:Edit', 'button', '{"title":"common.edit"}', 1, 0),
(2041700000000120303, 2041700000000100301, 'BlogArticleDelete', NULL, NULL, NULL, 'Blog:Article:Delete', 'button', '{"title":"common.delete"}', 1, 0),
(2041700000000120304, 2041700000000100301, 'BlogArticleImport', NULL, NULL, NULL, 'Blog:Article:Import', 'button', '{"title":"导入 WordPress"}', 1, 1),
(2041700000000120305, 2041700000000100301, 'BlogArticlePreviewButton', NULL, NULL, NULL, 'Blog:Article:Preview', 'button', '{"title":"预览"}', 1, 2),
(2041700000000100305, 2041700000000100300, 'BlogArticlePreview', '/blog/article/:articleId/preview', '/blog/article/preview/index', NULL, 'Blog:Article:Preview', 'menu', '{"activePath":"/blog/article","hideInMenu":true,"title":"文章预览"}', 1, 4),
(2041700000000100302, 2041700000000100300, 'BlogCategory', '/blog/category', '/blog/category/list', NULL, 'Blog:Category:List', 'menu', '{"icon":"lucide:folder-tree","title":"分类管理"}', 1, 1),
(2041700000000120311, 2041700000000100302, 'BlogCategoryCreate', NULL, NULL, NULL, 'Blog:Category:Create', 'button', '{"title":"common.create"}', 1, 0),
(2041700000000120312, 2041700000000100302, 'BlogCategoryEdit', NULL, NULL, NULL, 'Blog:Category:Edit', 'button', '{"title":"common.edit"}', 1, 0),

View File

@ -245,6 +245,8 @@ VALUES
(2041700000000120302, 2041700000000100301, 'BlogArticleEdit', NULL, NULL, NULL, 'Blog:Article:Edit', 'button', '{"title":"common.edit"}', 1, 0),
(2041700000000120303, 2041700000000100301, 'BlogArticleDelete', NULL, NULL, NULL, 'Blog:Article:Delete', 'button', '{"title":"common.delete"}', 1, 0),
(2041700000000120304, 2041700000000100301, 'BlogArticleImport', NULL, NULL, NULL, 'Blog:Article:Import', 'button', '{"title":"导入 WordPress"}', 1, 1),
(2041700000000120305, 2041700000000100301, 'BlogArticlePreviewButton', NULL, NULL, NULL, 'Blog:Article:Preview', 'button', '{"title":"预览"}', 1, 2),
(2041700000000100305, 2041700000000100300, 'BlogArticlePreview', '/blog/article/:articleId/preview', '/blog/article/preview/index', NULL, 'Blog:Article:Preview', 'menu', '{"activePath":"/blog/article","hideInMenu":true,"title":"文章预览"}', 1, 4),
(2041700000000100302, 2041700000000100300, 'BlogCategory', '/blog/category', '/blog/category/list', NULL, 'Blog:Category:List', 'menu', '{"icon":"lucide:folder-tree","title":"分类管理"}', 1, 1),
(2041700000000120311, 2041700000000100302, 'BlogCategoryCreate', NULL, NULL, NULL, 'Blog:Category:Create', 'button', '{"title":"common.create"}', 1, 0),
(2041700000000120312, 2041700000000100302, 'BlogCategoryEdit', NULL, NULL, NULL, 'Blog:Category:Edit', 'button', '{"title":"common.edit"}', 1, 0),

View File

@ -362,7 +362,7 @@ export class BlogArticleService {
);
} else {
nextArticle.contentMarkdown = current?.contentMarkdown || '';
nextArticle.contentHtml = await this.markdownService.renderToHtml(
nextArticle.contentHtml = await this.markdownService.sanitizeHtml(
body.content,
);
}

View File

@ -643,12 +643,16 @@ const routeTestCases: Record<string, RouteTestCase> = {
const response = await request(server)
.post('/blog/article/update')
.send({
content: '<pre class="wp-block-code hljs-codeblock"></pre>',
contentFormat: 'html',
id: blogArticle.id,
title: '本地文章',
})
.expect(200);
expect(blogArticleServiceMock.update).toHaveBeenCalledWith({
content: '<pre class="wp-block-code hljs-codeblock"></pre>',
contentFormat: 'html',
id: blogArticle.id,
title: '本地文章',
});

View File

@ -119,6 +119,38 @@ describe('BlogArticleService', () => {
});
});
it('sanitizes html content without markdown rerendering when updating imported articles', async () => {
const rawArgonCodeblock =
'<pre class="wp-block-code hljs-codeblock" id="demo"><code class="hljs sql"><table class="hljs-ln"><tbody><tr><td class="hljs-ln-line hljs-ln-code">select 1;</td></tr></tbody></table></code><div class="hljs-control"><div class="hljs-control-btn hljs-control-copy"></div></div></pre><script>alert(1)</script>';
const currentArticle = {
contentHtml: '<p>旧正文</p>',
contentMarkdown: '旧正文',
id: '50',
isDeleted: false,
slug: 'wordpress-post',
status: 'publish',
title: 'WordPress 文章',
};
repository.findOne.mockResolvedValue(currentArticle);
await service.update({
content: rawArgonCodeblock,
contentFormat: 'html',
id: '50',
title: 'WordPress 文章',
});
expect(markdownService.renderToHtml).not.toHaveBeenCalled();
expect(markdownService.sanitizeHtml).toHaveBeenCalledWith(rawArgonCodeblock);
expect(repository.save).toHaveBeenCalledWith(
expect.objectContaining({
contentHtml:
'<pre class="wp-block-code hljs-codeblock" id="demo"><code class="hljs sql"><table class="hljs-ln"><tbody><tr><td class="hljs-ln-line hljs-ln-code">select 1;</td></tr></tbody></table></code><div class="hljs-control"><div class="hljs-control-btn hljs-control-copy"></div></div></pre>',
contentMarkdown: '旧正文',
}),
);
});
it('queries public list with publish status and maps response fields', async () => {
const builder = createQueryBuilderMock([
{

View File

@ -2,7 +2,20 @@ import { readFileSync } from 'node:fs';
import { join } from 'node:path';
describe('blog-menu.sql', () => {
const sql = readFileSync(join(process.cwd(), 'sql/blog-menu.sql'), 'utf8');
const sqlFiles = [
readFileSync(join(process.cwd(), 'sql/blog-menu.sql'), 'utf8'),
readFileSync(join(process.cwd(), 'sql/vben-admin-init.sql'), 'utf8'),
];
/**
* SQL
* @param snippet - SQL
*/
function expectAllSqlToContain(snippet: string) {
sqlFiles.forEach((sql) => {
expect(sql).toContain(snippet);
});
}
/**
*
@ -17,7 +30,7 @@ describe('blog-menu.sql', () => {
component: string,
authCode: string,
) {
expect(sql).toContain(
expectAllSqlToContain(
`'${name}', '${path}', '${component}', NULL, '${authCode}', 'menu'`,
);
}
@ -30,18 +43,20 @@ describe('blog-menu.sql', () => {
'BlogArticleEdit',
'BlogArticleDelete',
'BlogArticleImport',
'BlogArticlePreview',
'BlogArticlePreviewButton',
'BlogCategory',
'BlogTag',
'BlogTheme',
'BlogThemeSave',
'BlogThemeImport',
].forEach((name) => {
expect(sql).toContain(`'${name}'`);
expectAllSqlToContain(`'${name}'`);
});
});
it('keeps database menu routes aligned with admin route components', () => {
expect(sql).toContain(
expectAllSqlToContain(
"'Blog', '/blog', NULL, '/blog/article', NULL, 'catalog'",
);
expectMenuRoute(
@ -50,6 +65,14 @@ describe('blog-menu.sql', () => {
'/blog/article/list',
'Blog:Article:List',
);
expectAllSqlToContain(
"'BlogArticlePreview', '/blog/article/:articleId/preview', '/blog/article/preview/index', NULL, 'Blog:Article:Preview', 'menu'",
);
expectAllSqlToContain(
`'BlogArticlePreviewButton', NULL, NULL, NULL, 'Blog:Article:Preview', 'button'`,
);
expectAllSqlToContain('"hideInMenu":true');
expectAllSqlToContain('"activePath":"/blog/article"');
expectMenuRoute(
'BlogCategory',
'/blog/category',
@ -66,9 +89,10 @@ describe('blog-menu.sql', () => {
});
it('grants blog menus to admin roles without deleting existing role menus', () => {
expect(sql).toContain('INSERT IGNORE INTO `admin_role_menu`');
expect(sql).toContain("menu.`name` LIKE 'Blog%'");
expect(sql).toContain("role.`role_code` IN ('super', 'admin')");
expect(sql).not.toMatch(/DELETE\s+FROM\s+`admin_role_menu`/i);
const blogMenuSql = sqlFiles[0] || '';
expect(blogMenuSql).toContain('INSERT IGNORE INTO `admin_role_menu`');
expect(blogMenuSql).toContain("menu.`name` LIKE 'Blog%'");
expect(blogMenuSql).toContain("role.`role_code` IN ('super', 'admin')");
expect(blogMenuSql).not.toMatch(/DELETE\s+FROM\s+`admin_role_menu`/i);
});
});