@@ -530,10 +530,14 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { | |||||
inboxes := map[string][]string{} | inboxes := map[string][]string{} | ||||
for _, f := range *followers { | 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 { | } else { | ||||
inboxes[f.SharedInbox] = []string{f.ActorID} | |||||
inboxes[inbox] = []string{f.ActorID} | |||||
} | } | ||||
} | } | ||||
@@ -573,10 +577,14 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { | |||||
inboxes := map[string][]string{} | inboxes := map[string][]string{} | ||||
for _, f := range *followers { | 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 { | } else { | ||||
inboxes[f.SharedInbox] = []string{f.ActorID} | |||||
inboxes[inbox] = []string{f.ActorID} | |||||
} | } | ||||
} | } | ||||
@@ -629,8 +637,7 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, | |||||
log.Error("Unable to get actor! %v", err) | log.Error("Unable to get actor! %v", err) | ||||
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."} | 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) | log.Error("Unable to unmarshal actor! %v", err) | ||||
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."} | return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."} | ||||
} | } | ||||
@@ -645,3 +652,48 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, | |||||
} | } | ||||
return actor, remoteUser, nil | 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 | |||||
} |
@@ -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) | |||||
} | |||||
} | |||||
} |
@@ -14,9 +14,10 @@ import ( | |||||
"archive/zip" | "archive/zip" | ||||
"bytes" | "bytes" | ||||
"encoding/csv" | "encoding/csv" | ||||
"github.com/writeas/web-core/log" | |||||
"strings" | "strings" | ||||
"time" | "time" | ||||
"github.com/writeas/web-core/log" | |||||
) | ) | ||||
func exportPostsCSV(u *User, posts *[]PublicPost) []byte { | func exportPostsCSV(u *User, posts *[]PublicPost) []byte { | ||||
@@ -37,7 +38,7 @@ func exportPostsCSV(u *User, posts *[]PublicPost) []byte { | |||||
w := csv.NewWriter(&b) | w := csv.NewWriter(&b) | ||||
w.WriteAll(r) // calls Flush internally | w.WriteAll(r) // calls Flush internally | ||||
if err := w.Error(); err != nil { | if err := w.Error(); err != nil { | ||||
log.Info("error writing csv:", err) | |||||
log.Info("error writing csv: %v", err) | |||||
} | } | ||||
return b.Bytes() | return b.Bytes() | ||||
@@ -252,6 +252,8 @@ body { | |||||
margin-bottom: 0.25em; | margin-bottom: 0.25em; | ||||
&+time { | &+time { | ||||
display: block; | display: block; | ||||
margin-top: 0.25em; | |||||
margin-bottom: 0.25em; | |||||
} | } | ||||
} | } | ||||
time { | time { | ||||
@@ -604,6 +606,9 @@ body#collection article, body#subpage article { | |||||
padding-top: 0; | padding-top: 0; | ||||
padding-bottom: 0; | padding-bottom: 0; | ||||
.book { | .book { | ||||
h2 { | |||||
font-size: 1.4em; | |||||
} | |||||
a.hidden.action { | a.hidden.action { | ||||
color: #666; | color: #666; | ||||
float: right; | float: right; | ||||
@@ -14,6 +14,12 @@ import ( | |||||
"database/sql" | "database/sql" | ||||
"encoding/json" | "encoding/json" | ||||
"fmt" | "fmt" | ||||
"html/template" | |||||
"net/http" | |||||
"regexp" | |||||
"strings" | |||||
"time" | |||||
"github.com/gorilla/mux" | "github.com/gorilla/mux" | ||||
"github.com/guregu/null" | "github.com/guregu/null" | ||||
"github.com/guregu/null/zero" | "github.com/guregu/null/zero" | ||||
@@ -31,11 +37,6 @@ import ( | |||||
"github.com/writeas/web-core/tags" | "github.com/writeas/web-core/tags" | ||||
"github.com/writeas/writefreely/page" | "github.com/writeas/writefreely/page" | ||||
"github.com/writeas/writefreely/parse" | "github.com/writeas/writefreely/parse" | ||||
"html/template" | |||||
"net/http" | |||||
"regexp" | |||||
"strings" | |||||
"time" | |||||
) | ) | ||||
const ( | const ( | ||||
@@ -67,7 +68,8 @@ type ( | |||||
} | } | ||||
AuthenticatedPost struct { | AuthenticatedPost struct { | ||||
ID string `json:"id" schema:"id"` | |||||
ID string `json:"id" schema:"id"` | |||||
Web bool `json:"web" schema:"web"` | |||||
*SubmittedPost | *SubmittedPost | ||||
} | } | ||||
@@ -623,6 +625,10 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||||
} | } | ||||
} | } | ||||
if p.Web { | |||||
p.IsRTL.Valid = true | |||||
} | |||||
if p.SubmittedPost == nil { | if p.SubmittedPost == nil { | ||||
return ErrPostNoUpdatableVals | return ErrPostNoUpdatableVals | ||||
} | } | ||||
@@ -732,7 +738,24 @@ func deletePost(app *App, w http.ResponseWriter, r *http.Request) error { | |||||
var collID sql.NullInt64 | var collID sql.NullInt64 | ||||
var coll *Collection | var coll *Collection | ||||
var pp *PublicPost | 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 | // Caller provided some way to authenticate; assume caller expects the | ||||
// post to be deleted based on a specific post owner, thus we should | // post to be deleted based on a specific post owner, thus we should | ||||
// return corresponding errors. | // return corresponding errors. | ||||
@@ -780,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) | res, err = t.Exec("DELETE FROM posts WHERE id = ? AND owner_id = ?", friendlyID, ownerID) | ||||
} | } | ||||
} else { | } 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 { | if err != nil { | ||||
return err | return err | ||||
@@ -263,6 +263,7 @@ | |||||
</dd> | </dd> | ||||
<dt> </dt><dd><input type="submit" value="Save changes" /></dd> | <dt> </dt><dd><input type="submit" value="Save changes" /></dd> | ||||
</dl> | </dl> | ||||
<input type="hidden" name="web" value="true" /> | |||||
</form> | </form> | ||||
</div> | </div> | ||||