@@ -530,10 +530,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} | |||
} | |||
} | |||
@@ -573,10 +577,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} | |||
} | |||
} | |||
@@ -629,8 +637,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."} | |||
} | |||
@@ -645,3 +652,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 | |||
} |
@@ -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" | |||
"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() | |||
@@ -252,6 +252,8 @@ body { | |||
margin-bottom: 0.25em; | |||
&+time { | |||
display: block; | |||
margin-top: 0.25em; | |||
margin-bottom: 0.25em; | |||
} | |||
} | |||
time { | |||
@@ -604,6 +606,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; | |||
@@ -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 ( | |||
@@ -67,7 +68,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 +625,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 | |||
} | |||
@@ -732,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. | |||
@@ -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) | |||
} | |||
} 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 | |||
@@ -263,6 +263,7 @@ | |||
</dd> | |||
<dt> </dt><dd><input type="submit" value="Save changes" /></dd> | |||
</dl> | |||
<input type="hidden" name="web" value="true" /> | |||
</form> | |||
</div> | |||