From 375e5cb0496da5c3e190f17d848cfe9deaa50f3a Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 5 Feb 2018 02:51:11 -0600 Subject: [PATCH] comment methods --- README.md | 3 +- app/store/bolt.go | 8 ++-- app/store/comment.go | 92 +++++++++++++++++++++++++++++++++++++++ app/store/comment_test.go | 44 +++++++++++++++++++ app/store/service.go | 2 +- app/store/store.go | 89 ------------------------------------- app/store/store_test.go | 56 +++++++++++------------- 7 files changed, 169 insertions(+), 125 deletions(-) create mode 100644 app/store/comment.go create mode 100644 app/store/comment_test.go diff --git a/README.md b/README.md index cf852b78..1359d915 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engi - copy provided `docker-compose.yml` and customize for your needs - make sure you **don't keep** `DEV=true` for any non-development deployments -- pull and start `docker-compose pull && docker compose up` +- pull prepared images from docker hub and start - `docker-compose pull && docker compose up -d` +- alternatively compile from sources - `docker-compose build` #### Parameters diff --git a/app/store/bolt.go b/app/store/bolt.go index 770f8754..f421059c 100644 --- a/app/store/bolt.go +++ b/app/store/bolt.go @@ -59,7 +59,9 @@ func (b *BoltDB) Create(comment Comment) (commentID string, err error) { // fill ID and time if empty if comment.ID == "" { - comment.ID = makeCommentID() + if err := comment.GenID(); err != nil { + return "", err + } } if comment.Timestamp.IsZero() { comment.Timestamp = time.Now() @@ -68,7 +70,7 @@ func (b *BoltDB) Create(comment Comment) (commentID string, err error) { comment.Votes = make(map[string]bool) } - comment = sanitizeComment(comment) // clear potentially dangerous js from all parts of comment + comment.Sanitize() // clear potentially dangerous js from all parts of comment bdb, err := b.db(comment.Locator.SiteID) if err != nil { @@ -142,7 +144,7 @@ func (b *BoltDB) Delete(locator Locator, commentID string) error { return errors.Wrapf(err, "can't load key %s from bucket %s", commentID, locator.URL) } // set deleted status and clear fields - comment = MaskComment(comment) + comment.Mask() comment.Deleted = true if err := b.save(bucket, []byte(commentID), comment); err != nil { diff --git a/app/store/comment.go b/app/store/comment.go new file mode 100644 index 00000000..8107fa65 --- /dev/null +++ b/app/store/comment.go @@ -0,0 +1,92 @@ +package store + +import ( + "crypto/rand" + "crypto/sha1" + "fmt" + "strings" + "time" + + "github.com/alecthomas/template" + "github.com/microcosm-cc/bluemonday" + "github.com/pkg/errors" +) + +// Comment represents a single comment with optional reference to its parent +type Comment struct { + ID string `json:"id"` + ParentID string `json:"pid"` + Text string `json:"text"` + User User `json:"user"` + Locator Locator `json:"locator"` + Score int `json:"score"` + Votes map[string]bool `json:"votes"` + Timestamp time.Time `json:"time"` + Pin bool `json:"pin,omitempty"` + Edit *Edit `json:"edit,omitempty"` + Deleted bool `json:"delete,omitempty"` +} + +// Locator keeps site and url of the post +type Locator struct { + SiteID string `json:"site,omitempty"` + URL string `json:"url"` +} + +// User holds user-related info +type User struct { + Name string `json:"name"` + ID string `json:"id"` + Picture string `json:"picture"` + Profile string `json:"profile"` + Admin bool `json:"admin"` + Blocked bool `json:"block,omitempty"` + IP string `json:"-"` +} + +// Edit indication +type Edit struct { + Timestamp time.Time `json:"time"` + Summary string `json:"summary"` +} + +// PostInfo holds summary for given post url +type PostInfo struct { + URL string `json:"url"` + Count int `json:"count"` +} + +// GenID generates sha1(random) string +func (c *Comment) GenID() error { + b := make([]byte, 64) + if _, err := rand.Read(b); err != nil { + return errors.Wrap(err, "can't get random") + } + s := sha1.New() + if _, err := s.Write(b); err != nil { + return errors.Wrap(err, "can't make sha1 for random") + } + c.ID = fmt.Sprintf("%x", s.Sum(nil)) + return nil +} + +// Sanitize clean dangerous html/js from the comment +func (c *Comment) Sanitize() { + p := bluemonday.UGCPolicy() + c.Text = p.Sanitize(c.Text) + c.User.ID = template.HTMLEscapeString(c.User.ID) + c.User.Name = template.HTMLEscapeString(c.User.Name) + c.User.Picture = p.Sanitize(c.User.Picture) + c.User.Profile = p.Sanitize(c.User.Profile) + + c.Text = strings.Replace(c.Text, "\n", "", -1) + c.Text = strings.Replace(c.Text, "\t", "", -1) +} + +// Mask clears comment info, reset to "Deleted/Blocked" +func (c *Comment) Mask() { + c.Text = "this comment was deleted" + c.Score = 0 + c.Votes = map[string]bool{} + c.Edit = nil +} diff --git a/app/store/comment_test.go b/app/store/comment_test.go new file mode 100644 index 00000000..cf00b4ed --- /dev/null +++ b/app/store/comment_test.go @@ -0,0 +1,44 @@ +package store + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComment_GenID(t *testing.T) { + c1 := Comment{} + assert.Nil(t, c1.GenID()) + assert.True(t, len(c1.ID) > 8, "cid1 is long enough") + + c2 := Comment{} + assert.Nil(t, c2.GenID()) + assert.True(t, len(c2.ID) > 8, "cid2 is long enough") + + assert.NotEqual(t, c1.ID, c2.ID, "cids different") +} + +func TestComment_Sanitize(t *testing.T) { + + tbl := []struct { + inp Comment + out Comment + }{ + {inp: Comment{}, out: Comment{}}, + { + inp: Comment{ + Text: `blah XSS` + "\n\t", + User: User{ID: `username`}, + }, + out: Comment{ + Text: `blah XSS`, + User: User{ID: `<a href="http://blah.com">username</a>`}, + }, + }, + } + + for n, tt := range tbl { + tt.inp.Sanitize() + assert.Equal(t, tt.out, tt.inp, "check #%d", n) + } +} diff --git a/app/store/service.go b/app/store/service.go index 2d93eacb..bdefc2d7 100644 --- a/app/store/service.go +++ b/app/store/service.go @@ -75,7 +75,7 @@ func (s *Service) EditComment(locator Locator, commentID string, text string, ed comment.Text = text comment.Edit = &edit comment.Edit.Timestamp = time.Now() - comment = sanitizeComment(comment) + comment.Sanitize() err = s.Put(locator, comment) return comment, err } diff --git a/app/store/store.go b/app/store/store.go index 558423af..b3a900d5 100644 --- a/app/store/store.go +++ b/app/store/store.go @@ -3,62 +3,10 @@ package store //go:generate sh -c "mockery -inpkg -name Interface -print > file.tmp && mv file.tmp store_mock.go" import ( - "crypto/rand" - "crypto/sha1" - "fmt" - "html/template" - "log" "sort" "strings" - "time" - - "github.com/microcosm-cc/bluemonday" ) -// Comment represents a single comment with optional reference to its parent -type Comment struct { - ID string `json:"id"` - ParentID string `json:"pid"` - Text string `json:"text"` - User User `json:"user"` - Locator Locator `json:"locator"` - Score int `json:"score"` - Votes map[string]bool `json:"votes"` - Timestamp time.Time `json:"time"` - Pin bool `json:"pin,omitempty"` - Edit *Edit `json:"edit,omitempty"` - Deleted bool `json:"delete,omitempty"` -} - -// Locator keeps site and url of the post -type Locator struct { - SiteID string `json:"site,omitempty"` - URL string `json:"url"` -} - -// User holds user-related info -type User struct { - Name string `json:"name"` - ID string `json:"id"` - Picture string `json:"picture"` - Profile string `json:"profile"` - Admin bool `json:"admin"` - Blocked bool `json:"block,omitempty"` - IP string `json:"-"` -} - -// Edit indication -type Edit struct { - Timestamp time.Time `json:"time"` - Summary string `json:"summary"` -} - -// PostInfo holds summary for given post url -type PostInfo struct { - URL string `json:"url"` - Count int `json:"count"` -} - // Interface combines all store interfaces type Interface interface { Accessor @@ -84,34 +32,6 @@ type Admin interface { IsBlocked(siteID string, userID string) bool // check if user blocked } -// makeCommentID generates sha1(random) string -func makeCommentID() string { - b := make([]byte, 64) - if _, err := rand.Read(b); err != nil { - log.Fatalf("[ERROR] can't get randoms, %s", err) - } - s := sha1.New() - if _, err := s.Write(b); err != nil { - log.Fatalf("[ERROR] can't make sha1 for random, %s", err) - } - return fmt.Sprintf("%x", s.Sum(nil)) -} - -// clean dangerous html/js from the comment -func sanitizeComment(comment Comment) Comment { - p := bluemonday.UGCPolicy() - comment.Text = p.Sanitize(comment.Text) - comment.User.ID = template.HTMLEscapeString(comment.User.ID) - comment.User.Name = template.HTMLEscapeString(comment.User.Name) - comment.User.Picture = p.Sanitize(comment.User.Picture) - comment.User.Profile = p.Sanitize(comment.User.Profile) - - comment.Text = strings.Replace(comment.Text, "\n", "", -1) - comment.Text = strings.Replace(comment.Text, "\t", "", -1) - - return comment -} - func sortComments(comments []Comment, sortFld string) []Comment { sort.Slice(comments, func(i, j int) bool { switch sortFld { @@ -133,12 +53,3 @@ func sortComments(comments []Comment, sortFld string) []Comment { }) return comments } - -// MaskComment clears comment info, reset to "Deleted/Blocked" -func MaskComment(comment Comment) Comment { - comment.Text = "this comment was deleted" - comment.Score = 0 - comment.Votes = map[string]bool{} - comment.Edit = nil - return comment -} diff --git a/app/store/store_test.go b/app/store/store_test.go index d33b6fd3..c3df0058 100644 --- a/app/store/store_test.go +++ b/app/store/store_test.go @@ -2,41 +2,35 @@ package store import ( "testing" + "time" "github.com/stretchr/testify/assert" ) -func TestStore_MakeCommentID(t *testing.T) { - cid1 := makeCommentID() - assert.True(t, len(cid1) > 8, "cid1 is long enough") - - cid2 := makeCommentID() - assert.True(t, len(cid2) > 8, "cid2 is long enough") - - assert.NotEqual(t, cid1, cid2, "cids different") -} - -func TestStore_SanitizeComment(t *testing.T) { - - tbl := []struct { - inp Comment - out Comment - }{ - {inp: Comment{}, out: Comment{}}, - { - inp: Comment{ - Text: `blah XSS` + "\n\t", - User: User{ID: `username`}, - }, - out: Comment{ - Text: `blah XSS`, - User: User{ID: `<a href="http://blah.com">username</a>`}, - }, - }, +func TestStore_sortComments(t *testing.T) { + cc := []Comment{ + {ID: "1", Score: 5, Timestamp: time.Date(2018, 2, 5, 10, 1, 0, 0, time.Local)}, + {ID: "2", Score: 4, Timestamp: time.Date(2018, 2, 5, 10, 2, 0, 0, time.Local)}, + {ID: "3", Score: 6, Timestamp: time.Date(2018, 2, 5, 10, 3, 0, 0, time.Local)}, } - for n, tt := range tbl { - out := sanitizeComment(tt.inp) - assert.Equal(t, tt.out, out, "check #%d", n) - } + sortComments(cc, "+time") + assert.Equal(t, "1", cc[0].ID) + assert.Equal(t, "2", cc[1].ID) + assert.Equal(t, "3", cc[2].ID) + + sortComments(cc, "-time") + assert.Equal(t, "3", cc[0].ID) + assert.Equal(t, "2", cc[1].ID) + assert.Equal(t, "1", cc[2].ID) + + sortComments(cc, "score") + assert.Equal(t, "2", cc[0].ID) + assert.Equal(t, "1", cc[1].ID) + assert.Equal(t, "3", cc[2].ID) + + sortComments(cc, "-score") + assert.Equal(t, "3", cc[0].ID) + assert.Equal(t, "1", cc[1].ID) + assert.Equal(t, "2", cc[2].ID) }