comment methods

This commit is contained in:
Umputun
2018-02-05 02:51:11 -06:00
parent f96ae1f358
commit 375e5cb049
7 changed files with 169 additions and 125 deletions
+2 -1
View File
@@ -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
View File
@@ -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 {
+92
View File
@@ -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
}
+44
View File
@@ -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: `&lt;a href=&#34;http://blah.com&#34;&gt;username&lt;/a&gt;`},
},
},
}
for n, tt := range tbl {
tt.inp.Sanitize()
assert.Equal(t, tt.out, tt.inp, "check #%d", n)
}
}
+1 -1
View File
@@ -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
}
-89
View File
@@ -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
View File
@@ -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: `&lt;a href=&#34;http://blah.com&#34;&gt;username&lt;/a&gt;`},
},
},
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)
}