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)
}