diff --git a/README.md b/README.md index b69cc6f6b4..ee2201ba8e 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ _this is the recommended way to run remark42_ | restricted-words | RESTRICTED_WORDS | | words banned in comments (can use `*`), _multi_ | | restricted-names | RESTRICTED_NAMES | | names prohibited to use by the user, _multi_ | | edit-time | EDIT_TIME | `5m` | edit window | +| admin-edit | ADMIN_EDIT | `false` | unlimited edit for admins | | read-age | READONLY_AGE | | read-only age of comments, days | | image-proxy.http2https | IMAGE_PROXY_HTTP2HTTPS | `false` | enable http->https proxy for images | | image-proxy.cache-external | IMAGE_PROXY_CACHE_EXTERNAL | `false` | enable caching external images to current image storage | diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index ba1c2f070c..898ef3568c 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -68,6 +68,7 @@ type ServerCommand struct { PositiveScore bool `long:"positive-score" env:"POSITIVE_SCORE" description:"enable positive score only"` ReadOnlyAge int `long:"read-age" env:"READONLY_AGE" default:"0" description:"read-only age of comments, days"` EditDuration time.Duration `long:"edit-time" env:"EDIT_TIME" default:"5m" description:"edit window"` + AdminEdit bool `long:"admin-edit" env:"ADMIN_EDIT" description:"unlimited edit for admins"` Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"` WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"` UpdateLimit float64 `long:"update-limit" env:"UPDATE_LIMIT" default:"0.5" description:"updates/sec limit"` @@ -355,6 +356,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { dataService := &service.DataStore{ Engine: storeEngine, EditDuration: s.EditDuration, + AdminEdits: s.AdminEdit, AdminStore: adminStore, MaxCommentSize: s.MaxCommentSize, MaxVotes: s.MaxVotes, diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index cf42715e0f..4de75d9466 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -404,6 +404,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { cnf := struct { Version string `json:"version"` EditDuration int `json:"edit_duration"` + AdminEdit bool `json:"admin_edit"` MaxCommentSize int `json:"max_comment_size"` Admins []string `json:"admins"` AdminEmail string `json:"admin_email"` @@ -421,6 +422,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { }{ Version: s.Version, EditDuration: int(s.DataService.EditDuration.Seconds()), + AdminEdit: s.DataService.AdminEdits, MaxCommentSize: s.DataService.MaxCommentSize, Admins: admins, AdminEmail: emails, diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index d2140509b8..3caad19a2a 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -166,6 +166,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { Orig: edit.Text, Summary: edit.Summary, Delete: edit.Delete, + Admin: user.Admin, } res, err := s.dataService.EditComment(locator, id, editReq) diff --git a/backend/app/rest/api/rest_public_test.go b/backend/app/rest/api/rest_public_test.go index 577ece336e..d5f11ab01c 100644 --- a/backend/app/rest/api/rest_public_test.go +++ b/backend/app/rest/api/rest_public_test.go @@ -550,6 +550,7 @@ func TestRest_Config(t *testing.T) { assert.Equal(t, 10.0, j["readonly_age"]) assert.Equal(t, 10000.0, j["max_image_size"]) assert.Equal(t, true, j["emoji_enabled"].(bool)) + assert.Equal(t, false, j["admin_edit"].(bool)) } func TestRest_Info(t *testing.T) { diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index 788d587b5c..96957f8630 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -36,6 +36,7 @@ type DataStore struct { TitleExtractor *TitleExtractor RestrictedWordsMatcher *RestrictedWordsMatcher ImageService *image.Service + AdminEdits bool // allow admin unlimited edits // granular locks scopedLocks struct { @@ -433,22 +434,35 @@ type EditRequest struct { Orig string Summary string Delete bool + Admin bool } // EditComment to edit text and update Edit info func (s *DataStore) EditComment(locator store.Locator, commentID string, req EditRequest) (comment store.Comment, err error) { - comment, err = s.Engine.Get(engine.GetRequest{Locator: locator, CommentID: commentID}) - if err != nil { - return comment, err + + editAllowed := func(comment store.Comment) error { + if req.Admin && s.AdminEdits { + return nil + } + + // edit allowed in editDuration window only + if s.EditDuration > 0 && time.Now().After(comment.Timestamp.Add(s.EditDuration)) { + return errors.Errorf("too late to edit %s", commentID) + } + + // edit rejected on replayed threads + if s.HasReplies(comment) { + return errors.Errorf("parent comment with reply can't be edited, %s", commentID) + } + return nil } - // edit allowed in editDuration window only - if s.EditDuration > 0 && time.Now().After(comment.Timestamp.Add(s.EditDuration)) { - return comment, errors.Errorf("too late to edit %s", commentID) + if comment, err = s.Engine.Get(engine.GetRequest{Locator: locator, CommentID: commentID}); err != nil { + return comment, err } - if s.HasReplies(comment) { - return comment, errors.Errorf("parent comment with reply can't be edited, %s", commentID) + if err = editAllowed(comment); err != nil { //nolint gocritic + return comment, err } if req.Delete { // delete request @@ -466,10 +480,7 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi comment.Text = req.Text comment.Orig = req.Orig - comment.Edit = &store.Edit{ - Timestamp: time.Now(), - Summary: req.Summary, - } + comment.Edit = &store.Edit{Timestamp: time.Now(), Summary: req.Summary} comment.Locator = locator comment.Sanitize() diff --git a/backend/app/store/service/service_test.go b/backend/app/store/service/service_test.go index cc2f48b9f7..2cf43e3209 100644 --- a/backend/app/store/service/service_test.go +++ b/backend/app/store/service/service_test.go @@ -742,6 +742,29 @@ func TestService_EditCommentReplyFailed(t *testing.T) { assert.EqualError(t, err, "parent comment with reply can't be edited, id-1") } +func TestService_EditCommentAdmin(t *testing.T) { + eng, teardown := prepStoreEngine(t) + defer teardown() + b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond, + AdminStore: admin.NewStaticKeyStore("secret 123"), AdminEdits: true} + + res, err := b.Last("radio-t", 0, time.Time{}, store.User{}) + t.Logf("%+v", res[0]) + assert.NoError(t, err) + require.Equal(t, 2, len(res)) + assert.Nil(t, res[0].Edit) + + time.Sleep(time.Second) + + _, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, + EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit", Admin: true}) + assert.NoError(t, err) + + _, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, + EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit", Admin: false}) + assert.Error(t, err) +} + func TestService_ValidateComment(t *testing.T) { b := DataStore{MaxCommentSize: 2000, AdminStore: admin.NewStaticKeyStore("secret 123")}