comment methods
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+5
-3
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 <a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>` + "\n\t",
|
||||
User: User{ID: `<a href="http://blah.com">username</a>`},
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+25
-31
@@ -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 <a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>` + "\n\t",
|
||||
User: User{ID: `<a href="http://blah.com">username</a>`},
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user