- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
192 lines
5.0 KiB
Go
192 lines
5.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"html"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go_blog/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// RSS 2.0 XML 结构定义。
|
|
|
|
// RSS 是 RSS 2.0 feed 的根元素。
|
|
type RSS struct {
|
|
XMLName xml.Name `xml:"rss"`
|
|
Version string `xml:"version,attr"`
|
|
Channel *Channel `xml:"channel"`
|
|
}
|
|
|
|
// Channel 表示包含 feed 元数据和条目(items)的 RSS channel。
|
|
type Channel struct {
|
|
Title string `xml:"title"`
|
|
Link string `xml:"link"`
|
|
Description string `xml:"description"`
|
|
Language string `xml:"language"`
|
|
LastBuildDate string `xml:"lastBuildDate,omitempty"`
|
|
Items []Item `xml:"item"`
|
|
}
|
|
|
|
// Item 表示 RSS feed 中的单篇文章。
|
|
type Item struct {
|
|
Title string `xml:"title"`
|
|
Link string `xml:"link"`
|
|
Description string `xml:"description"`
|
|
Author string `xml:"author,omitempty"`
|
|
PubDate string `xml:"pubDate"`
|
|
GUID string `xml:"guid"`
|
|
}
|
|
|
|
// RSSFeed 生成最新已发布文章的 RSS 2.0 feed。
|
|
func RSSFeed(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// 确定用于站点元数据的当前语言。
|
|
lang, exists := c.Get("lang")
|
|
if !exists {
|
|
lang = "en"
|
|
}
|
|
langStr := lang.(string)
|
|
|
|
// 获取站点设置用于 feed 元数据。
|
|
siteSetting := &models.SiteSetting{}
|
|
db.First(siteSetting)
|
|
|
|
// SECURITY_TODO #16:配置后使用设置中的规范化站点 URL——
|
|
// 请求的 Host 可被攻击者控制,否则会污染 feed 中的每个链接。
|
|
// 对于旧部署则回退并给出警告。
|
|
var baseURL string
|
|
if u := strings.TrimSpace(siteSetting.SiteURL); u != "" {
|
|
baseURL = strings.TrimRight(u, "/")
|
|
} else {
|
|
log.Printf("WARNING: Site URL is not set in settings; RSS links use request Host %q (set settings_site_url to a fixed URL)",
|
|
c.Request.Host)
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
|
scheme = "https"
|
|
}
|
|
baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host)
|
|
}
|
|
|
|
// 获取最新 20 篇已发布文章。
|
|
var articles []models.Article
|
|
db.Where("status = ?", models.ArticlePublished).
|
|
Preload("Author").
|
|
Order(publishedArticleOrder).
|
|
Limit(20).
|
|
Find(&articles)
|
|
|
|
// 构建 channel 元数据。
|
|
channel := &Channel{
|
|
Title: siteSetting.LogoText(langStr),
|
|
Link: baseURL,
|
|
Description: siteSetting.HomeSubtitle(langStr),
|
|
Language: getRSSLanguage(langStr),
|
|
Items: make([]Item, 0, len(articles)),
|
|
}
|
|
|
|
// 将 lastBuildDate 设置为最新文章的发布日期。
|
|
if len(articles) > 0 && articles[0].PublishedAt != nil {
|
|
channel.LastBuildDate = formatRSSTime(*articles[0].PublishedAt)
|
|
}
|
|
|
|
// 将文章转换为 RSS 条目。
|
|
for _, article := range articles {
|
|
item := Item{
|
|
Title: article.Title,
|
|
Link: fmt.Sprintf("%s/article/%s", baseURL, article.Slug),
|
|
Description: getArticleDescription(&article),
|
|
PubDate: formatRSSTime(getArticlePubDate(&article)),
|
|
GUID: fmt.Sprintf("%s/article/%s", baseURL, article.Slug),
|
|
}
|
|
|
|
// 添加作者信息。
|
|
if article.Author.DisplayName != "" {
|
|
item.Author = article.Author.DisplayName
|
|
} else {
|
|
item.Author = article.Author.Username
|
|
}
|
|
|
|
channel.Items = append(channel.Items, item)
|
|
}
|
|
|
|
// 构建 RSS feed。
|
|
feed := &RSS{
|
|
Version: "2.0",
|
|
Channel: channel,
|
|
}
|
|
|
|
// 设置正确的内容类型并返回 XML。
|
|
c.Header("Content-Type", "application/rss+xml; charset=utf-8")
|
|
c.XML(http.StatusOK, feed)
|
|
}
|
|
}
|
|
|
|
// getRSSLanguage 将内部语言代码转换为 RSS 语言格式。
|
|
func getRSSLanguage(lang string) string {
|
|
switch lang {
|
|
case "zh":
|
|
return "zh-CN"
|
|
case "en":
|
|
return "en-US"
|
|
default:
|
|
return "en-US"
|
|
}
|
|
}
|
|
|
|
// formatRSSTime 将 time.Time 格式化为 RSS 2.0 要求的 RFC1123Z 格式。
|
|
func formatRSSTime(t time.Time) string {
|
|
return t.Format(time.RFC1123Z)
|
|
}
|
|
|
|
// getArticlePubDate 返回文章的发布日期,无则回退到创建日期。
|
|
func getArticlePubDate(article *models.Article) time.Time {
|
|
if article.PublishedAt != nil {
|
|
return *article.PublishedAt
|
|
}
|
|
return article.CreatedAt
|
|
}
|
|
|
|
// getArticleDescription 返回文章用于 RSS 的描述。
|
|
// 优先使用摘要字段;没有则回退到截断的正文。
|
|
func getArticleDescription(article *models.Article) string {
|
|
if article.Summary != "" {
|
|
return html.EscapeString(article.Summary)
|
|
}
|
|
|
|
// 去除 HTML 标签并将正文截断到 200 个字符。
|
|
content := stripHTMLTags(article.Content)
|
|
if len(content) > 200 {
|
|
content = content[:200] + "..."
|
|
}
|
|
return html.EscapeString(content)
|
|
}
|
|
|
|
// stripHTMLTags 从字符串中去除 HTML 标签(基础实现)。
|
|
func stripHTMLTags(s string) string {
|
|
// 通过查找 < 与 > 的配对去除 HTML 标签。
|
|
var result strings.Builder
|
|
inTag := false
|
|
for _, r := range s {
|
|
if r == '<' {
|
|
inTag = true
|
|
continue
|
|
}
|
|
if r == '>' {
|
|
inTag = false
|
|
continue
|
|
}
|
|
if !inTag {
|
|
result.WriteRune(r)
|
|
}
|
|
}
|
|
// 清理多余空格并去除首尾空白。
|
|
cleaned := strings.Join(strings.Fields(result.String()), " ")
|
|
return cleaned
|
|
}
|