From b9b41b1ef7279b1f24c7d0866df836976ef23bc5 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 18 May 2019 23:16:42 -0400 Subject: [PATCH 1/9] Enable un-setting RTL setting via web Previously, once RTL was enabled on a post, you couldn't unset it via the web application. This fixes that. (Fixes #103) --- posts.go | 7 ++++++- templates/edit-meta.tmpl | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/posts.go b/posts.go index b21588c..e4cc81b 100644 --- a/posts.go +++ b/posts.go @@ -67,7 +67,8 @@ type ( } AuthenticatedPost struct { - ID string `json:"id" schema:"id"` + ID string `json:"id" schema:"id"` + Web bool `json:"web" schema:"web"` *SubmittedPost } @@ -623,6 +624,10 @@ func existingPost(app *app, w http.ResponseWriter, r *http.Request) error { } } + if p.Web { + p.IsRTL.Valid = true + } + if p.SubmittedPost == nil { return ErrPostNoUpdatableVals } diff --git a/templates/edit-meta.tmpl b/templates/edit-meta.tmpl index 5d2bf1a..108c552 100644 --- a/templates/edit-meta.tmpl +++ b/templates/edit-meta.tmpl @@ -263,6 +263,7 @@
 
+ From 3986c8eec16b6ebff216c39c637f1762494defc8 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 20 May 2019 14:34:11 -0700 Subject: [PATCH 2/9] add missing string variable in log statement in export.go + go fmt --- export.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/export.go b/export.go index e23b850..42715b9 100644 --- a/export.go +++ b/export.go @@ -14,9 +14,10 @@ import ( "archive/zip" "bytes" "encoding/csv" - "github.com/writeas/web-core/log" "strings" "time" + + "github.com/writeas/web-core/log" ) func exportPostsCSV(u *User, posts *[]PublicPost) []byte { @@ -37,7 +38,7 @@ func exportPostsCSV(u *User, posts *[]PublicPost) []byte { w := csv.NewWriter(&b) w.WriteAll(r) // calls Flush internally if err := w.Error(); err != nil { - log.Info("error writing csv:", err) + log.Info("error writing csv: %v", err) } return b.Bytes() From ff2d3fc3d5fcf237590eee2ce871deb6faf458f4 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 20 May 2019 14:41:26 -0700 Subject: [PATCH 3/9] fixes issue #100 - can't follow from pubgate this moves the unmarshaling of a remote actor out into a new helper which accounts for the possibility of a context being a list or a single entity. i.e. a string or an object. basics tests are provided for both situations also go fmt'd the file activitypub.go --- .vscode/settings.json | 3 +++ activitypub.go | 61 +++++++++++++++++++++++++++++++++++++------ activitypub_test.go | 31 ++++++++++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 activitypub_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f06b593 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "go.formatTool": "goimports" +} \ No newline at end of file diff --git a/activitypub.go b/activitypub.go index 3b0ac82..0291bf9 100644 --- a/activitypub.go +++ b/activitypub.go @@ -17,6 +17,13 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "time" + "github.com/go-sql-driver/mysql" "github.com/gorilla/mux" "github.com/writeas/activity/streams" @@ -26,12 +33,6 @@ import ( "github.com/writeas/web-core/activitypub" "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/log" - "io/ioutil" - "net/http" - "net/http/httputil" - "net/url" - "strconv" - "time" ) const ( @@ -647,8 +648,7 @@ func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, log.Error("Unable to get actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."} } - if err := json.Unmarshal(actorResp, &actor); err != nil { - // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string + if err := unmarshalActor(actorResp, actor); err != nil { log.Error("Unable to unmarshal actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."} } @@ -663,3 +663,48 @@ func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, } return actor, remoteUser, nil } + +// unmarshal actor normalizes the actor response to conform to +// the type Person from github.com/writeas/web-core/activitysteams +// +// some implementations return different context field types +// this converts any non-slice contexts into a slice +func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error { + // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string + + // flexActor overrides the Context field to allow + // all valid representations during unmarshal + flexActor := struct { + activitystreams.Person + Context json.RawMessage `json:"@context,omitempty"` + }{} + if err := json.Unmarshal(actorResp, &flexActor); err != nil { + return err + } + + actor.Endpoints = flexActor.Endpoints + actor.Followers = flexActor.Followers + actor.Following = flexActor.Following + actor.ID = flexActor.ID + actor.Icon = flexActor.Icon + actor.Inbox = flexActor.Inbox + actor.Name = flexActor.Name + actor.Outbox = flexActor.Outbox + actor.PreferredUsername = flexActor.PreferredUsername + actor.PublicKey = flexActor.PublicKey + actor.Summary = flexActor.Summary + actor.Type = flexActor.Type + actor.URL = flexActor.URL + + func(val interface{}) { + switch val.(type) { + case []interface{}: + // already a slice, do nothing + actor.Context = val.([]interface{}) + default: + actor.Context = []interface{}{val} + } + }(flexActor.Context) + + return nil +} diff --git a/activitypub_test.go b/activitypub_test.go new file mode 100644 index 0000000..7a1a89a --- /dev/null +++ b/activitypub_test.go @@ -0,0 +1,31 @@ +package writefreely + +import ( + "testing" + + "github.com/writeas/web-core/activitystreams" +) + +var actorTestTable = []struct { + Name string + Resp []byte +}{ + { + "Context as a string", + []byte(`{"@context":"https://www.w3.org/ns/activitystreams"}`), + }, + { + "Context as a list", + []byte(`{"@context":["one string", "two strings"]}`), + }, +} + +func TestUnmarshalActor(t *testing.T) { + for _, tc := range actorTestTable { + actor := activitystreams.Person{} + err := unmarshalActor(tc.Resp, &actor) + if err != nil { + t.Errorf("%s failed with error %s", tc.Name, err) + } + } +} From 95215aa39db51f9510ded34480a62fb61242c4dd Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 20 May 2019 14:41:26 -0700 Subject: [PATCH 4/9] fixes issue #100 - can't follow from pubgate this moves the unmarshaling of a remote actor out into a new helper which accounts for the possibility of a context being a list or a single entity. i.e. a string or an object. basics tests are provided for both situations also go fmt'd the file activitypub.go --- activitypub.go | 61 +++++++++++++++++++++++++++++++++++++++------ activitypub_test.go | 31 +++++++++++++++++++++++ 2 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 activitypub_test.go diff --git a/activitypub.go b/activitypub.go index 3b0ac82..0291bf9 100644 --- a/activitypub.go +++ b/activitypub.go @@ -17,6 +17,13 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "time" + "github.com/go-sql-driver/mysql" "github.com/gorilla/mux" "github.com/writeas/activity/streams" @@ -26,12 +33,6 @@ import ( "github.com/writeas/web-core/activitypub" "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/log" - "io/ioutil" - "net/http" - "net/http/httputil" - "net/url" - "strconv" - "time" ) const ( @@ -647,8 +648,7 @@ func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, log.Error("Unable to get actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."} } - if err := json.Unmarshal(actorResp, &actor); err != nil { - // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string + if err := unmarshalActor(actorResp, actor); err != nil { log.Error("Unable to unmarshal actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."} } @@ -663,3 +663,48 @@ func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, } return actor, remoteUser, nil } + +// unmarshal actor normalizes the actor response to conform to +// the type Person from github.com/writeas/web-core/activitysteams +// +// some implementations return different context field types +// this converts any non-slice contexts into a slice +func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error { + // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string + + // flexActor overrides the Context field to allow + // all valid representations during unmarshal + flexActor := struct { + activitystreams.Person + Context json.RawMessage `json:"@context,omitempty"` + }{} + if err := json.Unmarshal(actorResp, &flexActor); err != nil { + return err + } + + actor.Endpoints = flexActor.Endpoints + actor.Followers = flexActor.Followers + actor.Following = flexActor.Following + actor.ID = flexActor.ID + actor.Icon = flexActor.Icon + actor.Inbox = flexActor.Inbox + actor.Name = flexActor.Name + actor.Outbox = flexActor.Outbox + actor.PreferredUsername = flexActor.PreferredUsername + actor.PublicKey = flexActor.PublicKey + actor.Summary = flexActor.Summary + actor.Type = flexActor.Type + actor.URL = flexActor.URL + + func(val interface{}) { + switch val.(type) { + case []interface{}: + // already a slice, do nothing + actor.Context = val.([]interface{}) + default: + actor.Context = []interface{}{val} + } + }(flexActor.Context) + + return nil +} diff --git a/activitypub_test.go b/activitypub_test.go new file mode 100644 index 0000000..7a1a89a --- /dev/null +++ b/activitypub_test.go @@ -0,0 +1,31 @@ +package writefreely + +import ( + "testing" + + "github.com/writeas/web-core/activitystreams" +) + +var actorTestTable = []struct { + Name string + Resp []byte +}{ + { + "Context as a string", + []byte(`{"@context":"https://www.w3.org/ns/activitystreams"}`), + }, + { + "Context as a list", + []byte(`{"@context":["one string", "two strings"]}`), + }, +} + +func TestUnmarshalActor(t *testing.T) { + for _, tc := range actorTestTable { + actor := activitystreams.Person{} + err := unmarshalActor(tc.Resp, &actor) + if err != nil { + t.Errorf("%s failed with error %s", tc.Name, err) + } + } +} From 4b1ca3e29672db658feace1cf5fda70753a99c23 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 1 Jun 2019 09:17:28 -0400 Subject: [PATCH 5/9] Make post body h2's smaller on index pages Previously,

s in a post were the exact same size as post titles on index pages (blog index, tag listing). This fixes that by reducing the font-size of body h2's. Closes #82. --- less/core.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/less/core.less b/less/core.less index a25d867..66ce3ba 100644 --- a/less/core.less +++ b/less/core.less @@ -604,6 +604,9 @@ body#collection article, body#subpage article { padding-top: 0; padding-bottom: 0; .book { + h2 { + font-size: 1.4em; + } a.hidden.action { color: #666; float: right; From 6bf4e1a52ea4d86fc3b17b9069ae39a91bbf3a2e Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 1 Jun 2019 09:26:58 -0400 Subject: [PATCH 6/9] Add spacing around time element on index pages This adds a small margin around a time element, which makes things look better when the body of a post starts with a header. --- less/core.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/less/core.less b/less/core.less index 66ce3ba..118acd8 100644 --- a/less/core.less +++ b/less/core.less @@ -252,6 +252,8 @@ body { margin-bottom: 0.25em; &+time { display: block; + margin-top: 0.25em; + margin-bottom: 0.25em; } } time { From d8fa85432dab62e36dd64827194e938448383ecf Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 3 Jun 2019 11:53:17 -0700 Subject: [PATCH 7/9] fix for Pubgate user not having SharedInbox --- activitypub.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/activitypub.go b/activitypub.go index 0291bf9..0971286 100644 --- a/activitypub.go +++ b/activitypub.go @@ -549,10 +549,14 @@ func deleteFederatedPost(app *app, p *PublicPost, collID int64) error { inboxes := map[string][]string{} for _, f := range *followers { - if _, ok := inboxes[f.SharedInbox]; ok { - inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID) + inbox := f.SharedInbox + if inbox == "" { + inbox = f.Inbox + } + if _, ok := inboxes[inbox]; ok { + inboxes[inbox] = append(inboxes[inbox], f.ActorID) } else { - inboxes[f.SharedInbox] = []string{f.ActorID} + inboxes[inbox] = []string{f.ActorID} } } @@ -592,10 +596,14 @@ func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error { inboxes := map[string][]string{} for _, f := range *followers { - if _, ok := inboxes[f.SharedInbox]; ok { - inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID) + inbox := f.SharedInbox + if inbox == "" { + inbox = f.Inbox + } + if _, ok := inboxes[inbox]; ok { + inboxes[inbox] = append(inboxes[inbox], f.ActorID) } else { - inboxes[f.SharedInbox] = []string{f.ActorID} + inboxes[inbox] = []string{f.ActorID} } } From 08799b220bc14477c38f6fc2766051ea8263fd97 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 3 Jun 2019 11:55:42 -0700 Subject: [PATCH 8/9] revert accidental .vscode folder inclusion --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f06b593..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "go.formatTool": "goimports" -} \ No newline at end of file From d58c142467ea0f33aea3b5a0b6b2d986721ed11a Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 5 Jun 2019 09:39:22 -0700 Subject: [PATCH 9/9] change delete post authentication logic this reorders the logic for checking authentication on post deletes to first check for a provided edit token and after check for an access token or auth'd user. --- posts.go | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/posts.go b/posts.go index e4cc81b..edd1c91 100644 --- a/posts.go +++ b/posts.go @@ -14,6 +14,12 @@ import ( "database/sql" "encoding/json" "fmt" + "html/template" + "net/http" + "regexp" + "strings" + "time" + "github.com/gorilla/mux" "github.com/guregu/null" "github.com/guregu/null/zero" @@ -31,11 +37,6 @@ import ( "github.com/writeas/web-core/tags" "github.com/writeas/writefreely/page" "github.com/writeas/writefreely/parse" - "html/template" - "net/http" - "regexp" - "strings" - "time" ) const ( @@ -737,7 +738,24 @@ func deletePost(app *app, w http.ResponseWriter, r *http.Request) error { var collID sql.NullInt64 var coll *Collection var pp *PublicPost - if accessToken != "" || u != nil { + if editToken != "" { + // TODO: SELECT owner_id, as well, and return appropriate error if NULL instead of running two queries + var dummy int64 + err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ?", friendlyID).Scan(&dummy) + switch { + case err == sql.ErrNoRows: + return impart.HTTPError{http.StatusNotFound, "Post not found."} + } + err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ? AND owner_id IS NULL", friendlyID).Scan(&dummy) + switch { + case err == sql.ErrNoRows: + // Post already has an owner. This could provide a bad experience + // for the user, but it's more important to ensure data isn't lost + // unexpectedly. So prevent deletion via token. + return impart.HTTPError{http.StatusConflict, "This post belongs to some user (hopefully yours). Please log in and delete it from that user's account."} + } + res, err = app.db.Exec("DELETE FROM posts WHERE id = ? AND modify_token = ? AND owner_id IS NULL", friendlyID, editToken) + } else if accessToken != "" || u != nil { // Caller provided some way to authenticate; assume caller expects the // post to be deleted based on a specific post owner, thus we should // return corresponding errors. @@ -785,26 +803,7 @@ func deletePost(app *app, w http.ResponseWriter, r *http.Request) error { res, err = t.Exec("DELETE FROM posts WHERE id = ? AND owner_id = ?", friendlyID, ownerID) } } else { - if editToken == "" { - return impart.HTTPError{http.StatusBadRequest, "No authenticated user or post token given."} - } - - // TODO: SELECT owner_id, as well, and return appropriate error if NULL instead of running two queries - var dummy int64 - err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ?", friendlyID).Scan(&dummy) - switch { - case err == sql.ErrNoRows: - return impart.HTTPError{http.StatusNotFound, "Post not found."} - } - err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ? AND owner_id IS NULL", friendlyID).Scan(&dummy) - switch { - case err == sql.ErrNoRows: - // Post already has an owner. This could provide a bad experience - // for the user, but it's more important to ensure data isn't lost - // unexpectedly. So prevent deletion via token. - return impart.HTTPError{http.StatusConflict, "This post belongs to some user (hopefully yours). Please log in and delete it from that user's account."} - } - res, err = app.db.Exec("DELETE FROM posts WHERE id = ? AND modify_token = ? AND owner_id IS NULL", friendlyID, editToken) + return impart.HTTPError{http.StatusBadRequest, "No authenticated user or post token given."} } if err != nil { return err