diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index 08479957..5dcaee93 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -148,17 +148,18 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") return } + log.Printf("[DEBUG] create comment %+v", comment) comment.PrepareUntrusted() // clean all fields user not supposed to set comment.User = user comment.User.IP = strings.Split(r.RemoteAddr, ":")[0] + + comment.Orig = comment.Text // original comment text, prior to md render if err = s.DataService.ValidateComment(&comment); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") return } - comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt))) - log.Printf("[DEBUG] create comment %+v", comment) // check if user blocked if s.adminService.checkBlocked(comment.Locator.SiteID, comment.User) { @@ -190,6 +191,7 @@ func (s *Rest) previewCommentCtrl(w http.ResponseWriter, r *http.Request) { return } comment.User = user + comment.Orig = comment.Text if err = s.DataService.ValidateComment(&comment); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") return @@ -223,9 +225,6 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} id := chi.URLParam(r, "id") - // render markdown - edit.Text = string(blackfriday.Run([]byte(edit.Text), blackfriday.WithNoExtensions())) - log.Printf("[DEBUG] update comment %s, %+v", id, edit) var currComment store.Comment @@ -239,7 +238,13 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { return } - res, err := s.DataService.EditComment(locator, id, edit.Text, store.Edit{Summary: edit.Summary}) + editReq := store.EditRequest{ + Text: string(blackfriday.Run([]byte(edit.Text), blackfriday.WithExtensions(mdExt))), // render markdown + Orig: edit.Text, + Summary: edit.Summary, + } + + res, err := s.DataService.EditComment(locator, id, editReq) if err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment") return diff --git a/app/rest/api/rest_test.go b/app/rest/api/rest_test.go index 413ef483..ac941d29 100644 --- a/app/rest/api/rest_test.go +++ b/app/rest/api/rest_test.go @@ -128,8 +128,8 @@ func TestServer_CreateAndGet(t *testing.T) { // create comment r := strings.NewReader(`{"text": "**test** *123* http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", r) - assert.Nil(t, err) - assert.Equal(t, http.StatusCreated, resp.StatusCode) + require.Nil(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) c := JSON{} @@ -145,6 +145,7 @@ func TestServer_CreateAndGet(t *testing.T) { err = json.Unmarshal([]byte(res), &comment) assert.Nil(t, err) assert.Equal(t, `

test 123 http://radio-t.com

`+"\n", comment.Text) + assert.Equal(t, "**test** *123* http://radio-t.com", comment.Orig) assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "ea64bfc178468d943ca5b836e2e700c335404973"}, comment.User) @@ -214,6 +215,7 @@ func TestServer_Update(t *testing.T) { assert.Nil(t, err) assert.Equal(t, id, c2.ID) assert.Equal(t, "

updated text

\n", c2.Text) + assert.Equal(t, "updated text", c2.Orig) assert.Equal(t, "my edit", c2.Edit.Summary) assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second) diff --git a/app/store/comment.go b/app/store/comment.go index 317d2337..21ede191 100644 --- a/app/store/comment.go +++ b/app/store/comment.go @@ -19,6 +19,7 @@ type Comment struct { ID string `json:"id"` ParentID string `json:"pid"` Text string `json:"text"` + Orig string `json:"orig,omitempty"` User User `json:"user"` Locator Locator `json:"locator"` Score int `json:"score"` @@ -64,7 +65,7 @@ type BlockedUser struct { Timestamp time.Time `json:"time"` } -// PrepareUntrusted preprocess comment received from untrusted source by clearing all +// PrepareUntrusted pre-processes a comment received from untrusted source by clearing all // autogen fields and reset everything users not supposed to provide func (c *Comment) PrepareUntrusted() { c.ID = "" // don't allow user to define ID, force auto-gen @@ -79,6 +80,7 @@ func (c *Comment) PrepareUntrusted() { // SetDeleted clears comment info, reset to deleted state func (c *Comment) SetDeleted() { c.Text = "" + c.Orig = "" c.Score = 0 c.Votes = map[string]bool{} c.Edit = nil @@ -90,15 +92,13 @@ func (c *Comment) Sanitize() { p := bluemonday.UGCPolicy() p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") c.Text = p.Sanitize(c.Text) + c.Orig = p.Sanitize(c.Orig) 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.Text = strings.Replace(c.Text, "\n", "", -1) - // c.Text = strings.Replace(c.Text, "\t", "", -1) } -// hashIP replace sensitive fields with hmac +// hashIP replace IP field with hashed hmac func (u *User) hashIP(secret string) { hashVal := func(val string) string { @@ -117,7 +117,7 @@ func (u *User) hashIP(secret string) { } // EncodeID hashes id to sha1. The function intentionally left outside of User struct because in some cases -// we need hashing for parts of id, in some others hasing for non-User values. +// we need hashing for parts of id, in some others hashing for non-User values. func EncodeID(id string) string { h := sha1.New() if _, err := h.Write([]byte(id)); err != nil { diff --git a/app/store/service.go b/app/store/service.go index 98564305..c354c8dc 100644 --- a/app/store/service.go +++ b/app/store/service.go @@ -88,8 +88,14 @@ func (s *Service) Vote(locator Locator, commentID string, userID string, val boo return comment, s.Put(locator, comment) } +type EditRequest struct { + Text string + Orig string + Summary string +} + // EditComment to edit text and update Edit info -func (s *Service) EditComment(locator Locator, commentID string, text string, edit Edit) (comment Comment, err error) { +func (s *Service) EditComment(locator Locator, commentID string, req EditRequest) (comment Comment, err error) { comment, err = s.Get(locator, commentID) if err != nil { return comment, err @@ -104,9 +110,12 @@ func (s *Service) EditComment(locator Locator, commentID string, text string, ed return comment, errors.Errorf("too late to edit %s", commentID) } - comment.Text = text - comment.Edit = &edit - comment.Edit.Timestamp = time.Now() + comment.Text = req.Text + comment.Orig = req.Orig + comment.Edit = &Edit{ + Timestamp: time.Now(), + Summary: req.Summary, + } comment.Sanitize() err = s.Put(locator, comment) @@ -130,11 +139,11 @@ func (s *Service) ValidateComment(c *Comment) error { if s.MaxCommentSize <= 0 { maxSize = defaultCommentMaxSize } - if c.Text == "" { + if c.Orig == "" { return errors.New("empty comment text") } - if len([]rune(c.Text)) > maxSize { - return errors.Errorf("comment text exceeded max allowed size %d (%d)", maxSize, len([]rune(c.Text))) + if len([]rune(c.Orig)) > maxSize { + return errors.Errorf("comment text exceeded max allowed size %d (%d)", maxSize, len([]rune(c.Orig))) } if c.User.ID == "" || c.User.Name == "" { return errors.Errorf("empty user info") diff --git a/app/store/service_test.go b/app/store/service_test.go index 414c977b..ee62f67f 100644 --- a/app/store/service_test.go +++ b/app/store/service_test.go @@ -139,17 +139,20 @@ func TestService_EditComment(t *testing.T) { assert.Equal(t, 2, len(res)) assert.Nil(t, res[0].Edit) - comment, err := b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "xxx", Edit{Summary: "my edit"}) + comment, err := b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, + EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"}) assert.Nil(t, err) assert.Equal(t, "my edit", comment.Edit.Summary) assert.Equal(t, "xxx", comment.Text) + assert.Equal(t, "yyy", comment.Orig) c, err := b.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID) assert.Nil(t, err) assert.Equal(t, "my edit", c.Edit.Summary) assert.Equal(t, "xxx", c.Text) - _, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "xxx", Edit{Summary: "my edit"}) + _, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, + EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"}) assert.NotNil(t, err, "allow edit once") } @@ -178,8 +181,8 @@ func TestService_EditCommentDurationFailed(t *testing.T) { time.Sleep(time.Second) - _, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "xxx", - Edit{Summary: "my edit"}) + _, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, + EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"}) assert.NotNil(t, err) } @@ -193,9 +196,9 @@ func TestService_ValidateComment(t *testing.T) { err error }{ {inp: Comment{}, err: errors.New("empty comment text")}, - {inp: Comment{Text: "something blah", User: User{ID: "myid", Name: "name"}}, err: nil}, - {inp: Comment{Text: "something blah", User: User{ID: "myid"}}, err: errors.New("empty user info")}, - {inp: Comment{Text: longText, User: User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size 2000 (4000)")}, + {inp: Comment{Orig: "something blah", User: User{ID: "myid", Name: "name"}}, err: nil}, + {inp: Comment{Orig: "something blah", User: User{ID: "myid"}}, err: errors.New("empty user info")}, + {inp: Comment{Orig: longText, User: User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size 2000 (4000)")}, } for n, tt := range tbl {