package api
import (
"encoding/xml"
"html"
"net/http"
"strings"
"time"
"unicode/utf8"
"git.golem15.com/golem15/sm-journal-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
)
// RSS is GET /_journal/api/v1/rss. Empty or disabled settings still yield a
// well-formed RSS 2.0 document (D-14).
func RSS(app *backpack.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
db, err := gdb(app)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
var settings models.Settings
if err := db.WithContext(r.Context()).First(&settings, 1).Error; err != nil {
settings = models.Settings{RSSLanguage: "en-us", RSSPostsPerFeed: 20, RSSEnabled: true}
}
limit := settings.RSSPostsPerFeed
if limit <= 0 {
limit = 20
}
var posts []models.Post
if err := applyPublished(db.WithContext(r.Context()).Model(&models.Post{})).
Preload("User").Preload("Categories").
Order("published_at DESC").Limit(limit).Find(&posts).Error; err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
title := "Journal"
if settings.RSSTitle != nil && strings.TrimSpace(*settings.RSSTitle) != "" {
title = strings.TrimSpace(*settings.RSSTitle)
}
desc := ""
if settings.RSSDescription != nil {
desc = *settings.RSSDescription
}
site := requestOrigin(r, app)
self := site + r.URL.RequestURI()
var b strings.Builder
b.WriteString(`` + "\n")
b.WriteString(`` + "\n")
b.WriteString("\n")
writeXMLTag(&b, "title", title)
writeXMLTag(&b, "link", site+"/")
writeXMLTag(&b, "description", desc)
if settings.RSSLanguage != "" {
writeXMLTag(&b, "language", settings.RSSLanguage)
}
if settings.RSSCopyright != nil && strings.TrimSpace(*settings.RSSCopyright) != "" {
writeXMLTag(&b, "copyright", *settings.RSSCopyright)
}
writeXMLTag(&b, "lastBuildDate", time.Now().UTC().Format(time.RFC1123Z))
writeXMLTag(&b, "generator", "Golem15 Journal")
b.WriteString(`` + "\n")
if settings.RSSImageURL != nil && strings.TrimSpace(*settings.RSSImageURL) != "" {
b.WriteString("\n")
writeXMLTag(&b, "url", *settings.RSSImageURL)
writeXMLTag(&b, "title", title)
writeXMLTag(&b, "link", site+"/")
b.WriteString("\n")
}
for i := range posts {
p := &posts[i]
postURL := site + "/blog/" + p.Slug
summary := p.Excerpt
if strings.TrimSpace(summary) == "" {
summary = truncateRunes(stripTags(p.ContentHTML), 300)
}
b.WriteString("- \n")
writeXMLTag(&b, "title", p.Title)
writeXMLTag(&b, "link", postURL)
b.WriteString(`` + xmlEsc(postURL) + "\n")
if p.PublishedAt != nil {
writeXMLTag(&b, "pubDate", p.PublishedAt.UTC().Format(time.RFC1123Z))
}
b.WriteString("\n")
if settings.RSSIncludeContent && p.ContentHTML != "" {
b.WriteString("\n")
}
if p.User != nil {
creator := strings.TrimSpace(p.User.FirstName + " " + p.User.LastName)
if creator != "" {
b.WriteString("" + xmlEsc(creator) + "\n")
}
}
for _, cat := range p.Categories {
writeXMLTag(&b, "category", cat.Name)
}
b.WriteString("
\n")
}
b.WriteString("\n\n")
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(b.String()))
}
}
func writeXMLTag(b *strings.Builder, name, value string) {
b.WriteString("<" + name + ">" + xmlEsc(value) + "" + name + ">\n")
}
func xmlEsc(s string) string {
var b strings.Builder
_ = xml.EscapeText(&b, []byte(s))
return b.String()
}
func cdata(s string) string {
return strings.ReplaceAll(s, "]]>", "]]>")
}
func requestOrigin(r *http.Request, app *backpack.App) string {
if app != nil && app.Config != nil {
if u := strings.TrimRight(strings.TrimSpace(app.Config.String("url")), "/"); u != "" {
return u
}
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
return scheme + "://" + r.Host
}
func truncateRunes(s string, n int) string {
if utf8.RuneCountInString(s) <= n {
return s
}
runes := []rune(html.UnescapeString(s))
if len(runes) > n {
return string(runes[:n])
}
return s
}