From 22a302315ae44b7bede7f2c614d702dd1ff3c5fa Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Thu, 28 May 2026 02:16:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20wangEditor=20=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=20ByteMD=20Markdown=20=E7=BC=96=E8=BE=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 wangEditor 5 库文件(1.27MB JS + 14KB CSS,含手动 hack) - 集成 ByteMD 1.22.0(基于 CodeMirror 5,不依赖 contentEditable) - 编辑器输出 Markdown 原文,后端 goldmark + bluemonday 双重防护渲染 HTML - 删除 editor.js 中 scrollIntoView hack、code-lang-label DOM 注入、PreviewAPI 调用 - 清理 posts.css 中 ~200 行 wangEditor 样式覆盖(w-e-* 类名) - 清理 show.html 中 code-lang-label JS 注入(goldmark 自带 language-xxx class) - 更新 CSP 安全策略:移除 CodeMirror 注释,保留 esm.sh CDN - go.mod:bluemonday 和 goldmark 均保留为直接依赖(纵深防御) --- go.mod | 8 +- internal/middleware/security.go | 5 +- internal/service/post_service.go | 52 +- templates/MetaLab-2026/html/posts/new.html | 49 +- templates/MetaLab-2026/html/posts/show.html | 12 - templates/MetaLab-2026/static/css/posts.css | 230 +- templates/MetaLab-2026/static/js/editor.js | 616 +- .../MetaLab-2026/static/lib/wangeditor.css | 27 - .../MetaLab-2026/static/lib/wangeditor.js | 24129 ---------------- 9 files changed, 222 insertions(+), 24906 deletions(-) delete mode 100644 templates/MetaLab-2026/static/lib/wangeditor.css delete mode 100644 templates/MetaLab-2026/static/lib/wangeditor.js diff --git a/go.mod b/go.mod index eb61687..345a592 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,14 @@ module metazone.cc/metalab go 1.26.0 require ( + github.com/chai2010/webp v1.4.0 github.com/gin-gonic/gin v1.12.0 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/microcosm-cc/bluemonday v1.0.27 github.com/spf13/viper v1.21.0 + github.com/yuin/goldmark v1.8.2 golang.org/x/crypto v0.52.0 + golang.org/x/image v0.41.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 ) @@ -16,7 +20,6 @@ require ( github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect - github.com/chai2010/webp v1.4.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect @@ -38,7 +41,6 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -52,11 +54,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect - github.com/yuin/goldmark v1.8.2 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/image v0.41.0 // indirect golang.org/x/net v0.54.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect diff --git a/internal/middleware/security.go b/internal/middleware/security.go index 08c43ce..c47965a 100644 --- a/internal/middleware/security.go +++ b/internal/middleware/security.go @@ -9,13 +9,14 @@ import ( func SecurityHeaders() gin.HandlerFunc { return func(c *gin.Context) { // Content-Security-Policy - // script-src: 本站 + esm.sh CDN (CodeMirror 6 ESM 模块) + // script-src: 本站 + esm.sh CDN (ByteMD ESM 模块) + // style-src: 本站 + esm.sh (ByteMD/highlight.js CSS) // connect-src: 本站 + esm.sh (source map 请求) // img-src: 本站 + data: URI (头像裁切) + blob: (粘贴图片) c.Header("Content-Security-Policy", "default-src 'self'; "+ "script-src 'self' 'unsafe-inline' https://esm.sh; "+ - "style-src 'self' 'unsafe-inline'; "+ + "style-src 'self' 'unsafe-inline' https://esm.sh; "+ "img-src 'self' data: blob:; "+ "connect-src 'self' https://esm.sh") diff --git a/internal/service/post_service.go b/internal/service/post_service.go index 32cf005..09465cc 100644 --- a/internal/service/post_service.go +++ b/internal/service/post_service.go @@ -12,9 +12,10 @@ import ( "github.com/yuin/goldmark" ) -// ucgPolicy 对 Goldmark 输出的 HTML 做安全消毒 -// UGC 默认策略会剥离 code/pre 的 class 属性,需要显式放行以保留 language-xxx -var ucgPolicy = func() *bluemonday.Policy { +// sanitizePolicy 纵深防御:对 goldmark 输出的 HTML 再做一层消毒 +// goldmark 默认已过滤原始 HTML 和危险链接,但 bluemonday 作为第二道防线 +// UGC 策略会剥离 code/pre 的 class 属性,需要显式放行以保留 language-xxx +var sanitizePolicy = func() *bluemonday.Policy { p := bluemonday.UGCPolicy() p.AllowAttrs("class").OnElements("code", "pre") return p @@ -48,29 +49,32 @@ func NewPostService(repo postStore, ss *config.SiteSettings) *PostService { } } -// auditEnabled 审核是否开启(文案审核与全局审核开关一致) +// auditEnabled 审核是否开启 func (s *PostService) auditEnabled() bool { return s.ss.IsAuditEnabled() } -// sanitizeHTML 对编辑器输出的 HTML 做安全消毒(bluemonday) -// 编辑器提交的是富文本 HTML,不再经过 Markdown 渲染 -func (s *PostService) sanitizeHTML(body string) string { - return ucgPolicy.Sanitize(body) -} - -// RenderMarkdown 对外暴露,用于预览(从纯文本 Markdown 渲染为 HTML) -func (s *PostService) RenderMarkdown(body string) (string, error) { +// renderMarkdown 将 Markdown 文本渲染为安全 HTML +// goldmark(默认安全模式)→ bluemonday(纵深防御) +func (s *PostService) renderMarkdown(body string) (string, error) { var buf bytes.Buffer if err := s.md.Convert([]byte(body), &buf); err != nil { - return "", err + return "", fmt.Errorf("markdown render: %w", err) } - return ucgPolicy.Sanitize(buf.String()), nil + return sanitizePolicy.Sanitize(buf.String()), nil +} + +// RenderMarkdown 对外暴露,用于预览 API +func (s *PostService) RenderMarkdown(body string) (string, error) { + return s.renderMarkdown(body) } // Create 创建帖子 func (s *PostService) Create(userID uint, title, body string) (*model.Post, error) { - bodyHTML := s.sanitizeHTML(body) + bodyHTML, err := s.renderMarkdown(body) + if err != nil { + return nil, err + } status := model.PostStatusApproved if s.auditEnabled() { @@ -78,11 +82,11 @@ func (s *PostService) Create(userID uint, title, body string) (*model.Post, erro } post := &model.Post{ - Title: title, - Body: body, - BodyHTML: bodyHTML, - UserID: userID, - Status: status, + Title: title, + Body: body, + BodyHTML: bodyHTML, + UserID: userID, + Status: status, AllowComment: true, } if err := s.repo.Create(post); err != nil { @@ -121,14 +125,18 @@ func (s *PostService) Update(postID uint, title, body string) error { return common.ErrUserNotFound } - // pending / locked 禁止编辑 if post.Status == model.PostStatusPending || post.Status == model.PostStatusLocked { return fmt.Errorf("当前状态不允许编辑") } + bodyHTML, err := s.renderMarkdown(body) + if err != nil { + return err + } + post.Title = title post.Body = body - post.BodyHTML = s.sanitizeHTML(body) + post.BodyHTML = bodyHTML // rejected 状态编辑后自动重置为 draft if post.Status == model.PostStatusRejected { diff --git a/templates/MetaLab-2026/html/posts/new.html b/templates/MetaLab-2026/html/posts/new.html index 6b4ee1a..2f4fefc 100644 --- a/templates/MetaLab-2026/html/posts/new.html +++ b/templates/MetaLab-2026/html/posts/new.html @@ -1,35 +1,16 @@ {{template "layout/header.html" .}} - + + @@ -48,30 +29,18 @@ maxlength="200" required autofocus> - {{/* 编辑器主体:编辑区 + 预览区 */}} + {{/* 编辑器主体 */}}
-
-
-
-
- -
-
-
- 预览将在此处显示... -
+
{{/* 状态栏 */}}
0 字 - 0 行 - 约 1 分钟 - - 预览 + 全屏
@@ -80,6 +49,4 @@ {{template "layout/footer.html" .}} - - - + diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html index 5d537aa..ed9c7d4 100644 --- a/templates/MetaLab-2026/html/posts/show.html +++ b/templates/MetaLab-2026/html/posts/show.html @@ -48,18 +48,6 @@