/*
-SGBlog -- Git-based CGI blogging engine
+SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
This program is free software: you can redistribute it and/or modify
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-// Git-based CGI blogging engine
package main
import (
"bytes"
"compress/gzip"
+ "crypto/sha1"
"encoding/hex"
"encoding/json"
"encoding/xml"
)
const (
- AtomFeed = "feed.atom"
+ AtomPostsFeed = "feed.atom"
+ AtomCommentsFeed = "comments.atom"
)
var (
}
}
+func bytes2uuid(b []byte) string {
+ raw := new([16]byte)
+ copy(raw[:], b)
+ raw[6] = (raw[6] & 0x0F) | uint8(4<<4) // version 4
+ return fmt.Sprintf("%x-%x-%x-%x-%x", raw[0:4], raw[4:6], raw[6:8], raw[8:10], raw[10:])
+}
+
func serveHTTP() {
cfgPath := os.Getenv("SGBLOG_CFG")
if cfgPath == "" {
}
defaultLinks = append(defaultLinks, `<link rel="top" href="`+cfg.URLPrefix+`/" title="top">`)
- atomURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomFeed
- defaultLinks = append(defaultLinks, `<link rel="alternate" title="Atom feed" href="`+atomURL+`" type="application/atom+xml">`)
+ atomPostsURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomPostsFeed
+ atomCommentsURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomCommentsFeed
headHash, err := initRepo(cfg)
if err != nil {
var table bytes.Buffer
table.WriteString(
"<table border=1>\n" +
- "<caption>Comments</caption>\n<tr>" +
"<th>N</th>" +
"<th>When</th>" +
"<th>Title</th>" +
table.WriteString("</table>")
var href string
- var links []string
+ links := []string{`<link rel="alternate" title="Posts feed" href="` + atomPostsURL + `" type="application/atom+xml">`}
var refs bytes.Buffer
+ if commentsTree != nil {
+ links = append(links, `<link rel="alternate" title="Comments feed" href="`+atomCommentsURL+`" type="application/atom+xml">`)
+ }
if offset > 0 {
if offsetPrev := offset - PageEntries; offsetPrev > 0 {
href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offsetPrev)
} else {
href = cfg.URLPrefix + "/"
}
- links = append(links, `<link rel="prev" href="`+href+`" title="newer">`)
+ links = append(links, `<link rel="prev" href="`+href+`" title="prev">`)
refs.WriteString("\n" + makeA(href, "[prev]"))
}
if !logEnded {
href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offset+PageEntries)
- links = append(links, `<link rel="next" href="`+href+`" title="older">`)
+ links = append(links, `<link rel="next" href="`+href+`" title="next">`)
refs.WriteString("\n" + makeA(href, "[next]"))
}
out.Write(table.Bytes())
out.Write(refs.Bytes())
out.Write([]byte("\n"))
- } else if pathInfo == "/"+AtomFeed {
+ } else if pathInfo == "/"+AtomPostsFeed {
commit, err := repo.CommitObject(*headHash)
if err != nil {
makeErr(err)
}
- etagHash.Write([]byte("ATOM"))
+ etagHash.Write([]byte("ATOM POSTS"))
etagHash.Write(commit.Hash[:])
checkETag(etagHash)
feed := atom.Feed{
Title: cfg.Title,
ID: cfg.AtomId,
Updated: atom.Time(commit.Author.When),
- Link: []atom.Link{{
- Rel: "self",
- Href: atomURL,
- }},
- Author: &atom.Person{Name: cfg.AtomAuthor},
+ Link: []atom.Link{{Rel: "self", Href: atomPostsURL}},
+ Author: &atom.Person{Name: cfg.AtomAuthor},
}
repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
if err != nil {
if err != nil {
break
}
-
- feedIdRaw := new([16]byte)
- copy(feedIdRaw[:], commit.Hash[:])
- feedIdRaw[6] = (feedIdRaw[6] & 0x0F) | uint8(4<<4) // version 4
- feedId := fmt.Sprintf(
- "%x-%x-%x-%x-%x",
- feedIdRaw[0:4],
- feedIdRaw[4:6],
- feedIdRaw[6:8],
- feedIdRaw[8:10],
- feedIdRaw[10:],
- )
-
lines := msgSplit(commit.Message)
feed.Entry = append(feed.Entry, &atom.Entry{
Title: lines[0],
- ID: "urn:uuid:" + feedId,
+ ID: "urn:uuid:" + bytes2uuid(commit.Hash[:]),
Link: []atom.Link{{
Rel: "alternate",
Href: cfg.AtomBaseURL + cfg.URLPrefix + "/" + commit.Hash.String(),
}},
Published: atom.Time(commit.Author.When),
Updated: atom.Time(commit.Author.When),
- Summary: &atom.Text{
+ Summary: &atom.Text{Type: "text", Body: lines[0]},
+ Content: &atom.Text{
Type: "text",
- Body: lines[0],
+ Body: strings.Join(lines[2:], "\n"),
},
+ })
+ }
+ data, err := xml.MarshalIndent(&feed, "", " ")
+ if err != nil {
+ makeErr(err)
+ }
+ out.Write(data)
+ goto AtomFinish
+ } else if pathInfo == "/"+AtomCommentsFeed {
+ commit, err := repo.CommitObject(commentsRef.Hash())
+ if err != nil {
+ makeErr(err)
+ }
+ etagHash.Write([]byte("ATOM COMMENTS"))
+ etagHash.Write(commit.Hash[:])
+ checkETag(etagHash)
+ idHasher, err := blake2b.New256(nil)
+ if err != nil {
+ panic(err)
+ }
+ idHasher.Write([]byte("ATOM COMMENTS"))
+ idHasher.Write([]byte(cfg.AtomId))
+ feed := atom.Feed{
+ Title: cfg.Title + " comments",
+ ID: "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+ Updated: atom.Time(commit.Author.When),
+ Link: []atom.Link{{Rel: "self", Href: atomCommentsURL}},
+ Author: &atom.Person{Name: cfg.AtomAuthor},
+ }
+ repoLog, err := repo.Log(&git.LogOptions{From: commentsRef.Hash()})
+ if err != nil {
+ makeErr(err)
+ }
+ for i := 0; i < PageEntries; i++ {
+ commit, err = repoLog.Next()
+ if err != nil {
+ break
+ }
+ fileStats, err := commit.Stats()
+ if err != nil {
+ makeErr(err)
+ }
+ t, err := commit.Tree()
+ if err != nil {
+ makeErr(err)
+ }
+ commentedHash := plumbing.NewHash(strings.ReplaceAll(
+ fileStats[0].Name, "/", "",
+ ))
+ comments := parseComments(getNote(t, commentedHash))
+ commit, err = repo.CommitObject(commentedHash)
+ if err != nil {
+ makeErr(err)
+ }
+ commentN := strconv.Itoa(len(comments) - 1)
+ lines := strings.Split(comments[len(comments)-1], "\n")
+ from := strings.TrimPrefix(lines[0], "From: ")
+ date := strings.TrimPrefix(lines[1], "Date: ")
+ idHasher.Reset()
+ idHasher.Write([]byte("COMMENT"))
+ idHasher.Write(commit.Hash[:])
+ idHasher.Write([]byte(commentN))
+ feed.Entry = append(feed.Entry, &atom.Entry{
+ Title: strings.Join([]string{
+ "Comment ", commentN,
+ " for \"", msgSplit(commit.Message)[0],
+ "\" by ", from,
+ }, ""),
+ Author: &atom.Person{Name: from},
+ ID: "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+ Link: []atom.Link{{
+ Rel: "alternate",
+ Href: strings.Join([]string{
+ cfg.AtomBaseURL, cfg.URLPrefix, "/",
+ commit.Hash.String(), "#comment", commentN,
+ }, ""),
+ }},
+ Published: atom.TimeStr(date),
+ Updated: atom.TimeStr(date),
Content: &atom.Text{
Type: "text",
Body: strings.Join(lines[2:], "\n"),
makeErr(err)
}
out.Write(data)
- os.Stdout.WriteString("Content-Type: text/xml; charset=UTF-8\n")
- os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
- if gzipWriter != nil {
- os.Stdout.WriteString("Content-Encoding: gzip\n")
- gzipWriter.Close()
- }
- os.Stdout.WriteString("\n")
- os.Stdout.Write(outBuf.Bytes())
- return
+ goto AtomFinish
} else if sha1DigestRe.MatchString(pathInfo[1:]) {
- commit, err := repo.CommitObject(plumbing.NewHash(pathInfo[1:]))
+ commit, err := repo.CommitObject(plumbing.NewHash(pathInfo[1 : 1+sha1.Size*2]))
if err != nil {
makeErr(err)
}
}
etagHash.Write([]byte("ENTRY"))
etagHash.Write(commit.Hash[:])
- notesRaw := getNote(notesTree, commit.Hash)
- etagHash.Write(notesRaw)
+ atomCommentsURL = strings.Join([]string{
+ cfg.AtomBaseURL, cfg.URLPrefix, "/",
+ commit.Hash.String(), "/", AtomCommentsFeed,
+ }, "")
commentsRaw := getNote(commentsTree, commit.Hash)
etagHash.Write(commentsRaw)
+ if strings.HasSuffix(pathInfo, AtomCommentsFeed) {
+ etagHash.Write([]byte("ATOM COMMENTS"))
+ checkETag(etagHash)
+ type Comment struct {
+ n string
+ from string
+ date string
+ body []string
+ }
+ commentsRaw := parseComments(commentsRaw)
+ var toSkip int
+ if len(commentsRaw) > PageEntries {
+ toSkip = len(commentsRaw) - PageEntries
+ }
+ comments := make([]Comment, 0, len(commentsRaw)-toSkip)
+ for i := len(commentsRaw) - 1; i >= toSkip; i-- {
+ lines := strings.Split(commentsRaw[i], "\n")
+ from := strings.TrimPrefix(lines[0], "From: ")
+ date := strings.TrimPrefix(lines[1], "Date: ")
+ comments = append(comments, Comment{
+ n: strconv.Itoa(i),
+ from: from,
+ date: strings.Replace(date, " ", "T", 1),
+ body: lines[3:],
+ })
+ }
+ idHasher, err := blake2b.New256(nil)
+ if err != nil {
+ panic(err)
+ }
+ idHasher.Write([]byte("ATOM COMMENTS"))
+ idHasher.Write(commit.Hash[:])
+ feed := atom.Feed{
+ Title: fmt.Sprintf("\"%s\" comments", msgSplit(commit.Message)[0]),
+ ID: "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+ Link: []atom.Link{{Rel: "self", Href: atomCommentsURL}},
+ Author: &atom.Person{Name: cfg.AtomAuthor},
+ }
+ if len(comments) > 0 {
+ feed.Updated = atom.TimeStr(comments[0].date)
+ } else {
+ feed.Updated = atom.Time(commit.Author.When)
+ }
+ for _, comment := range comments {
+ idHasher.Reset()
+ idHasher.Write([]byte("COMMENT"))
+ idHasher.Write(commit.Hash[:])
+ idHasher.Write([]byte(comment.n))
+ feed.Entry = append(feed.Entry, &atom.Entry{
+ Title: strings.Join([]string{
+ "Comment", comment.n,
+ "by", comment.from,
+ }, " "),
+ Author: &atom.Person{Name: comment.from},
+ ID: "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+ Link: []atom.Link{{
+ Rel: "alternate",
+ Href: strings.Join([]string{
+ cfg.AtomBaseURL,
+ cfg.URLPrefix, "/",
+ commit.Hash.String(),
+ "#comment", comment.n,
+ }, ""),
+ }},
+ Published: atom.TimeStr(comment.date),
+ Updated: atom.TimeStr(comment.date),
+ Content: &atom.Text{
+ Type: "text",
+ Body: strings.Join(comment.body, "\n"),
+ },
+ })
+ }
+ data, err := xml.MarshalIndent(&feed, "", " ")
+ if err != nil {
+ makeErr(err)
+ }
+ out.Write(data)
+ goto AtomFinish
+ }
+ notesRaw := getNote(notesTree, commit.Hash)
+ etagHash.Write(notesRaw)
checkETag(etagHash)
lines := msgSplit(commit.Message)
title := lines[0]
when := commit.Author.When.Format(sgblog.WhenFmt)
os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
- links := []string{}
+ links := []string{`<link rel="alternate" title="Comments feed" href="` + atomCommentsURL + `" type="application/atom+xml">`}
var parent string
if len(commit.ParentHashes) > 0 {
parent = commit.ParentHashes[0].String()
- links = append(links, `<link rel="prev" href="`+cfg.URLPrefix+"/"+parent+`" title="older">`)
+ links = append(links, `<link rel="prev" href="`+cfg.URLPrefix+"/"+parent+`" title="prev">`)
}
out.Write([]byte(startHTML(fmt.Sprintf("%s (%s)", title, when), links)))
if cfg.AboutURL != "" {
}
out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.URLPrefix+"/", "index"))))
if parent != "" {
- out.Write([]byte(fmt.Sprintf(
- "[%s]\n",
- makeA(cfg.URLPrefix+"/"+parent, "older"),
- )))
+ out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.URLPrefix+"/"+parent, "prev"))))
}
out.Write([]byte(fmt.Sprintf(
"[<tt><a title=\"When\">%s</a></tt>]\n"+
- "[<tt><a title=\"Hash\">%s</a></tt>]\n"+
+ "[<tt><a title=\"What\">%s</a></tt>]\n"+
"<hr/>\n<h2>%s</h2>\n<pre>\n",
when, commit.Hash.String(), title,
)))
}
out.Write([]byte("</pre>\n<hr/>\n"))
if len(notesRaw) > 0 {
- out.Write([]byte("Note:<pre>\n" + string(notesRaw) + "\n</pre>\n<hr/>\n"))
+ out.Write([]byte("Note:<pre>\n"))
+ for _, line := range strings.Split(string(notesRaw), "\n") {
+ out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
+ }
+ out.Write([]byte("</pre>\n<hr/>\n"))
}
if cfg.CommentsEmail != "" {
out.Write([]byte("[" + makeA(
"mailto:"+cfg.CommentsEmail+"?subject="+commit.Hash.String(),
- "write comment",
+ "leave comment",
) + "]\n"))
}
out.Write([]byte("<dl>\n"))
gzipWriter.Close()
}
os.Stdout.Write(outBuf.Bytes())
+ return
+
+AtomFinish:
+ os.Stdout.WriteString("Content-Type: text/xml; charset=UTF-8\n")
+ os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
+ if gzipWriter != nil {
+ os.Stdout.WriteString("Content-Encoding: gzip\n")
+ gzipWriter.Close()
+ }
+ os.Stdout.WriteString("\n")
+ os.Stdout.Write(outBuf.Bytes())
}