From 95a98234eb9dc3fe893515673543b7b759fb558f Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 9 Aug 2019 14:04:15 -0700 Subject: [PATCH 001/118] fix panic on duplicate remoteuser key this changes handleFetchCollectionInbox to log _all_ errors after attempting to insert an actor in the remoteusers table. previously checking for all errors _except_ duplicate keys would cause a panic if an actor made a request to follow while already having followed. --- activitypub.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/activitypub.go b/activitypub.go index 997609d..d47a7ea 100644 --- a/activitypub.go +++ b/activitypub.go @@ -375,11 +375,11 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // Add follower locally, since it wasn't found before res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox) if err != nil { - if !app.db.isDuplicateKeyErr(err) { - t.Rollback() - log.Error("Couldn't add new remoteuser in DB: %v\n", err) - return - } + // if duplicate key, res will be nil and panic on + // res.LastInsertId below + t.Rollback() + log.Error("Couldn't add new remoteuser in DB: %v\n", err) + return } followerID, err = res.LastInsertId() From ee4fe2f4adf635284080f91118d87675ad843b42 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 16 Aug 2019 14:27:24 -0700 Subject: [PATCH 002/118] add basic text file imports this adds basic support for importing files as blog posts. .txt and .md are supported at this time and the collection is selectable, defaulting to draft. if a collection is specified the post is federated. --- account.go | 13 +-- account_import.go | 134 +++++++++++++++++++++++++++++ go.mod | 4 +- go.sum | 12 +++ routes.go | 9 +- templates/user/import.tmpl | 33 +++++++ templates/user/include/header.tmpl | 2 + 7 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 account_import.go create mode 100644 templates/user/import.tmpl diff --git a/account.go b/account.go index 1cf259b..9db9b1f 100644 --- a/account.go +++ b/account.go @@ -13,6 +13,13 @@ package writefreely import ( "encoding/json" "fmt" + "html/template" + "net/http" + "regexp" + "strings" + "sync" + "time" + "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/guregu/null/zero" @@ -22,12 +29,6 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/writefreely/author" "github.com/writeas/writefreely/page" - "html/template" - "net/http" - "regexp" - "strings" - "sync" - "time" ) type ( diff --git a/account_import.go b/account_import.go new file mode 100644 index 0000000..c8fe8a2 --- /dev/null +++ b/account_import.go @@ -0,0 +1,134 @@ +package writefreely + +import ( + "fmt" + "html/template" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/hashicorp/go-multierror" + "github.com/writeas/impart" + wfimport "github.com/writeas/import" + "github.com/writeas/web-core/log" +) + +func viewImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error { + // Fetch extra user data + p := NewUserPage(app, r, u, "Import", nil) + + c, err := app.db.GetCollections(u) + if err != nil { + return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("unable to fetch collections: %v", err)} + } + + d := struct { + *UserPage + Collections *[]Collection + Flashes []template.HTML + }{ + UserPage: p, + Collections: c, + Flashes: []template.HTML{}, + } + + flashes, _ := getSessionFlashes(app, w, r, nil) + for _, flash := range flashes { + d.Flashes = append(d.Flashes, template.HTML(flash)) + } + + showUserPage(w, "import", d) + return nil +} + +func handleImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error { + // limit 10MB per submission + r.ParseMultipartForm(10 << 20) + files := r.MultipartForm.File["files"] + var fileErrs []error + for _, formFile := range files { + // TODO: count uploaded files that succeed and report back with message + file, err := formFile.Open() + if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to open form file: %s", formFile.Filename)) + log.Error("import textfile: open from form: %v", err) + continue + } + defer file.Close() + + tempFile, err := ioutil.TempFile("", "post-upload-*.txt") + if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to create temporary file for: %s", formFile.Filename)) + log.Error("import textfile: create temp file: %v", err) + continue + } + defer tempFile.Close() + + _, err = io.Copy(tempFile, file) + if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to copy file into temporary location: %s", formFile.Filename)) + log.Error("import textfile: copy to temp: %v", err) + continue + } + + info, err := tempFile.Stat() + if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to get file info of: %s", formFile.Filename)) + log.Error("import textfile: stat temp file: %v", err) + continue + } + post, err := wfimport.FromFile(filepath.Join(os.TempDir(), info.Name())) + if err == wfimport.ErrEmptyFile { + // not a real error so don't log + _ = addSessionFlash(app, w, r, fmt.Sprintf("%s was empty, import skipped", formFile.Filename), nil) + continue + } else if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to read copy of %s", formFile.Filename)) + log.Error("import textfile: file to post: %v", err) + continue + } + + post.Collection = r.PostFormValue("collection") + coll, _ := app.db.GetCollection(post.Collection) + if coll == nil { + coll = &Collection{ + ID: 0, + } + } + + submittedPost := SubmittedPost{ + Title: &post.Title, + Content: &post.Content, + Font: "norm", + } + if coll.ID != 0 && app.cfg.App.Federation { + token, err := app.db.GetAccessToken(u.ID) + if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to authenticate uploading: %s", formFile.Filename)) + log.Error("import textfile: get accesstoken: %+v", err) + continue + } + ownedPost, err := app.db.CreateOwnedPost(&submittedPost, token, coll.Alias, app.cfg.App.Host) + if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to create owned post for %s", formFile.Filename)) + log.Error("import textfile: create owned post: %v", err) + continue + } + go federatePost(app, ownedPost, coll.ID, false) + } else { + _, err = app.db.CreatePost(u.ID, coll.ID, &submittedPost) + if err != nil { + fileErrs = append(fileErrs, fmt.Errorf("failed to create post from %s", formFile.Filename)) + log.Error("import textfile: create db post: %v", err) + continue + } + } + } + if len(fileErrs) != 0 { + _ = addSessionFlash(app, w, r, multierror.ListFormatFunc(fileErrs), nil) + } + + return impart.HTTPError{http.StatusFound, "/me/import"} +} diff --git a/go.mod b/go.mod index cc5fc57..8c62e9e 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/gorilla/schema v1.0.2 github.com/gorilla/sessions v1.1.3 github.com/guregu/null v3.4.0+incompatible + github.com/hashicorp/go-multierror v1.0.0 github.com/ikeikeikeike/go-sitemap-generator v1.0.1 github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 github.com/imdario/mergo v0.3.7 // indirect @@ -56,6 +57,7 @@ require ( github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2 github.com/writeas/httpsig v1.0.0 github.com/writeas/impart v1.1.0 + github.com/writeas/import v0.0.0-20190815235139-628d10daaa9e github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 github.com/writeas/nerds v1.0.0 github.com/writeas/openssl-go v1.0.0 // indirect @@ -63,7 +65,7 @@ require ( github.com/writeas/slug v1.2.0 github.com/writeas/web-core v1.0.0 github.com/writefreely/go-nodeinfo v1.2.0 - golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f // indirect + golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 // indirect golang.org/x/sys v0.0.0-20190209173611-3b5209105503 // indirect diff --git a/go.sum b/go.sum index 8898bec..30e4a9d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= +code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f h1:m1tYqjD/N0vF/S8s/ZKz/eccUr8RAAcrOK2MhXeTegA= @@ -80,6 +82,10 @@ github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9R github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM= github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/ikeikeikeike/go-sitemap-generator v1.0.1 h1:49Fn8gro/B12vCY8pf5/+/Jpr3kwB9TvP0MSymo69SY= github.com/ikeikeikeike/go-sitemap-generator v1.0.1/go.mod h1:QI+zWsz6yQyxkG9LWNcnu0f7aiAE5tPdsZOsICgmd1c= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM= @@ -151,10 +157,16 @@ github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6Fk github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2 h1:DUsp4OhdfI+e6iUqcPQlwx8QYXuUDsToTz/x82D3Zuo= github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2/go.mod h1:w2VxyRO/J5vfNjJHYVubsjUGHd3RLDoVciz0DE3ApOc= +github.com/writeas/go-writeas v1.1.0 h1:WHGm6wriBkxYAOGbvriXH8DlMUGOi6jhSZLUZKQ+4mQ= +github.com/writeas/go-writeas v1.1.0/go.mod h1:oh9U1rWaiE0p3kzdKwwvOpNXgp0P0IELI7OLOwV4fkA= github.com/writeas/httpsig v1.0.0 h1:peIAoIA3DmlP8IG8tMNZqI4YD1uEnWBmkcC9OFPjt3A= github.com/writeas/httpsig v1.0.0/go.mod h1:7ClMGSrSVXJbmiLa17bZ1LrG1oibGZmUMlh3402flPY= github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= +github.com/writeas/import v0.0.0-20190815214647-baae8acd8d06 h1:S6oKKP8GhSoyZUvVuhO9UiQ9f+U1aR/x5B4MP7YQHaU= +github.com/writeas/import v0.0.0-20190815214647-baae8acd8d06/go.mod h1:f3K8z7YnJwKnPIT4h7980n9C6cQb4DIB2QcxVCTB7lE= +github.com/writeas/import v0.0.0-20190815235139-628d10daaa9e h1:31PkvDTWkjzC1nGzWw9uAE92ZfcVyFX/K9L9ejQjnEs= +github.com/writeas/import v0.0.0-20190815235139-628d10daaa9e/go.mod h1:f3K8z7YnJwKnPIT4h7980n9C6cQb4DIB2QcxVCTB7lE= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 h1:baEp0631C8sT2r/hqwypIw2snCFZa6h7U6TojoLHu/c= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219/go.mod h1:NyM35ayknT7lzO6O/1JpfgGyv+0W9Z9q7aE0J8bXxfQ= github.com/writeas/nerds v1.0.0 h1:ZzRcCN+Sr3MWID7o/x1cr1ZbLvdpej9Y1/Ho+JKlqxo= diff --git a/routes.go b/routes.go index 724c532..022a7ea 100644 --- a/routes.go +++ b/routes.go @@ -11,13 +11,14 @@ package writefreely import ( + "net/http" + "path/filepath" + "strings" + "github.com/gorilla/mux" "github.com/writeas/go-webfinger" "github.com/writeas/web-core/log" "github.com/writefreely/go-nodeinfo" - "net/http" - "path/filepath" - "strings" ) // InitStaticRoutes adds routes for serving static files. @@ -93,6 +94,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { me.HandleFunc("/posts/export.json", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET") me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET") me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET") + me.HandleFunc("/import", handler.User(viewImport)).Methods("GET") + me.HandleFunc("/import", handler.User(handleImport)).Methods("POST") me.HandleFunc("/settings", handler.User(viewSettings)).Methods("GET") me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET") me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET") diff --git a/templates/user/import.tmpl b/templates/user/import.tmpl new file mode 100644 index 0000000..c258783 --- /dev/null +++ b/templates/user/import.tmpl @@ -0,0 +1,33 @@ +{{define "import"}} +{{template "header" .}} + +
+

Import

+

Upload text or markdown files to import as posts.

+
+
+ + +
+ + +
+ +
+
+ {{if .Flashes}} +
    + {{range .Flashes}}
  • {{.}}
  • {{end}} +
+ {{end}} +
+ +{{template "footer" .}} +{{end}} \ No newline at end of file diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl index 312d0b8..e8fd908 100644 --- a/templates/user/include/header.tmpl +++ b/templates/user/include/header.tmpl @@ -27,6 +27,7 @@ {{if .IsAdmin}}
  • Admin
  • {{end}}
  • Settings
  • Export
  • +
  • Import

  • Log out
  • @@ -45,6 +46,7 @@ {{if .IsAdmin}}
  • Admin dashboard
  • {{end}}
  • Account settings
  • Export
  • +
  • Import
  • {{if .CanInvite}}
  • Invite people
  • {{end}}

  • Log out
  • From 0ca198c715a81161d79ba65c23046424a5fbe601 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Sat, 17 Aug 2019 16:18:40 -0700 Subject: [PATCH 003/118] include nice alert message on success different template action for partial or complete import success --- account_import.go | 25 +++++++++++++++++++++++-- templates/user/import.tmpl | 5 +++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/account_import.go b/account_import.go index c8fe8a2..3fd434e 100644 --- a/account_import.go +++ b/account_import.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "github.com/hashicorp/go-multierror" "github.com/writeas/impart" @@ -28,6 +29,8 @@ func viewImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error *UserPage Collections *[]Collection Flashes []template.HTML + Message string + InfoMsg bool }{ UserPage: p, Collections: c, @@ -36,7 +39,14 @@ func viewImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error flashes, _ := getSessionFlashes(app, w, r, nil) for _, flash := range flashes { - d.Flashes = append(d.Flashes, template.HTML(flash)) + if strings.HasPrefix(flash, "SUCCESS: ") { + d.Message = strings.TrimPrefix(flash, "SUCCESS: ") + } else if strings.HasPrefix(flash, "INFO: ") { + d.Message = strings.TrimPrefix(flash, "INFO: ") + d.InfoMsg = true + } else { + d.Flashes = append(d.Flashes, template.HTML(flash)) + } } showUserPage(w, "import", d) @@ -48,8 +58,9 @@ func handleImport(app *App, u *User, w http.ResponseWriter, r *http.Request) err r.ParseMultipartForm(10 << 20) files := r.MultipartForm.File["files"] var fileErrs []error + filesSubmitted := len(files) + var filesImported int for _, formFile := range files { - // TODO: count uploaded files that succeed and report back with message file, err := formFile.Open() if err != nil { fileErrs = append(fileErrs, fmt.Errorf("failed to open form file: %s", formFile.Filename)) @@ -125,10 +136,20 @@ func handleImport(app *App, u *User, w http.ResponseWriter, r *http.Request) err continue } } + filesImported++ } if len(fileErrs) != 0 { _ = addSessionFlash(app, w, r, multierror.ListFormatFunc(fileErrs), nil) } + if filesImported == filesSubmitted { + verb := "posts" + if filesSubmitted == 1 { + verb = "post" + } + _ = addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: Import complete, %d %s imported.", filesImported, verb), nil) + } else if filesImported > 0 { + _ = addSessionFlash(app, w, r, fmt.Sprintf("INFO: %d of %d posts imported, see details below.", filesImported, filesSubmitted), nil) + } return impart.HTTPError{http.StatusFound, "/me/import"} } diff --git a/templates/user/import.tmpl b/templates/user/import.tmpl index c258783..3833095 100644 --- a/templates/user/import.tmpl +++ b/templates/user/import.tmpl @@ -2,6 +2,11 @@ {{template "header" .}}
    + {{if .Message}} +
    +

    {{.Message}}

    +
    + {{end}}

    Import

    Upload text or markdown files to import as posts.

    From 6c5d89ac86d3c51746a0501a2135e92e4ba16631 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 19 Aug 2019 09:05:52 -0700 Subject: [PATCH 004/118] move import post handler under /api handler for post request to import is now under /api/me/import form target updated also allow all plaintext files in form --- routes.go | 2 +- templates/user/import.tmpl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/routes.go b/routes.go index 022a7ea..7dcdc65 100644 --- a/routes.go +++ b/routes.go @@ -95,7 +95,6 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET") me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET") me.HandleFunc("/import", handler.User(viewImport)).Methods("GET") - me.HandleFunc("/import", handler.User(handleImport)).Methods("POST") me.HandleFunc("/settings", handler.User(viewSettings)).Methods("GET") me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET") me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET") @@ -108,6 +107,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST") apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST") apiMe.HandleFunc("/invites", handler.User(handleCreateUserInvite)).Methods("POST") + apiMe.HandleFunc("/import", handler.User(handleImport)).Methods("POST") // Sign up validation write.HandleFunc("/api/alias", handler.All(handleUsernameCheck)).Methods("POST") diff --git a/templates/user/import.tmpl b/templates/user/import.tmpl index 3833095..002cf14 100644 --- a/templates/user/import.tmpl +++ b/templates/user/import.tmpl @@ -10,9 +10,9 @@

    Import

    Upload text or markdown files to import as posts.

    -
    + - +
    +
    {{else}} diff --git a/templates/user/articles.tmpl b/templates/user/articles.tmpl index 67d3e0b..3edb89c 100644 --- a/templates/user/articles.tmpl +++ b/templates/user/articles.tmpl @@ -6,6 +6,9 @@ {{if .Flashes}}
      {{range .Flashes}}
    • {{.}}
    • {{end}}
    {{end}} +{{if .Suspended}} + {{template "user-suspended"}} +{{end}}

    drafts

    diff --git a/templates/user/collection.tmpl b/templates/user/collection.tmpl index 8af3bda..edd06c1 100644 --- a/templates/user/collection.tmpl +++ b/templates/user/collection.tmpl @@ -8,6 +8,9 @@
    + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

    Customize {{.DisplayTitle}} view blog

    {{if .Flashes}}
      diff --git a/templates/user/collections.tmpl b/templates/user/collections.tmpl index 481fd8f..7f6e83c 100644 --- a/templates/user/collections.tmpl +++ b/templates/user/collections.tmpl @@ -7,6 +7,9 @@ {{range .Flashes}}
    • {{.}}
    • {{end}}
    {{end}} +{{if .Suspended}} + {{template "user-suspended"}} +{{end}}

    blogs

      {{range $i, $el := .Collections}}
    • diff --git a/templates/user/include/suspended.tmpl b/templates/user/include/suspended.tmpl new file mode 100644 index 0000000..b1e42c8 --- /dev/null +++ b/templates/user/include/suspended.tmpl @@ -0,0 +1,6 @@ +{{define "user-suspended"}} +
      +

      This account is currently suspended.

      +

      Please contact the instance administrator to discuss reactivation.

      +
      +{{end}} diff --git a/templates/user/settings.tmpl b/templates/user/settings.tmpl index 822d091..d5cc33d 100644 --- a/templates/user/settings.tmpl +++ b/templates/user/settings.tmpl @@ -7,17 +7,14 @@ h3 { font-weight: normal; } .section > *:not(input) { font-size: 0.86em; }
      + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

      {{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}admin settings{{end}}{{end}}

      {{if .Flashes}}
        {{range .Flashes}}
      • {{.}}
      • {{end}}
      {{end}} - {{if .Suspended}} -
      -

      This account is currently suspended.

      -

      Please contact the instance administrator to discuss reactivation.

      -
      - {{end}} {{ if .IsLogOut }}

      Please add an email address and/or passphrase so you can log in again later.

      diff --git a/templates/user/stats.tmpl b/templates/user/stats.tmpl index f5588fb..705f1e0 100644 --- a/templates/user/stats.tmpl +++ b/templates/user/stats.tmpl @@ -17,6 +17,9 @@ td.none {
      + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

      {{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats

      Stats for all time.

      diff --git a/users.go b/users.go index e8be252..05b5bb4 100644 --- a/users.go +++ b/users.go @@ -19,6 +19,13 @@ import ( "github.com/writeas/writefreely/key" ) +type UserStatus int + +const ( + UserActive = iota + UserSuspended +) + type ( userCredentials struct { Alias string `json:"alias" schema:"alias"` @@ -59,7 +66,7 @@ type ( HasPass bool `json:"has_pass"` Email zero.String `json:"email"` Created time.Time `json:"created"` - Suspended bool `json:"suspended"` + Status UserStatus `json:"status"` clearEmail string `json:"email"` } From 5429ca4ab09258c264468d510dd6246abe385d9e Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 25 Oct 2019 13:40:32 -0700 Subject: [PATCH 018/118] add check for suspended user on single posts also fix logic bug in posts.go viewCollectionPost checking the page owner --- posts.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/posts.go b/posts.go index 15d93c8..6974a4f 100644 --- a/posts.go +++ b/posts.go @@ -387,10 +387,6 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { return ErrInternalGeneral } - if suspended { - return ErrPostNotFound - } - // Check if post has been unpublished if content == "" { gone = true @@ -438,9 +434,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { page := struct { *AnonymousPost page.StaticPage - Username string - IsOwner bool - SiteURL string + Username string + IsOwner bool + SiteURL string + Suspended bool }{ AnonymousPost: post, StaticPage: pageForReq(app, r), @@ -451,6 +448,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID } + if !page.IsOwner && suspended { + return ErrPostNotFound + } + page.Suspended = suspended err = templates["post"].ExecuteTemplate(w, "post", page) if err != nil { log.Error("Post template execute error: %v", err) @@ -1389,7 +1390,7 @@ Are you sure it was ever here?`, return err } } - p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64 + p.IsOwner = owner != nil && p.OwnerID.Valid && u.ID == p.OwnerID.Int64 p.Collection = coll p.IsTopLevel = app.cfg.App.SingleUser From bf4f8793832df63f2e426ac9edc556029d6a738b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 4 Nov 2019 14:06:24 -0500 Subject: [PATCH 019/118] Update hosting options in README Now: Write.as Pro and Write.as for Teams --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4f0b6bb..68da89b 100644 --- a/README.md +++ b/README.md @@ -47,15 +47,15 @@ It's designed to be flexible and share your writing widely, so it's built around ## Hosting -We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as](https://write.as) for individuals, and [WriteFreely.host](https://writefreely.host) for communities. Besides saving you time, as a customer you directly help fund WriteFreely development. +We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as Pro](https://write.as/pro) for individuals, and [Write.as for Teams](https://write.as/for/teams) for businesses. Besides saving you time and effort, both services directly fund WriteFreely development and ensure the long-term sustainability of our open source work. -### [![Write.as](https://write.as/img/writeas-wf-readme.png)](https://write.as/) +### [![Write.as Pro](https://writefreely.org/img/writeas-pro-readme.png)](https://write.as/pro) -Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pricing). +Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pro). -### [![WriteFreely.host](https://writefreely.host/img/wfhost-wf-readme.png)](https://writefreely.host) +### [![Write.as for Teams](https://writefreely.org/img/writeas-for-teams-readme.png)](https://write.as/for/teams) -[WriteFreely.host](https://writefreely.host) makes it easy to start a close-knit community — to share knowledge, complement your Mastodon instance, or publish updates in your organization. We take care of the hosting, upgrades, backups, and maintenance so you can focus on writing. +[Write.as for Teams](https://write.as/for/teams) gives your organization, business, or [open source project](https://write.as/for/open-source) a clutter-free space to share updates or proposals and build your collective knowledge. We take care of hosting, upgrades, backups, and maintenance so your team can focus on writing. ## Quick start From da7dcfee6affa0dc02255bcbd6669dce606dbd26 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 7 Nov 2019 14:07:00 +0900 Subject: [PATCH 020/118] Move admin template IsSuspended logic into method This adds a User.IsSuspended() method and uses it when displaying the user's status on admin pages, instead of doing a magic number check. This should also help in the future, in case this logic ever changes. Ref T661 --- templates/user/admin/users.tmpl | 5 ++--- templates/user/admin/view-user.tmpl | 11 ++++++----- users.go | 4 ++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/templates/user/admin/users.tmpl b/templates/user/admin/users.tmpl index 6d5d6f7..03a3f6c 100644 --- a/templates/user/admin/users.tmpl +++ b/templates/user/admin/users.tmpl @@ -19,9 +19,8 @@ {{.CreatedFriendly}} {{if .IsAdmin}}Admin{{else}}User{{end}} - {{if eq .Status 1}}suspended{{else}}active{{end}} + {{if .IsSuspended}}suspended{{else}}active{{end}} + {{end}} diff --git a/templates/user/admin/view-user.tmpl b/templates/user/admin/view-user.tmpl index 01eb1f0..9226c39 100644 --- a/templates/user/admin/view-user.tmpl +++ b/templates/user/admin/view-user.tmpl @@ -60,14 +60,15 @@ td.active-suspend > input[type="submit"] { Status - {{if eq .User.Status 1}} -

      User is currently Suspended

      - {{else}} -

      User is currently Active

      + {{if .User.IsSuspended}} +

      Suspended

      + + {{else}} +

      Active

      - {{end}} + diff --git a/users.go b/users.go index 05b5bb4..5eb2e61 100644 --- a/users.go +++ b/users.go @@ -126,3 +126,7 @@ func (u *User) IsAdmin() bool { // TODO: get this from database return u.ID == 1 } + +func (u *User) IsSuspended() bool { + return u.Status&UserSuspended != 0 +} From c9f72198310078a01ee00810210a22a1493c6fe1 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 7 Nov 2019 16:49:52 +0900 Subject: [PATCH 021/118] Move user status in list out of
      The link here is a little redundant, and might make people think that it actually changes the status by clicking on it. --- templates/user/admin/users.tmpl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/templates/user/admin/users.tmpl b/templates/user/admin/users.tmpl index 03a3f6c..df840b2 100644 --- a/templates/user/admin/users.tmpl +++ b/templates/user/admin/users.tmpl @@ -18,9 +18,7 @@ {{.Username}} {{.CreatedFriendly}} {{if .IsAdmin}}Admin{{else}}User{{end}} - - {{if .IsSuspended}}suspended{{else}}active{{end}} - + {{if .IsSuspended}}Suspended{{else}}Active{{end}} {{end}} From 280c32afdce41c473a5844ee6553ff03544750f6 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 7 Nov 2019 16:59:02 +0900 Subject: [PATCH 022/118] Confirm suspension before submitting the form This also includes a bit of explanation about what suspending a user actually does. Ref T661 --- templates/user/admin/view-user.tmpl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/templates/user/admin/view-user.tmpl b/templates/user/admin/view-user.tmpl index 9226c39..3b13c33 100644 --- a/templates/user/admin/view-user.tmpl +++ b/templates/user/admin/view-user.tmpl @@ -57,7 +57,7 @@ td.active-suspend > input[type="submit"] { {{if .LastPost}}{{.LastPost}}{{else}}Never{{end}} -
      + Status @@ -116,5 +116,11 @@ td.active-suspend > input[type="submit"] { {{end}}
      + + {{template "footer" .}} {{end}} From 619b10c3e5f4f4e28752dbe302a632ea92c0d84a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 7 Nov 2019 17:09:43 +0900 Subject: [PATCH 023/118] Fix "suspended" message location on Drafts Previously it was above the header. Ref T661 --- templates/post.tmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/post.tmpl b/templates/post.tmpl index 74135a3..52d53a9 100644 --- a/templates/post.tmpl +++ b/templates/post.tmpl @@ -35,10 +35,6 @@ {{template "highlighting" .}} - - {{if .Suspended}} - {{template "user-suspended"}} - {{end}}

      {{.SiteName}}

      + + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}
      {{if .Title}}

      {{.Title}}

      {{end}}{{ if .IsPlainText }}

      {{.Content}}

      {{ else }}
      {{.HTMLContent}}
      {{ end }}
      From e1149cd1e95154e4e72aca606e5582ad5dcbc7cf Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 7 Nov 2019 17:24:04 +0900 Subject: [PATCH 024/118] Fix URLs in CSV exports This includes the instance's hostname in calls to export a CSV file and PublicPost.CanonicalURL(). It also fixes a panic in that method during CSV export caused by draft posts. --- account.go | 2 +- export.go | 5 +++-- posts.go | 6 +++--- read.go | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/account.go b/account.go index 920fc9d..180e9b0 100644 --- a/account.go +++ b/account.go @@ -625,7 +625,7 @@ func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, // Export as CSV if strings.HasSuffix(r.URL.Path, ".csv") { - data = exportPostsCSV(u, posts) + data = exportPostsCSV(app.cfg.App.Host, u, posts) return data, filename, err } if strings.HasSuffix(r.URL.Path, ".zip") { diff --git a/export.go b/export.go index 3b5ac49..592bc0c 100644 --- a/export.go +++ b/export.go @@ -20,7 +20,7 @@ import ( "github.com/writeas/web-core/log" ) -func exportPostsCSV(u *User, posts *[]PublicPost) []byte { +func exportPostsCSV(hostName string, u *User, posts *[]PublicPost) []byte { var b bytes.Buffer r := [][]string{ @@ -30,8 +30,9 @@ func exportPostsCSV(u *User, posts *[]PublicPost) []byte { var blog string if p.Collection != nil { blog = p.Collection.Alias + p.Collection.hostName = hostName } - f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)} + f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(hostName), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)} r = append(r, f) } diff --git a/posts.go b/posts.go index 1f35eda..d004296 100644 --- a/posts.go +++ b/posts.go @@ -1061,9 +1061,9 @@ func (p *Post) processPost() PublicPost { return *res } -func (p *PublicPost) CanonicalURL() string { +func (p *PublicPost) CanonicalURL(hostName string) string { if p.Collection == nil || p.Collection.Alias == "" { - return p.Collection.hostName + "/" + p.ID + return hostName + "/" + p.ID } return p.Collection.CanonicalURL() + p.Slug.String } @@ -1072,7 +1072,7 @@ func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object o := activitystreams.NewArticleObject() o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID o.Published = p.Created - o.URL = p.CanonicalURL() + o.URL = p.CanonicalURL(cfg.App.Host) o.AttributedTo = p.Collection.FederatedAccount() o.CC = []string{ p.Collection.FederatedAccount() + "/followers", diff --git a/read.go b/read.go index ec0305a..df24621 100644 --- a/read.go +++ b/read.go @@ -293,7 +293,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e } title = p.PlainDisplayTitle() - permalink = p.CanonicalURL() + permalink = p.CanonicalURL(app.cfg.App.Host) if p.Collection != nil { author = p.Collection.Title } else { From c0b75f6b65e375b04a100ca6b9963e579d4282d5 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 8 Nov 2019 08:47:03 -0800 Subject: [PATCH 025/118] pass hostname to canonical url in post templates the change to take a hostname in Post.CanonicalURL broke a few template using that function. This adds a Hostname string to the Post being passed to templates and passes it to calls to Post.CanonicalURL --- posts.go | 2 ++ templates/chorus-collection-post.tmpl | 6 +++--- templates/chorus-collection.tmpl | 2 +- templates/collection-post.tmpl | 6 +++--- templates/collection-tags.tmpl | 2 +- templates/collection.tmpl | 2 +- templates/read.tmpl | 8 ++++---- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/posts.go b/posts.go index d004296..6cb76a2 100644 --- a/posts.go +++ b/posts.go @@ -1380,12 +1380,14 @@ Are you sure it was ever here?`, IsFound bool IsAdmin bool CanInvite bool + Hostname string }{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, + Hostname: app.cfg.App.Host, } tp.IsAdmin = u != nil && u.IsAdmin() tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index bab2e31..d229c62 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -8,7 +8,7 @@ - + @@ -25,7 +25,7 @@ - + {{range .Images}}{{else}}{{end}} @@ -77,7 +77,7 @@ article time.dt-published {


      diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl index e36d3b5..ebee403 100644 --- a/templates/chorus-collection.tmpl +++ b/templates/chorus-collection.tmpl @@ -68,7 +68,7 @@ body#collection header nav.tabs a:first-child { {{/*end*/}} {{if .PinnedPosts}} + {{range .PinnedPosts}}{{.PlainDisplayTitle}}{{end}} {{end}} diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index 7075226..a4084b3 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -9,7 +9,7 @@ {{ if .IsFound }} - + @@ -26,7 +26,7 @@ - + {{range .Images}}{{else}}{{end}} @@ -50,7 +50,7 @@

      {{end}} diff --git a/templates/read.tmpl b/templates/read.tmpl index 9541ab5..91fbeb4 100644 --- a/templates/read.tmpl +++ b/templates/read.tmpl @@ -87,17 +87,17 @@ {{ if gt (len .Posts) 0 }}
      {{range .Posts}}
      - {{if .Title.String}}

      + {{if .Title.String}}

      {{else}} -

      +

      {{end}}

      {{if .Collection}}from {{.Collection.DisplayTitle}}{{else}}Anonymous{{end}}

      {{if .Excerpt}}
      {{.Excerpt}}
      - {{localstr "Read more..." .Language.String}}{{else}}
      {{ if not .HTMLContent }}

      {{.Content}}

      {{ else }}{{.HTMLContent}}{{ end }}
       
      + {{localstr "Read more..." .Language.String}}{{else}}
      {{ if not .HTMLContent }}

      {{.Content}}

      {{ else }}{{.HTMLContent}}{{ end }}
       
      - {{localstr "Read more..." .Language.String}}{{end}}
      + {{localstr "Read more..." .Language.String}}{{end}} {{end}}
      {{ else }} From f66d5bf1e8fa35330f52c2bb6b58f9720831029b Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Sat, 9 Nov 2019 11:41:39 -0800 Subject: [PATCH 026/118] use .Host instead of adding .Hostname --- posts.go | 2 -- templates/chorus-collection-post.tmpl | 6 +++--- templates/chorus-collection.tmpl | 2 +- templates/collection-post.tmpl | 6 +++--- templates/collection-tags.tmpl | 2 +- templates/collection.tmpl | 2 +- templates/read.tmpl | 8 ++++---- 7 files changed, 13 insertions(+), 15 deletions(-) diff --git a/posts.go b/posts.go index 6cb76a2..d004296 100644 --- a/posts.go +++ b/posts.go @@ -1380,14 +1380,12 @@ Are you sure it was ever here?`, IsFound bool IsAdmin bool CanInvite bool - Hostname string }{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, - Hostname: app.cfg.App.Host, } tp.IsAdmin = u != nil && u.IsAdmin() tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index d229c62..18fb632 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -8,7 +8,7 @@ - + @@ -25,7 +25,7 @@ - + {{range .Images}}{{else}}{{end}} @@ -77,7 +77,7 @@ article time.dt-published {


      diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl index ebee403..14d5fbd 100644 --- a/templates/chorus-collection.tmpl +++ b/templates/chorus-collection.tmpl @@ -68,7 +68,7 @@ body#collection header nav.tabs a:first-child { {{/*end*/}} {{if .PinnedPosts}} + {{range .PinnedPosts}}{{.PlainDisplayTitle}}{{end}} {{end}} diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index a4084b3..4af5cb8 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -9,7 +9,7 @@ {{ if .IsFound }} - + @@ -26,7 +26,7 @@ - + {{range .Images}}{{else}}{{end}} @@ -50,7 +50,7 @@

      {{end}} diff --git a/templates/read.tmpl b/templates/read.tmpl index 91fbeb4..f1cbf29 100644 --- a/templates/read.tmpl +++ b/templates/read.tmpl @@ -87,17 +87,17 @@ {{ if gt (len .Posts) 0 }}
      {{range .Posts}}
      - {{if .Title.String}}

      + {{if .Title.String}}

      {{else}} -

      +

      {{end}}

      {{if .Collection}}from {{.Collection.DisplayTitle}}{{else}}Anonymous{{end}}

      {{if .Excerpt}}
      {{.Excerpt}}
      - {{localstr "Read more..." .Language.String}}{{else}}
      {{ if not .HTMLContent }}

      {{.Content}}

      {{ else }}{{.HTMLContent}}{{ end }}
       
      + {{localstr "Read more..." .Language.String}}{{else}}
      {{ if not .HTMLContent }}

      {{.Content}}

      {{ else }}{{.HTMLContent}}{{ end }}
       
      - {{localstr "Read more..." .Language.String}}{{end}}
      + {{localstr "Read more..." .Language.String}}{{end}} {{end}}
      {{ else }} From 2c2ee0c00cd80e199678ac53adac25d6ff5803a3 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 11 Nov 2019 15:16:04 +0900 Subject: [PATCH 027/118] Tweak "suspended" notification copy --- templates/user/include/suspended.tmpl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/user/include/suspended.tmpl b/templates/user/include/suspended.tmpl index b1e42c8..e5d9be8 100644 --- a/templates/user/include/suspended.tmpl +++ b/templates/user/include/suspended.tmpl @@ -1,6 +1,5 @@ {{define "user-suspended"}}
      -

      This account is currently suspended.

      -

      Please contact the instance administrator to discuss reactivation.

      +

      Your account is suspended. You can still access all of your posts and blogs, but no one else can currently see them.

      {{end}} From 6e09fcb9e2a3088c9c5ad1cbbbb5cc5947d2122a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 11 Nov 2019 16:02:22 +0900 Subject: [PATCH 028/118] Change password reset endpoint to /admin/user/{Username}/passphrase Ref T695 --- routes.go | 2 +- templates/user/admin/view-user.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routes.go b/routes.go index 003b7d1..de19ff2 100644 --- a/routes.go +++ b/routes.go @@ -144,7 +144,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET") write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") - write.HandleFunc("/admin/user/{username}", handler.Admin(handleAdminResetUserPass)).Methods("POST") + write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST") write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST") diff --git a/templates/user/admin/view-user.tmpl b/templates/user/admin/view-user.tmpl index 211297d..91fdaf1 100644 --- a/templates/user/admin/view-user.tmpl +++ b/templates/user/admin/view-user.tmpl @@ -62,7 +62,7 @@ button[type="submit"].danger { Password {{if not .OwnUserPage}} - +