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) + "\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 }