- internal/httpapi:§7.1 全部路由(公开浏览 / 管理端 / 认证 / feed), 服务端统一可见性过滤(含回收站仅 admin 出口)、图片魔数校验与 immutable/no-store 缓存头分流、统一 404 防枚举、slug 自解冲突与 409 字段级错误、fail-only 登录限流(429 + Retry-After)、 设置白名单(永不序列化口令哈希) - internal/webui:go:embed dist + SPA fallback(资产指纹长缓存、 深链回退 index.html)+ html/template 元信息注入(仅可见笔记) - cmd/pure-note:serve/init/backup/gc/version 子命令,优雅停机与 每小时会话清理 - 含全部 §13 测试组:表驱动可见性矩阵、迁移守卫、认证会话、CSRF、 上传、回收站/gc、slug 策略、设置白名单、webui MapFS 单测
130 lines
3.6 KiB
Go
130 lines
3.6 KiB
Go
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,
|
||
})
|
||
}
|
||
feed := rssFeed{
|
||
Version: "2.0",
|
||
Channel: rssChannel{
|
||
Title: ss.SiteTitle,
|
||
Link: base + "/",
|
||
Description: ss.SiteDesc,
|
||
Language: "zh-CN",
|
||
LastBuild: lastBuild.Format(time.RFC1123Z),
|
||
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"))
|
||
}
|