Files
pure-note/internal/httpapi/feed.go
T
wangairnan 1272d680a8 fix(httpapi): no-store 接线、图片缓存头时序、解压炸弹上限与 RSS 空日期
/api/admin/* 挂 NoStore、/api/me 内联 no-store(P1-3); 图片缓存头移至数据读取成功后,404 不携带 public immutable(P2-9); 上传先 DecodeConfig 限制像素 ≤2^25 再解码(P2-12); 无公开笔记时省略 lastBuildDate(P2-10)。
2026-09-08 17:32:52 +08:00

135 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package httpapi
import (
"encoding/xml"
"net/http"
"time"
"pure-note/internal/markdown"
)
// ---- RSS 2.0(§7.1:仅公开;服务端 goldmark+bluemonday 渲染)----
type rssFeed struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Channel rssChannel `xml:"channel"`
}
type rssChannel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Language string `xml:"language,omitempty"`
LastBuild string `xml:"lastBuildDate,omitempty"`
Items []rssItem `xml:"item"`
}
type rssItem struct {
Title string `xml:"title"`
Link string `xml:"link"`
GUID string `xml:"guid"`
PubDate string `xml:"pubDate"` // RFC 822(RSS 2.0 规范)
Description string `xml:"description"` // 已清洗的 HTML(经 encoding/xml 自动转义)
}
// feedItemLimit RSS 条目上限(个人规模足够,避免全量渲染)。
const feedItemLimit = 50
func (s *Server) handleRSS(w http.ResponseWriter, r *http.Request) {
ss, err := s.st.GetSiteSettings()
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
base := baseURL(r)
notes, err := s.st.ListPublicNotesFull(feedItemLimit)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
items := make([]rssItem, 0, len(notes))
var lastBuild time.Time
for _, n := range notes {
// 服务端渲染:goldmark(默认转义)→ bluemonday 白名单(§7.5)
html := markdown.Render(n.Content)
pub := time.Unix(n.UpdatedAt, 0)
if pub.After(lastBuild) {
lastBuild = pub
}
items = append(items, rssItem{
Title: n.Title,
Link: base + "/notes/" + n.Slug,
GUID: base + "/notes/" + n.Slug,
PubDate: pub.Format(time.RFC1123Z),
Description: html,
})
}
// 无公开笔记时省略 lastBuildDate(零值 Format 会产出公元 1 年的非法日期)
lastBuildStr := ""
if !lastBuild.IsZero() {
lastBuildStr = lastBuild.Format(time.RFC1123Z)
}
feed := rssFeed{
Version: "2.0",
Channel: rssChannel{
Title: ss.SiteTitle,
Link: base + "/",
Description: ss.SiteDesc,
Language: "zh-CN",
LastBuild: lastBuildStr,
Items: items,
},
}
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=300")
w.Write([]byte(xml.Header))
enc := xml.NewEncoder(w)
enc.Indent("", " ")
_ = enc.Encode(feed)
}
// ---- sitemap(仅公开笔记)----
type urlSet struct {
XMLName xml.Name `xml:"urlset"`
XMLNS string `xml:"xmlns,attr"`
URLs []siteURL `xml:"url"`
}
type siteURL struct {
Loc string `xml:"loc"`
LastMod time.Time `xml:"lastmod,omitempty"`
}
func (s *Server) handleSitemap(w http.ResponseWriter, r *http.Request) {
base := baseURL(r)
set := urlSet{XMLNS: "http://www.sitemaps.org/schemas/sitemap/0.9"}
set.URLs = append(set.URLs, siteURL{Loc: base + "/"})
notes, _, err := s.st.ListPublicNotes(1, 10000, "")
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
for _, n := range notes {
set.URLs = append(set.URLs, siteURL{
Loc: base + "/notes/" + n.Slug,
LastMod: time.Unix(n.UpdatedAt, 0),
})
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write([]byte(xml.Header))
enc := xml.NewEncoder(w)
enc.Indent("", " ")
_ = enc.Encode(set)
}
// ---- robots.txt ----
func (s *Server) handleRobots(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Write([]byte("User-agent: *\nAllow: /\nDisallow: /admin\n\nSitemap: " + baseURL(r) + "/sitemap.xml\n"))
}