feat: opaque session、安装/发帖 SSR 与最小 Admin 后台
浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings; 落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
97
services/compose_body.go
Normal file
97
services/compose_body.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"html"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
mdImageRe = regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`)
|
||||
mdLinkRe = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
||||
)
|
||||
|
||||
// ComposeBodyToHTML 将发帖表单正文转为可消毒 HTML。
|
||||
// 若已含块级 HTML 标签则原样交 Sanitize;否则按纯文本/轻量 Markdown 转段落。
|
||||
func ComposeBodyToHTML(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
lower := strings.ToLower(raw)
|
||||
if strings.Contains(lower, "<p") || strings.Contains(lower, "<div") ||
|
||||
strings.Contains(lower, "<h1") || strings.Contains(lower, "<ul") ||
|
||||
strings.Contains(lower, "<ol") || strings.Contains(lower, "<pre") ||
|
||||
strings.Contains(lower, "<blockquote") {
|
||||
return raw
|
||||
}
|
||||
|
||||
// 先转义,再恢复轻量 md 图片/链接
|
||||
esc := html.EscapeString(raw)
|
||||
esc = mdImageRe.ReplaceAllStringFunc(esc, func(m string) string {
|
||||
sub := mdImageRe.FindStringSubmatch(m)
|
||||
if len(sub) != 3 {
|
||||
return m
|
||||
}
|
||||
alt, src := sub[1], sub[2]
|
||||
// 仅允许站内 uploads 或 http(s)
|
||||
if !safeComposeURL(src) {
|
||||
return m
|
||||
}
|
||||
return `<p><img src="` + html.EscapeString(src) + `" alt="` + alt + `"></p>`
|
||||
})
|
||||
esc = mdLinkRe.ReplaceAllStringFunc(esc, func(m string) string {
|
||||
sub := mdLinkRe.FindStringSubmatch(m)
|
||||
if len(sub) != 3 {
|
||||
return m
|
||||
}
|
||||
text, href := sub[1], sub[2]
|
||||
if !safeComposeURL(href) {
|
||||
return m
|
||||
}
|
||||
return `<a href="` + html.EscapeString(href) + `" rel="noopener noreferrer">` + text + `</a>`
|
||||
})
|
||||
|
||||
parts := strings.Split(esc, "\n\n")
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
// 已是独立 img 段
|
||||
if strings.HasPrefix(p, "<p><img ") {
|
||||
b.WriteString(p)
|
||||
continue
|
||||
}
|
||||
p = strings.ReplaceAll(p, "\n", "<br>\n")
|
||||
b.WriteString("<p>")
|
||||
b.WriteString(p)
|
||||
b.WriteString("</p>\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func safeComposeURL(u string) bool {
|
||||
u = strings.TrimSpace(u)
|
||||
if strings.HasPrefix(u, "/uploads/") || strings.HasPrefix(u, "/media/") {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://")
|
||||
}
|
||||
|
||||
// HTMLToComposePlain 编辑页回显:去掉简单标签便于 textarea 编辑(尽力而为)
|
||||
func HTMLToComposePlain(htmlBody string) string {
|
||||
s := strings.TrimSpace(htmlBody)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
// img → markdown
|
||||
imgRe := regexp.MustCompile(`(?i)<img[^>]+src="([^"]+)"[^>]*>`)
|
||||
s = imgRe.ReplaceAllString(s, "")
|
||||
s = regexp.MustCompile(`(?i)</p>\s*<p>`).ReplaceAllString(s, "\n\n")
|
||||
s = regexp.MustCompile(`(?i)<br\s*/?>`).ReplaceAllString(s, "\n")
|
||||
s = regexp.MustCompile(`(?i)</?p[^>]*>`).ReplaceAllString(s, "")
|
||||
s = regexp.MustCompile(`(?i)<[^>]+>`).ReplaceAllString(s, "")
|
||||
return strings.TrimSpace(html.UnescapeString(s))
|
||||
}
|
||||
Reference in New Issue
Block a user