Merge branch 'develop' into librarization

pull/102/head
Matt Baer 6 years ago committed by GitHub
commit d5c2fe47da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 68
      activitypub.go
  2. 31
      activitypub_test.go
  3. 5
      export.go
  4. 5
      less/core.less
  5. 58
      posts.go
  6. 1
      templates/edit-meta.tmpl

@ -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 { inbox := f.SharedInbox
inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID) 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 { inbox := f.SharedInbox
inboxes[f.SharedInbox] = append(inboxes[f.SharedInbox], f.ActorID) 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 { if err := unmarshalActor(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
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."}
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)
} }
if err != nil { if err != nil {
return err return err

@ -263,6 +263,7 @@
</dd> </dd>
<dt>&nbsp;</dt><dd><input type="submit" value="Save changes" /></dd> <dt>&nbsp;</dt><dd><input type="submit" value="Save changes" /></dd>
</dl> </dl>
<input type="hidden" name="web" value="true" />
</form> </form>
</div> </div>

Loading…
Cancel
Save