From 81847fbbcc66e215c76bf745923836a75631e34f Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 5 Aug 2019 09:27:51 -0400 Subject: [PATCH 01/90] Land on Blogs page when SimpleNav is enabled This shows the Blogs page instead of the Editor to logged in users on the `/` path when the new `simple_nav` config option is enabled. Ref T680 --- app.go | 7 ++++++- config/config.go | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 1fa6f8a..6d6a689 100644 --- a/app.go +++ b/app.go @@ -193,7 +193,12 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { // Show correct page based on user auth status and configured landing path u := getUserSession(app, r) if u != nil { - // User is logged in, so show the Pad + // User is logged in, so show the Pad or Blogs page, depending on config + if app.cfg.App.SimpleNav { + // Simple nav, so home page is Blogs page + return viewCollections(app, u, w, r) + } + // Default config, so home page is editor return handleViewPad(app, w, r) } diff --git a/config/config.go b/config/config.go index f46282e..d026a74 100644 --- a/config/config.go +++ b/config/config.go @@ -68,6 +68,7 @@ type ( JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` Landing string `ini:"landing"` + SimpleNav bool `ini:"simple_nav"` // Users SingleUser bool `ini:"single_user"` From 90ad50c7f5c30f45065798304cf7708a47ac9342 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 5 Aug 2019 09:34:47 -0400 Subject: [PATCH 02/90] Use normal nav on user pages when SimpleNav This shows About, Reader, Log out links on backend user pages when logged in. It also adds "New post" buttons on the backend pages and blogs. --- less/core.less | 25 +++++++++++++++++++++++++ templates/base.tmpl | 2 +- templates/collection.tmpl | 1 + templates/user/include/header.tmpl | 23 ++++++++++++++++++++--- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/less/core.less b/less/core.less index 7e39f35..b1b8301 100644 --- a/less/core.less +++ b/less/core.less @@ -405,6 +405,31 @@ body { } } +nav#full-nav { + margin: 0; + + .left-side { + display: inline-block; + + a:first-child { + margin-left: 0; + } + } + + .right-side { + float: right; + } +} + +nav#full-nav a.simple-btn { + font-family: @sansFont; + border: 1px solid #ccc !important; + padding: .5rem 1rem; + margin: 0; + .rounded(.25em); + text-decoration: none; +} + .post-title { a { &:link { diff --git a/templates/base.tmpl b/templates/base.tmpl index 775dac9..c1f17ad 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -19,7 +19,7 @@ {{end}} diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 18942a9..364ba15 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -48,6 +48,7 @@ {{else}}
  • {{.SiteName}}
  • {{end}} + {{if .SimpleNav}}
  • New Post
  • {{end}}
  • Customize
  • Stats

  • diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl index 312d0b8..93020e2 100644 --- a/templates/user/include/header.tmpl +++ b/templates/user/include/header.tmpl @@ -38,7 +38,13 @@ {{else}} -

    {{.SiteName}}

    + {{ if .SimpleNav }} + {{end}} {{end}}
    From 1d25784d20537923889eb8a36d7efc2cfbdace32 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 5 Aug 2019 09:54:05 -0400 Subject: [PATCH 03/90] Add `bare` editor option This adds a new editor template that strips away most of the customization features in the default editor and includes only: - publishing - editing - viewing word count It also restricts publishing to a user's first collection, so it's optimized for instances that only allow users to have a single collection and don't use Drafts. Ref T680 T677 --- less/core.less | 2 +- less/pad-theme.less | 4 +- less/pad.less | 7 ++ templates/bare.tmpl | 235 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 templates/bare.tmpl diff --git a/less/core.less b/less/core.less index b1b8301..f4332a9 100644 --- a/less/core.less +++ b/less/core.less @@ -421,7 +421,7 @@ nav#full-nav { } } -nav#full-nav a.simple-btn { +nav#full-nav a.simple-btn, .tool button { font-family: @sansFont; border: 1px solid #ccc !important; padding: .5rem 1rem; diff --git a/less/pad-theme.less b/less/pad-theme.less index af1f95c..a8f668e 100644 --- a/less/pad-theme.less +++ b/less/pad-theme.less @@ -63,7 +63,7 @@ body#pad, body#pad-sub { } } #belt { - a { + a, button { color: #000; } } @@ -100,7 +100,7 @@ body#pad, body#pad-sub { } } #belt { - a { + a, button { color: white; } } diff --git a/less/pad.less b/less/pad.less index d37c6bc..a132b30 100644 --- a/less/pad.less +++ b/less/pad.less @@ -222,6 +222,13 @@ body#pad, body#pad-sub { font-style: italic; } } + button { + font-family: @sansFont; + background-color: transparent; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + border: 0; + } } } } diff --git a/templates/bare.tmpl b/templates/bare.tmpl new file mode 100644 index 0000000..1398a47 --- /dev/null +++ b/templates/bare.tmpl @@ -0,0 +1,235 @@ +{{define "pad"}} + + + + {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} + + + + + + + + +
    + + + +
    +
    + {{if not .SingleUser}}

    {{.SiteName}}

    {{end}} + + +
    + +
    + {{if .Editing}}{{end}} +
    +
    +
    + + + + +{{end}} From 17f7bc1becd321810694105535b5bc9ce892ddac Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 6 Aug 2019 09:15:05 -0400 Subject: [PATCH 04/90] Move user navigation to its own template section Ref T681 --- templates/user/include/header.tmpl | 37 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl index 93020e2..5f29646 100644 --- a/templates/user/include/header.tmpl +++ b/templates/user/include/header.tmpl @@ -1,20 +1,4 @@ -{{define "header"}} - - - - - {{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}} - - - - - - - - - - - +{{define "user-navigation"}} {{if .SingleUser}} {{else}} - {{ if .SimpleNav }} - {{if .SimpleNav}}{{if .Username}}
    + {{if .Chorus}}{{if .Username}}{{end}} From 130116092113e17f76fdec327c2e7f3fa3bface0 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 9 Aug 2019 11:15:36 -0700 Subject: [PATCH 18/90] fix tar bombs this changes the release targets in the Makefile to use a subdirectory of the format BINARYNAME_GITREV so extracting the archive results in a single directory. --- Makefile | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index c88ac79..66fb4c5 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ GOBUILD=$(GOCMD) build $(LDFLAGS) GOTEST=$(GOCMD) test $(LDFLAGS) GOGET=$(GOCMD) get BINARY_NAME=writefreely +TAR_NAME=$(BINARY_NAME)_$(GITREV) DOCKERCMD=docker IMAGE_NAME=writeas/writefreely TMPBIN=./tmp @@ -69,39 +70,40 @@ install : build cd less/; $(MAKE) install $(MFLAGS) release : clean ui assets - mkdir build - cp -r templates build - cp -r pages build - cp -r static build - mkdir build/keys + mkdir -p build/$(TAR_NAME) + cp -r templates build/$(TAR_NAME) + cp -r pages build/$(TAR_NAME) + cp -r static build/$(TAR_NAME) + mkdir build/$(TAR_NAME)/keys $(MAKE) build-linux - mv build/$(BINARY_NAME)-linux-amd64 build/$(BINARY_NAME) - cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz * - rm build/$(BINARY_NAME) + mv build/$(BINARY_NAME)-linux-amd64 build/$(TAR_NAME)/$(BINARY_NAME) + tar -cvzf $(TAR_NAME)_linux_amd64.tar.gz -C build $(TAR_NAME) + rm build/$(TAR_NAME)/$(BINARY_NAME) $(MAKE) build-arm7 - mv build/$(BINARY_NAME)-linux-arm-7 build/$(BINARY_NAME) - cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_arm7.tar.gz * - rm build/$(BINARY_NAME) + mv build/$(BINARY_NAME)-linux-arm-7 build/$(TAR_NAME)/$(BINARY_NAME) + tar -cvzf $(TAR_NAME)_linux_arm7.tar.gz -C build $(TAR_NAME) + rm build/$(TAR_NAME)/$(BINARY_NAME) $(MAKE) build-darwin - mv build/$(BINARY_NAME)-darwin-10.6-amd64 build/$(BINARY_NAME) - cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_macos_amd64.tar.gz * - rm build/$(BINARY_NAME) + mv build/$(BINARY_NAME)-darwin-10.6-amd64 build/$(TAR_NAME)/$(BINARY_NAME) + tar -cvzf $(TAR_NAME)_macos_amd64.tar.gz -C build $(TAR_NAME) + rm build/$(TAR_NAME)/$(BINARY_NAME) $(MAKE) build-windows - mv build/$(BINARY_NAME)-windows-4.0-amd64.exe build/$(BINARY_NAME).exe - cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./* + mv build/$(BINARY_NAME)-windows-4.0-amd64.exe build/$(TAR_NAME)/$(BINARY_NAME).exe + cd build; zip -r ../$(TAR_NAME)_windows_amd64.zip ./$(TAR_NAME) + rm build/$(TAR_NAME)/$(BINARY_NAME) $(MAKE) build-docker $(MAKE) release-docker # This assumes you're on linux/amd64 release-linux : clean ui - mkdir build - cp -r templates build - cp -r pages build - cp -r static build - mkdir build/keys + mkdir -p build/$(TAR_NAME) + cp -r templates build/$(TAR_NAME) + cp -r pages build/$(TAR_NAME) + cp -r static build/$(TAR_NAME) + mkdir build/$(TAR_NAME)/keys $(MAKE) build-no-sqlite - mv cmd/writefreely/$(BINARY_NAME) build/$(BINARY_NAME) - cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz * + mv cmd/writefreely/$(BINARY_NAME) build/$(TAR_NAME)/$(BINARY_NAME) + tar -cvzf $(TAR_NAME)_linux_amd64.tar.gz -C build $(TAR_NAME) release-docker : $(DOCKERCMD) push $(IMAGE_NAME) From 3c104cb3aa3c1164e0337ea11cc7c41dbd5643e8 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 9 Aug 2019 11:31:42 -0700 Subject: [PATCH 19/90] check for lessc executable in any location previously the checks were explicit locations which does not work when using something like nvm to manage node packages and versions. this checks for the executable and sets the script variable LESSC to the full path of the one found. if none was found the make command will error. --- less/Makefile | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/less/Makefile b/less/Makefile index e81258a..5e51b4d 100644 --- a/less/Makefile +++ b/less/Makefile @@ -1,13 +1,8 @@ -ifeq ($(shell which lessc),/usr/bin/lessc) - LESSC=/usr/bin/lessc -else ifeq ($(shell which lessc),/usr/local/bin/lessc) - LESSC=/usr/local/bin/lessc -else ifeq ($(shell which lessc),/bin/lessc) - LESSC=/bin/lessc -else - LESSC=node_modules/.bin/lessc +LESSC := $(shell command -v lessc 2> /dev/null) + +ifndef LESSC + $(error "less is not installed, please run install-less.sh") endif -export LESSC CSSDIR=../static/css/ From d8405680b4e1720a020b86a94a4684700d0c2e9c Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 9 Aug 2019 14:57:09 -0400 Subject: [PATCH 20/90] Respect `private` setting with home page Reader Ref T681 --- app.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 8b630df..1e24c75 100644 --- a/app.go +++ b/app.go @@ -193,16 +193,20 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { return handleViewCollection(app, w, r) } - if app.cfg.App.Chorus { - // This instance is focused on reading, so show Reader on home route - return viewLocalTimeline(app, w, r) - } - // Multi-user instance forceLanding := r.FormValue("landing") == "1" if !forceLanding { // Show correct page based on user auth status and configured landing path u := getUserSession(app, r) + + if app.cfg.App.Chorus { + // This instance is focused on reading, so show Reader on home route if not + // private or a private-instance user is logged in. + if !app.cfg.App.Private || u != nil { + return viewLocalTimeline(app, w, r) + } + } + if u != nil { // User is logged in, so show the Pad return handleViewPad(app, w, r) From 047ad0323b12c702cef5ebcfde3af88fa0c3d610 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 9 Aug 2019 14:58:17 -0400 Subject: [PATCH 21/90] Don't show user pages in nav when unauth'd Ref T681 T680 --- templates/base.tmpl | 2 ++ templates/user/include/header.tmpl | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/base.tmpl b/templates/base.tmpl index 678ada8..f8b66c8 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -41,9 +41,11 @@ {{ end }} About {{ if not .SingleUser }} + {{ if .Username }} {{if gt .MaxBlogs 1}}Blogs{{end}} {{if and (and .Chorus (eq .MaxBlogs 1)) .Username}}My Posts{{end}} {{if not .DisableDrafts}}Drafts{{end}} + {{ end }} {{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}Reader{{end}} {{if and (and .Chorus .OpenRegistration) (not .Username)}}Sign up{{end}} {{if not .Username}}Log in{{else if .SimpleNav}}Log out{{end}} diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl index 5923c83..9d5bb54 100644 --- a/templates/user/include/header.tmpl +++ b/templates/user/include/header.tmpl @@ -50,9 +50,11 @@ {{ end }} About {{ if not .SingleUser }} + {{ if .Username }} {{if gt .MaxBlogs 1}}Blogs{{end}} - {{if and (and .Chorus (eq .MaxBlogs 1)) .Username}}My Posts{{end}} + {{if and .Chorus (eq .MaxBlogs 1)}}My Posts{{end}} {{if not .DisableDrafts}}Drafts{{end}} + {{ end }} {{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}Reader{{end}} {{if and (and .Chorus .OpenRegistration) (not .Username)}}Sign up{{end}} {{if .Username}}Log out{{else}}Log in{{end}} From 95a98234eb9dc3fe893515673543b7b759fb558f Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 9 Aug 2019 14:04:15 -0700 Subject: [PATCH 22/90] 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 7a53af355e57f0730891ac3782e17e35629f95b9 Mon Sep 17 00:00:00 2001 From: Daniel Watkins Date: Sat, 10 Aug 2019 12:02:38 -0400 Subject: [PATCH 23/90] Emit the server software and version to the log on startup --- app.go | 7 ++++++- cmd/writefreely/main.go | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index dec0ef2..c52f59d 100644 --- a/app.go +++ b/app.go @@ -486,9 +486,14 @@ func ConnectToDatabase(app *App) error { return nil } +// FormatVersion constructs the version string for the application +func FormatVersion() string { + return serverSoftware + " " + softwareVer +} + // OutputVersion prints out the version of the application. func OutputVersion() { - fmt.Println(serverSoftware + " " + softwareVer) + fmt.Println(FormatVersion()) } // NewApp creates a new app instance. diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index 10cd7d6..48993c7 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -113,6 +113,7 @@ func main() { // Initialize the application var err error + log.Info("Starting %s...", writefreely.FormatVersion()) app, err = writefreely.Initialize(app, *debugPtr) if err != nil { log.Error("%s", err) From b373aad29874384d6d4d58f14019387d8ebc7758 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 12 Aug 2019 09:58:30 -0700 Subject: [PATCH 24/90] prevent future posts from showing in pins this changes GetPinnedPosts to accept an includeFutre bool, which returns future dated pinned posts when true. --- collections.go | 14 ++++++++++++-- database.go | 12 +++++++++--- go.mod | 2 +- posts.go | 6 +++++- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/collections.go b/collections.go index aee74f7..7eb3741 100644 --- a/collections.go +++ b/collections.go @@ -769,6 +769,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro displayPage.Collections = pubColls } } + isOwner := owner != nil if owner == nil { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) @@ -782,7 +783,11 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro // Add more data // TODO: fix this mess of collections inside collections - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) + if isOwner { + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, true) + } else { + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, false) + } err = templates["collection"].ExecuteTemplate(w, "collection", displayPage) if err != nil { @@ -866,6 +871,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e displayPage.Collections = pubColls } } + isOwner := owner != nil if owner == nil { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) @@ -878,7 +884,11 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e coll.Owner = displayPage.Owner // Add more data // TODO: fix this mess of collections inside collections - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) + if isOwner { + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, true) + } else { + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, false) + } err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage) if err != nil { diff --git a/database.go b/database.go index 34c5234..d3ff338 100644 --- a/database.go +++ b/database.go @@ -94,7 +94,7 @@ type writestore interface { UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error GetLastPinnedPostPos(collID int64) int64 - GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) + GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) RemoveCollectionRedirect(t *sql.Tx, alias string) error GetCollectionRedirect(alias string) (new string) IsCollectionAttributeOn(id int64, attr string) bool @@ -1533,9 +1533,15 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 { return lastPos.Int64 } -func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) { +func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) { // FIXME: sqlite-backed instances don't include ellipsis on truncated titles - rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID) + rows := &sql.Rows{} + var err error + if includeFuture { + rows, err = db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID) + } else { + rows, err = db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL AND created <= "+db.now()+" ORDER BY pinned_position ASC", coll.ID) + } if err != nil { log.Error("Failed selecting pinned posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."} diff --git a/go.mod b/go.mod index cc5fc57..5e040ba 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,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/posts.go b/posts.go index 2f3606f..edef6fb 100644 --- a/posts.go +++ b/posts.go @@ -1380,7 +1380,11 @@ Are you sure it was ever here?`, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, } - tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll) + if p.IsOwner { + tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, true) + } else { + tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, false) + } tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) if !postFound { From ca957c4b6d59a1d32eae911ba30571d064d53b61 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 12 Aug 2019 12:35:17 -0700 Subject: [PATCH 25/90] fix missing collection hostname GetCollections and GetPublishableCollections now take a hostname parameter to allow setting the collecion hostname. All collections used in memory now have their hostname set. --- account.go | 21 +++++++++++---------- admin.go | 11 ++++++----- collections.go | 6 ++++-- database.go | 11 ++++++----- export.go | 2 +- go.mod | 2 +- pad.go | 7 ++++--- postrender.go | 12 +++++++----- 8 files changed, 40 insertions(+), 32 deletions(-) diff --git a/account.go b/account.go index 1cf259b..c69e2fe 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 ( @@ -546,7 +547,7 @@ func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser if err != nil { log.Error("Login: Unable to get user posts: %v", err) } - colls, err := app.db.GetCollections(u) + colls, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("Login: Unable to get user collections: %v", err) } @@ -716,7 +717,7 @@ func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Requ return ErrBadRequestedType } - p, err := app.db.GetCollections(u) + p, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { return err } @@ -739,7 +740,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err log.Error("unable to fetch flashes: %v", err) } - c, err := app.db.GetPublishableCollections(u) + c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } @@ -762,7 +763,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err } func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error { - c, err := app.db.GetCollections(u) + c, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) return fmt.Errorf("No collections") diff --git a/admin.go b/admin.go index fe19ad5..a27a068 100644 --- a/admin.go +++ b/admin.go @@ -13,16 +13,17 @@ package writefreely import ( "database/sql" "fmt" + "net/http" + "runtime" + "strconv" + "time" + "github.com/gogits/gogs/pkg/tool" "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" - "net/http" - "runtime" - "strconv" - "time" ) var ( @@ -195,7 +196,7 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque p.LastPost = lp.Format("January 2, 2006, 3:04 PM") } - colls, err := app.db.GetCollections(p.User) + colls, err := app.db.GetCollections(p.User, app.cfg.App.Host) if err != nil { return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)} } diff --git a/collections.go b/collections.go index aee74f7..997d4d7 100644 --- a/collections.go +++ b/collections.go @@ -724,6 +724,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return err } + c.hostName = app.cfg.App.Host + // Serve ActivityStreams data now, if requested if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { ac := c.PersonObject() @@ -762,7 +764,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro owner = u displayPage.CanPin = true - pubColls, err := app.db.GetPublishableCollections(owner) + pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } @@ -859,7 +861,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e owner = u displayPage.CanPin = true - pubColls, err := app.db.GetPublishableCollections(owner) + pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } diff --git a/database.go b/database.go index 34c5234..c980225 100644 --- a/database.go +++ b/database.go @@ -65,8 +65,8 @@ type writestore interface { ChangeSettings(app *App, u *User, s *userSettings) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error - GetCollections(u *User) (*[]Collection, error) - GetPublishableCollections(u *User) (*[]Collection, error) + GetCollections(u *User, hostName string) (*[]Collection, error) + GetPublishableCollections(u *User, hostName string) (*[]Collection, error) GetMeStats(u *User) userMeStats GetTotalCollections() (int64, error) GetTotalPosts() (int64, error) @@ -1559,7 +1559,7 @@ func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) return &posts, nil } -func (db *datastore) GetCollections(u *User) (*[]Collection, error) { +func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, error) { rows, err := db.Query("SELECT id, alias, title, description, privacy, view_count FROM collections WHERE owner_id = ? ORDER BY id ASC", u.ID) if err != nil { log.Error("Failed selecting from collections: %v", err) @@ -1575,6 +1575,7 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) { log.Error("Failed scanning row: %v", err) break } + c.hostName = hostName c.URL = c.CanonicalURL() c.Public = c.IsPublic() @@ -1588,8 +1589,8 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) { return &colls, nil } -func (db *datastore) GetPublishableCollections(u *User) (*[]Collection, error) { - c, err := db.GetCollections(u) +func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Collection, error) { + c, err := db.GetCollections(u, hostName) if err != nil { return nil, err } diff --git a/export.go b/export.go index 47a2603..86855e2 100644 --- a/export.go +++ b/export.go @@ -104,7 +104,7 @@ func compileFullExport(app *App, u *User) *ExportUser { User: u, } - colls, err := app.db.GetCollections(u) + colls, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } diff --git a/go.mod b/go.mod index cc5fc57..5e040ba 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,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/pad.go b/pad.go index 1545b4f..b806615 100644 --- a/pad.go +++ b/pad.go @@ -11,12 +11,13 @@ package writefreely import ( + "net/http" + "strings" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" - "net/http" - "strings" ) func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { @@ -47,7 +48,7 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { } var err error if appData.User != nil { - appData.Blogs, err = app.db.GetPublishableCollections(appData.User) + appData.Blogs, err = app.db.GetPublishableCollections(appData.User, app.cfg.App.Host) if err != nil { log.Error("Unable to get user's blogs for Pad: %v", err) } diff --git a/postrender.go b/postrender.go index af715be..93c2089 100644 --- a/postrender.go +++ b/postrender.go @@ -12,17 +12,18 @@ package writefreely import ( "fmt" - "github.com/microcosm-cc/bluemonday" - stripmd "github.com/writeas/go-strip-markdown" - "github.com/writeas/saturday" - "github.com/writeas/web-core/stringmanip" - "github.com/writeas/writefreely/parse" "html" "html/template" "regexp" "strings" "unicode" "unicode/utf8" + + "github.com/microcosm-cc/bluemonday" + stripmd "github.com/writeas/go-strip-markdown" + blackfriday "github.com/writeas/saturday" + "github.com/writeas/web-core/stringmanip" + "github.com/writeas/writefreely/parse" ) var ( @@ -36,6 +37,7 @@ var ( func (p *Post) formatContent(c *Collection, isOwner bool) { baseURL := c.CanonicalURL() + // TODO: redundant if !isSingleUser { baseURL = "/" + c.Alias + "/" } From 1d80e47e074a32d480e4fe65f59f08d27b437f0d Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 12 Aug 2019 13:51:29 -0700 Subject: [PATCH 26/90] change subdirectory to writefreely instead of writefreely_versionstring --- Makefile | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index 66fb4c5..c85cf6f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ GOBUILD=$(GOCMD) build $(LDFLAGS) GOTEST=$(GOCMD) test $(LDFLAGS) GOGET=$(GOCMD) get BINARY_NAME=writefreely -TAR_NAME=$(BINARY_NAME)_$(GITREV) +BUILDPATH=build/$(BINARY_NAME) DOCKERCMD=docker IMAGE_NAME=writeas/writefreely TMPBIN=./tmp @@ -70,40 +70,40 @@ install : build cd less/; $(MAKE) install $(MFLAGS) release : clean ui assets - mkdir -p build/$(TAR_NAME) - cp -r templates build/$(TAR_NAME) - cp -r pages build/$(TAR_NAME) - cp -r static build/$(TAR_NAME) - mkdir build/$(TAR_NAME)/keys + mkdir -p $(BUILDPATH) + cp -r templates $(BUILDPATH) + cp -r pages $(BUILDPATH) + cp -r static $(BUILDPATH) + mkdir $(BUILDPATH)/keys $(MAKE) build-linux - mv build/$(BINARY_NAME)-linux-amd64 build/$(TAR_NAME)/$(BINARY_NAME) - tar -cvzf $(TAR_NAME)_linux_amd64.tar.gz -C build $(TAR_NAME) - rm build/$(TAR_NAME)/$(BINARY_NAME) + mv build/$(BINARY_NAME)-linux-amd64 $(BUILDPATH)/$(BINARY_NAME) + tar -cvzf $(BINARY_NAME)_$(GIT_REV)_linux_amd64.tar.gz -C build $(BINARY_NAME) + rm $(BUILDPATH)/$(BINARY_NAME) $(MAKE) build-arm7 - mv build/$(BINARY_NAME)-linux-arm-7 build/$(TAR_NAME)/$(BINARY_NAME) - tar -cvzf $(TAR_NAME)_linux_arm7.tar.gz -C build $(TAR_NAME) - rm build/$(TAR_NAME)/$(BINARY_NAME) + mv build/$(BINARY_NAME)-linux-arm-7 $(BUILDPATH)/$(BINARY_NAME) + tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_arm7.tar.gz -C build $(BINARY_NAME) + rm $(BUILDPATH)/$(BINARY_NAME) $(MAKE) build-darwin - mv build/$(BINARY_NAME)-darwin-10.6-amd64 build/$(TAR_NAME)/$(BINARY_NAME) - tar -cvzf $(TAR_NAME)_macos_amd64.tar.gz -C build $(TAR_NAME) - rm build/$(TAR_NAME)/$(BINARY_NAME) + mv build/$(BINARY_NAME)-darwin-10.6-amd64 $(BUILDPATH)/$(BINARY_NAME) + tar -cvzf $(BINARY_NAME)_$(GITREV)_macos_amd64.tar.gz -C build $(BINARY_NAME) + rm $(BUILDPATH)/$(BINARY_NAME) $(MAKE) build-windows - mv build/$(BINARY_NAME)-windows-4.0-amd64.exe build/$(TAR_NAME)/$(BINARY_NAME).exe - cd build; zip -r ../$(TAR_NAME)_windows_amd64.zip ./$(TAR_NAME) - rm build/$(TAR_NAME)/$(BINARY_NAME) + mv build/$(BINARY_NAME)-windows-4.0-amd64.exe $(BUILDPATH)/$(BINARY_NAME).exe + cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./$(BINARY_NAME) + rm $(BUILDPATH)/$(BINARY_NAME) $(MAKE) build-docker $(MAKE) release-docker # This assumes you're on linux/amd64 -release-linux : clean ui - mkdir -p build/$(TAR_NAME) - cp -r templates build/$(TAR_NAME) - cp -r pages build/$(TAR_NAME) - cp -r static build/$(TAR_NAME) - mkdir build/$(TAR_NAME)/keys +release-linux : clean + mkdir -p $(BUILDPATH) + cp -r templates $(BUILDPATH) + cp -r pages $(BUILDPATH) + cp -r static $(BUILDPATH) + mkdir $(BUILDPATH)/keys $(MAKE) build-no-sqlite - mv cmd/writefreely/$(BINARY_NAME) build/$(TAR_NAME)/$(BINARY_NAME) - tar -cvzf $(TAR_NAME)_linux_amd64.tar.gz -C build $(TAR_NAME) + mv cmd/writefreely/$(BINARY_NAME) $(BUILDPATH)/$(BINARY_NAME) + tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz -C build $(BINARY_NAME) release-docker : $(DOCKERCMD) push $(IMAGE_NAME) From f241d694258fd1c501249a09a00930b86be35d51 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 12 Aug 2019 14:12:35 -0700 Subject: [PATCH 27/90] reduce GetPinnedPosts calls to single line --- collections.go | 12 ++---------- posts.go | 6 +----- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/collections.go b/collections.go index 7eb3741..adf89d4 100644 --- a/collections.go +++ b/collections.go @@ -783,11 +783,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro // Add more data // TODO: fix this mess of collections inside collections - if isOwner { - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, true) - } else { - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, false) - } + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) err = templates["collection"].ExecuteTemplate(w, "collection", displayPage) if err != nil { @@ -884,11 +880,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e coll.Owner = displayPage.Owner // Add more data // TODO: fix this mess of collections inside collections - if isOwner { - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, true) - } else { - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, false) - } + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage) if err != nil { diff --git a/posts.go b/posts.go index edef6fb..a1383fa 100644 --- a/posts.go +++ b/posts.go @@ -1380,11 +1380,7 @@ Are you sure it was ever here?`, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, } - if p.IsOwner { - tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, true) - } else { - tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, false) - } + tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner) tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) if !postFound { From 55dc1917fe477e263caa616594b8a886214957e0 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 12 Aug 2019 14:13:02 -0700 Subject: [PATCH 28/90] use established future posts pattern --- database.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/database.go b/database.go index d3ff338..1eea6bb 100644 --- a/database.go +++ b/database.go @@ -1535,13 +1535,11 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 { func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) { // FIXME: sqlite-backed instances don't include ellipsis on truncated titles - rows := &sql.Rows{} - var err error - if includeFuture { - rows, err = db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID) - } else { - rows, err = db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL AND created <= "+db.now()+" ORDER BY pinned_position ASC", coll.ID) + timeCondition := "" + if !includeFuture { + timeCondition = "AND created <= " + db.now() } + rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL "+timeCondition+" ORDER BY pinned_position ASC", coll.ID) if err != nil { log.Error("Failed selecting pinned posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."} From 8a29a4dfc94b91a753391c4752b1ea4116d97ef6 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 14 Aug 2019 23:14:34 -0400 Subject: [PATCH 29/90] Link to home page in bare editor in chorus mode Ref T681 --- templates/bare.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/bare.tmpl b/templates/bare.tmpl index 1398a47..a4194c9 100644 --- a/templates/bare.tmpl +++ b/templates/bare.tmpl @@ -19,7 +19,7 @@
    - {{if not .SingleUser}}

    {{.SiteName}}

    {{end}} + {{if not .SingleUser}}

    {{if .Chorus}}{{else}}{{end}}{{.SiteName}}

    {{end}} From 55808233fd322373452f0c41240b542470251e6c Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 14 Aug 2019 23:25:02 -0400 Subject: [PATCH 30/90] Fix logic for showing sign up link This prevents the link from showing when an instance lands on the sign up page anyway. Ref T681 --- templates/base.tmpl | 2 +- templates/user/include/header.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/base.tmpl b/templates/base.tmpl index f8b66c8..aae7850 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -47,7 +47,7 @@ {{if not .DisableDrafts}}Drafts{{end}} {{ end }} {{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}Reader{{end}} - {{if and (and .Chorus .OpenRegistration) (not .Username)}}Sign up{{end}} + {{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}Sign up{{end}} {{if not .Username}}Log in{{else if .SimpleNav}}Log out{{end}} {{ end }} diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl index 9d5bb54..0feca89 100644 --- a/templates/user/include/header.tmpl +++ b/templates/user/include/header.tmpl @@ -56,7 +56,7 @@ {{if not .DisableDrafts}}Drafts{{end}} {{ end }} {{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}Reader{{end}} - {{if and (and .Chorus .OpenRegistration) (not .Username)}}Sign up{{end}} + {{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}Sign up{{end}} {{if .Username}}Log out{{else}}Log in{{end}} {{ end }} {{else}} From 42a22193355b6cbcf9e7d31bc93f52f63916721c Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 22 Aug 2019 10:48:58 -0700 Subject: [PATCH 31/90] add ui back to target release linux --- Makefile | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c85cf6f..f1a99b8 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ release : clean ui assets $(MAKE) release-docker # This assumes you're on linux/amd64 -release-linux : clean +release-linux : clean ui mkdir -p $(BUILDPATH) cp -r templates $(BUILDPATH) cp -r pages $(BUILDPATH) diff --git a/go.mod b/go.mod index cc5fc57..5e040ba 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,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 From 6e9000659c4a93e40034773f8b2b4f02c3962359 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 22 Aug 2019 12:16:37 -0700 Subject: [PATCH 32/90] fix typo in Makefile GITREV release target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f1a99b8..757bcfd 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ release : clean ui assets mkdir $(BUILDPATH)/keys $(MAKE) build-linux mv build/$(BINARY_NAME)-linux-amd64 $(BUILDPATH)/$(BINARY_NAME) - tar -cvzf $(BINARY_NAME)_$(GIT_REV)_linux_amd64.tar.gz -C build $(BINARY_NAME) + tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz -C build $(BINARY_NAME) rm $(BUILDPATH)/$(BINARY_NAME) $(MAKE) build-arm7 mv build/$(BINARY_NAME)-linux-arm-7 $(BUILDPATH)/$(BINARY_NAME) From 77f7b4a522cf2dd522a64a86b32dee245f1cfd6f Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 28 Aug 2019 12:37:45 -0700 Subject: [PATCH 33/90] Add account suspension features This renders all requests for that user's posts, collections and related ActivityPub endpoints with 404 responses. While suspended, users may not create or edit posts or collections. User status is listed in the admin user page Admin view of user details shows status and now has a button to activate or suspend a user. --- account.go | 29 +++++++----- activitypub.go | 40 ++++++++++++++++ admin.go | 34 ++++++++++++-- collections.go | 34 +++++++++++++- database.go | 51 ++++++++++++++++---- errors.go | 5 +- feed.go | 14 +++++- invites.go | 13 +++-- migrations/migrations.go | 2 + migrations/v3.go | 29 ++++++++++++ pad.go | 22 +++++++-- posts.go | 73 +++++++++++++++++++++++++++-- read.go | 14 +++--- routes.go | 8 ++-- schema.sql | 1 + sqlite.sql | 3 +- templates/edit-meta.tmpl | 4 ++ templates/pad.tmpl | 6 ++- templates/user/admin/users.tmpl | 5 ++ templates/user/admin/view-user.tmpl | 32 +++++++++++++ templates/user/settings.tmpl | 6 +++ users.go | 1 + webfinger.go | 11 ++++- 23 files changed, 381 insertions(+), 56 deletions(-) create mode 100644 migrations/v3.go diff --git a/account.go b/account.go index 1cf259b..49700b4 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 ( @@ -1011,14 +1012,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err obj := struct { *UserPage - Email string - HasPass bool - IsLogOut bool + Email string + HasPass bool + IsLogOut bool + Suspended bool }{ - UserPage: NewUserPage(app, r, u, "Account Settings", flashes), - Email: fullUser.EmailClear(app.keys), - HasPass: passIsSet, - IsLogOut: r.FormValue("logout") == "1", + UserPage: NewUserPage(app, r, u, "Account Settings", flashes), + Email: fullUser.EmailClear(app.keys), + HasPass: passIsSet, + IsLogOut: r.FormValue("logout") == "1", + Suspended: fullUser.Suspended, } showUserPage(w, "settings", obj) diff --git a/activitypub.go b/activitypub.go index 997609d..c10838d 100644 --- a/activitypub.go +++ b/activitypub.go @@ -80,6 +80,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host p := c.PersonObject() @@ -105,6 +113,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host if app.cfg.App.SingleUser { @@ -158,6 +174,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host accountRoot := c.FederatedAccount() @@ -204,6 +228,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host accountRoot := c.FederatedAccount() @@ -238,6 +270,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // TODO: return Reject? return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host if debugging { diff --git a/admin.go b/admin.go index fe19ad5..dc8580d 100644 --- a/admin.go +++ b/admin.go @@ -13,16 +13,17 @@ package writefreely import ( "database/sql" "fmt" + "net/http" + "runtime" + "strconv" + "time" + "github.com/gogits/gogs/pkg/tool" "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" - "net/http" - "runtime" - "strconv" - "time" ) var ( @@ -229,6 +230,31 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque return nil } +func handleAdminToggleUserSuspended(app *App, u *User, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + username := vars["username"] + if username == "" { + return impart.HTTPError{http.StatusFound, "/admin/users"} + } + + userToToggle, err := app.db.GetUserForAuth(username) + if err != nil { + log.Error("failed to get user: %v", err) + return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)} + } + if userToToggle.Suspended { + err = app.db.SetUserSuspended(userToToggle.ID, false) + } else { + err = app.db.SetUserSuspended(userToToggle.ID, true) + } + if err != nil { + log.Error("toggle user suspended: %v", err) + return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user suspended: %v")} + } + // TODO: invalidate sessions + return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)} +} + func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error { p := struct { *UserPage diff --git a/collections.go b/collections.go index aee74f7..95dd808 100644 --- a/collections.go +++ b/collections.go @@ -379,6 +379,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { } var userID int64 + var err error if reqJSON && !c.Web { accessToken = r.Header.Get("Authorization") if accessToken == "" { @@ -395,6 +396,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { } userID = u.ID } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("new collection: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } if !author.IsValidUsername(app.cfg, c.Alias) { return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."} @@ -724,6 +733,15 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("view collection: get owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrCollectionNotFound + } // Serve ActivityStreams data now, if requested if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { ac := c.PersonObject() @@ -824,6 +842,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e return err } + if u.Suspended { + return ErrCollectionNotFound + } + page := getCollectionPage(vars) c, err := processCollectionPermissions(app, cr, u, w, r) @@ -916,7 +938,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error if reqJSON && !isWeb { // Ensure an access token was given accessToken := r.Header.Get("Authorization") - u = &User{} u.ID = app.db.GetUserID(accessToken) if u.ID == -1 { return ErrBadAccessToken @@ -928,6 +949,16 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error } } + suspended, err := app.db.IsUserSuspended(u.ID) + if err != nil { + log.Error("existing collection: get user suspended status: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrUserSuspended + } + if r.Method == "DELETE" { err := app.db.DeleteCollection(collAlias, u.ID) if err != nil { @@ -940,7 +971,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error } c := SubmittedCollection{OwnerID: uint64(u.ID)} - var err error if reqJSON { // Decode JSON request diff --git a/database.go b/database.go index 34c5234..150a74f 100644 --- a/database.go +++ b/database.go @@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u func (db *datastore) GetUserByID(id int64) (*User, error) { u := &User{ID: id} - err := db.QueryRow("SELECT username, password, email, created FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created) + err := db.QueryRow("SELECT username, password, email, created, suspended FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -308,6 +308,23 @@ func (db *datastore) GetUserByID(id int64) (*User, error) { return u, nil } +// IsUserSuspended returns true if the user account associated with id is +// currently suspended. +func (db *datastore) IsUserSuspended(id int64) (bool, error) { + u := &User{ID: id} + + err := db.QueryRow("SELECT suspended FROM users WHERE id = ?", id).Scan(&u.Suspended) + switch { + case err == sql.ErrNoRows: + return false, ErrUserNotFound + case err != nil: + log.Error("Couldn't SELECT user password: %v", err) + return false, err + } + + return u.Suspended, nil +} + // DoesUserNeedAuth returns true if the user hasn't provided any methods for // authenticating with the account, such a passphrase or email address. // Any errors are reported to admin and silently quashed, returning false as the @@ -347,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) { func (db *datastore) GetUserForAuth(username string) (*User, error) { u := &User{Username: username} - err := db.QueryRow("SELECT id, password, email, created FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created) + err := db.QueryRow("SELECT id, password, email, created, suspended FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) switch { case err == sql.ErrNoRows: // Check if they've entered the wrong, unnormalized username @@ -370,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) { func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) { u := &User{ID: userID} - err := db.QueryRow("SELECT id, password, email, created FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created) + err := db.QueryRow("SELECT id, password, email, created, suspended FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -1624,7 +1641,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats { } func (db *datastore) GetTotalCollections() (collCount int64, err error) { - err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) + err = db.QueryRow(` + SELECT COUNT(*) + FROM collections c + LEFT JOIN users u ON u.id = c.owner_id + WHERE u.suspended = 0`).Scan(&collCount) if err != nil { log.Error("Unable to fetch collections count: %v", err) } @@ -1632,7 +1653,11 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) { } func (db *datastore) GetTotalPosts() (postCount int64, err error) { - err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) + err = db.QueryRow(` + SELECT COUNT(*) + FROM posts p + LEFT JOIN users u ON u.id = p.owner_id + WHERE u.Suspended = 0`).Scan(&postCount) if err != nil { log.Error("Unable to fetch posts count: %v", err) } @@ -2341,17 +2366,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) { limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage) } - rows, err := db.Query("SELECT id, username, created FROM users ORDER BY created DESC LIMIT " + limitStr) + rows, err := db.Query("SELECT id, username, created, suspended FROM users ORDER BY created DESC LIMIT " + limitStr) if err != nil { - log.Error("Failed selecting from posts: %v", err) - return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."} + log.Error("Failed selecting from users: %v", err) + return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."} } defer rows.Close() users := []User{} for rows.Next() { u := User{} - err = rows.Scan(&u.ID, &u.Username, &u.Created) + err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Suspended) if err != nil { log.Error("Failed scanning GetAllUsers() row: %v", err) break @@ -2388,6 +2413,14 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) { return &t, nil } +func (db *datastore) SetUserSuspended(id int64, suspend bool) error { + _, err := db.Exec("UPDATE users SET suspended = ? WHERE id = ?", suspend, id) + if err != nil { + return fmt.Errorf("failed to update user suspended status: %v", err) + } + return nil +} + func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) { var t time.Time err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) diff --git a/errors.go b/errors.go index 0092b7f..fa7304f 100644 --- a/errors.go +++ b/errors.go @@ -11,8 +11,9 @@ package writefreely import ( - "github.com/writeas/impart" "net/http" + + "github.com/writeas/impart" ) // Commonly returned HTTP errors @@ -46,6 +47,8 @@ var ( ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."} ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."} + + ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is suspended, contact the administrator."} ) // Post operation errors diff --git a/feed.go b/feed.go index dd82c33..353b1b9 100644 --- a/feed.go +++ b/feed.go @@ -12,12 +12,13 @@ package writefreely import ( "fmt" + "net/http" + "time" + . "github.com/gorilla/feeds" "github.com/gorilla/mux" stripmd "github.com/writeas/go-strip-markdown" "github.com/writeas/web-core/log" - "net/http" - "time" ) func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { @@ -34,6 +35,15 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { if err != nil { return nil } + + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("view feed: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host if c.IsPrivate() || c.IsProtected() { diff --git a/invites.go b/invites.go index 561255f..93b82b4 100644 --- a/invites.go +++ b/invites.go @@ -12,15 +12,16 @@ package writefreely import ( "database/sql" + "html/template" + "net/http" + "strconv" + "time" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/nerds/store" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" - "html/template" - "net/http" - "strconv" - "time" ) type Invite struct { @@ -77,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re muVal := r.FormValue("uses") expVal := r.FormValue("expires") + if u.Suspended { + return ErrUserSuspended + } + var err error var maxUses int if muVal != "0" { diff --git a/migrations/migrations.go b/migrations/migrations.go index 70e4b7b..de3f487 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -13,6 +13,7 @@ package migrations import ( "database/sql" + "github.com/writeas/web-core/log" ) @@ -57,6 +58,7 @@ func (m *migration) Migrate(db *datastore) error { var migrations = []Migration{ New("support user invites", supportUserInvites), // -> V1 (v0.8.0) New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0) + New("support users suspension", supportUserSuspension), // V2 -> V3 () } // CurrentVer returns the current migration version the application is on diff --git a/migrations/v3.go b/migrations/v3.go new file mode 100644 index 0000000..c7c00a9 --- /dev/null +++ b/migrations/v3.go @@ -0,0 +1,29 @@ +/* + * Copyright © 2019 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +package migrations + +func supportUserSuspension(db *datastore) error { + t, err := db.Begin() + + _, err = t.Exec(`ALTER TABLE users ADD COLUMN suspended ` + db.typeBool() + ` DEFAULT '0' NOT NULL`) + if err != nil { + t.Rollback() + return err + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} diff --git a/pad.go b/pad.go index 1545b4f..8a54b76 100644 --- a/pad.go +++ b/pad.go @@ -11,12 +11,13 @@ package writefreely import ( + "net/http" + "strings" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" - "net/http" - "strings" ) func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { @@ -34,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { } appData := &struct { page.StaticPage - Post *RawPost - User *User - Blogs *[]Collection + Post *RawPost + User *User + Blogs *[]Collection + Suspended bool Editing bool // True if we're modifying an existing post EditCollection *Collection // Collection of the post we're editing, if any @@ -51,6 +53,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { if err != nil { log.Error("Unable to get user's blogs for Pad: %v", err) } + appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID) + if err != nil { + log.Error("Unable to get users suspension status for Pad: %v", err) + } } padTmpl := app.cfg.App.Editor @@ -121,12 +127,18 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error { EditCollection *Collection // Collection of the post we're editing, if any Flashes []string NeedsToken bool + Suspended bool }{ StaticPage: pageForReq(app, r), Post: &RawPost{Font: "norm"}, User: getUserSession(app, r), } var err error + appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID) + if err != nil { + log.Error("view meta: get user suspended status: %v", err) + return ErrInternalGeneral + } if action == "" && slug == "" { return ErrPostNotFound diff --git a/posts.go b/posts.go index 2f3606f..2d64808 100644 --- a/posts.go +++ b/posts.go @@ -380,6 +380,16 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { } } + suspended, err := app.db.IsUserSuspended(ownerID.Int64) + if err != nil { + log.Error("view post: get collection owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrPostNotFound + } + // Check if post has been unpublished if content == "" { gone = true @@ -496,6 +506,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { } else { userID = app.db.GetUserID(accessToken) } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("new post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + if userID == -1 { return ErrNotLoggedIn } @@ -508,7 +527,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { var p *SubmittedPost if reqJSON { decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&p) + err = decoder.Decode(&p) if err != nil { log.Error("Couldn't parse new post JSON request: %v\n", err) return ErrBadJSON @@ -554,7 +573,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { var newPost *PublicPost = &PublicPost{} var coll *Collection - var err error if accessToken != "" { newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host) } else { @@ -662,6 +680,15 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { } } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("existing post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + // Modify post struct p.ID = postID @@ -856,11 +883,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error { ownerID = u.ID } + suspended, err := app.db.IsUserSuspended(ownerID) + if err != nil { + log.Error("add post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + // Parse claimed posts in format: // [{"id": "...", "token": "..."}] var claims *[]ClaimPostRequest decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&claims) + err = decoder.Decode(&claims) if err != nil { return ErrBadJSONArray } @@ -950,13 +986,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { userID = u.ID } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("pin post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + // Parse request var posts []struct { ID string `json:"id"` Position int64 `json:"position"` } decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&posts) + err = decoder.Decode(&posts) if err != nil { return ErrBadJSONArray } @@ -992,6 +1037,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { var collID int64 + var ownerID int64 var coll *Collection var err error vars := mux.Vars(r) @@ -1007,12 +1053,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { return err } collID = coll.ID + ownerID = coll.OwnerID } p, err := app.db.GetPost(vars["post"], collID) if err != nil { return err } + suspended, err := app.db.IsUserSuspended(ownerID) + if err != nil { + log.Error("fetch post: get owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrPostNotFound + } p.extractData() @@ -1270,6 +1326,15 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error } c.hostName = app.cfg.App.Host + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("view collection post: get owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrPostNotFound + } // Check collection permissions if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { return ErrPostNotFound diff --git a/read.go b/read.go index 3bc91c7..e7d1e55 100644 --- a/read.go +++ b/read.go @@ -13,6 +13,12 @@ package writefreely import ( "database/sql" "fmt" + "html/template" + "math" + "net/http" + "strconv" + "time" + . "github.com/gorilla/feeds" "github.com/gorilla/mux" stripmd "github.com/writeas/go-strip-markdown" @@ -20,11 +26,6 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/web-core/memo" "github.com/writeas/writefreely/page" - "html/template" - "math" - "net/http" - "strconv" - "time" ) const ( @@ -62,7 +63,8 @@ func (app *App) FetchPublicPosts() (interface{}, error) { rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated FROM collections c LEFT JOIN posts p ON p.collection_id = c.id - WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) + LEFT JOIN users u ON u.id = p.owner_id + WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.suspended = 0 ORDER BY p.created DESC`) if err != nil { log.Error("Failed selecting from posts: %v", err) diff --git a/routes.go b/routes.go index 724c532..e7014cd 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. @@ -143,6 +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(handleAdminToggleUserSuspended)).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/schema.sql b/schema.sql index b3fae97..3a79736 100644 --- a/schema.sql +++ b/schema.sql @@ -225,6 +225,7 @@ CREATE TABLE IF NOT EXISTS `users` ( `password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, `email` varbinary(255) DEFAULT NULL, `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `suspended` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; diff --git a/sqlite.sql b/sqlite.sql index 90989ed..920ffed 100644 --- a/sqlite.sql +++ b/sqlite.sql @@ -214,7 +214,8 @@ CREATE TABLE IF NOT EXISTS `users` ( username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, email TEXT DEFAULT NULL, - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + suspended INTEGER NOT NULL DEFAULT 0 ); -- -------------------------------------------------------- diff --git a/templates/edit-meta.tmpl b/templates/edit-meta.tmpl index 8d96b15..6707e68 100644 --- a/templates/edit-meta.tmpl +++ b/templates/edit-meta.tmpl @@ -269,6 +269,10 @@ {{template "footer" .}} {{end}} From f85f0751a3a7480bc29450cf42a4360c31ff3083 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 25 Oct 2019 12:04:24 -0700 Subject: [PATCH 61/90] address PR comments - update error messages to be correct - move suspended message into template and include for other pages - check suspended status on all relevant pages and show message if logged in user is suspended. - fix possible nil pointer error - remove changes to db schema files - add version comment to migration - add UserStatus type with UserActive and UserSuspended - change database table to use status column instead of suspended - update toggle suspended handler to be toggle status in prep for possible future inclusion of further user statuses --- account.go | 29 ++++++++++++++++++++++++++- activitypub.go | 10 ++++----- admin.go | 13 ++++++------ collections.go | 26 +++++++++++++----------- database.go | 29 ++++++++++++++------------- invites.go | 2 +- migrations/migrations.go | 2 +- migrations/v3.go | 4 ++-- posts.go | 22 +++++++++++--------- read.go | 2 +- routes.go | 2 +- schema.sql | 1 - sqlite.sql | 3 +-- templates.go | 12 +++++++---- templates/chorus-collection-post.tmpl | 3 +++ templates/chorus-collection.tmpl | 3 +++ templates/collection-post.tmpl | 3 +++ templates/collection-tags.tmpl | 3 +++ templates/collection.tmpl | 3 +++ templates/password-collection.tmpl | 3 +++ templates/post.tmpl | 3 +++ templates/user/admin/users.tmpl | 2 +- templates/user/admin/view-user.tmpl | 4 ++-- templates/user/articles.tmpl | 3 +++ templates/user/collection.tmpl | 3 +++ templates/user/collections.tmpl | 3 +++ templates/user/include/suspended.tmpl | 6 ++++++ templates/user/settings.tmpl | 9 +++------ templates/user/stats.tmpl | 3 +++ users.go | 9 ++++++++- 30 files changed, 148 insertions(+), 72 deletions(-) create mode 100644 templates/user/include/suspended.tmpl diff --git a/account.go b/account.go index c3d55ba..0faa7bb 100644 --- a/account.go +++ b/account.go @@ -750,14 +750,20 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err log.Error("unable to fetch collections: %v", err) } + suspended, err := app.db.IsUserSuspended(u.ID) + if err != nil { + log.Error("view articles: %v", err) + } d := struct { *UserPage AnonymousPosts *[]PublicPost Collections *[]Collection + Suspended bool }{ UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), AnonymousPosts: p, Collections: c, + Suspended: suspended, } d.UserPage.SetMessaging(u) w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") @@ -779,6 +785,11 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) uc, _ := app.db.GetUserCollectionCount(u.ID) // TODO: handle any errors + suspended, err := app.db.IsUserSuspended(u.ID) + if err != nil { + log.Error("view collections %v", err) + return fmt.Errorf("view collections: %v", err) + } d := struct { *UserPage Collections *[]Collection @@ -786,11 +797,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) UsedCollections, TotalCollections int NewBlogsDisabled bool + Suspended bool }{ UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), Collections: c, UsedCollections: int(uc), NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), + Suspended: suspended, } d.UserPage.SetMessaging(u) showUserPage(w, "collections", d) @@ -808,13 +821,20 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques return ErrCollectionNotFound } + suspended, err := app.db.IsUserSuspended(u.ID) + if err != nil { + log.Error("view edit collection %v", err) + return fmt.Errorf("view edit collection: %v", err) + } flashes, _ := getSessionFlashes(app, w, r, nil) obj := struct { *UserPage *Collection + Suspended bool }{ UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), Collection: c, + Suspended: suspended, } showUserPage(w, "collection", obj) @@ -976,17 +996,24 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error titleStats = c.DisplayTitle() + " " } + suspended, err := app.db.IsUserSuspended(u.ID) + if err != nil { + log.Error("view stats: %v", err) + return err + } obj := struct { *UserPage VisitsBlog string Collection *Collection TopPosts *[]PublicPost APFollowers int + Suspended bool }{ UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), VisitsBlog: alias, Collection: c, TopPosts: topPosts, + Suspended: suspended, } if app.cfg.App.Federation { folls, err := app.db.GetAPFollowers(c) @@ -1026,7 +1053,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err Email: fullUser.EmailClear(app.keys), HasPass: passIsSet, IsLogOut: r.FormValue("logout") == "1", - Suspended: fullUser.Suspended, + Suspended: fullUser.Status == UserSuspended, } showUserPage(w, "settings", obj) diff --git a/activitypub.go b/activitypub.go index 06b0468..eeaa1fa 100644 --- a/activitypub.go +++ b/activitypub.go @@ -82,7 +82,7 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection activities: %v", err) return ErrInternalGeneral } if suspended { @@ -115,7 +115,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection outbox: %v", err) return ErrInternalGeneral } if suspended { @@ -176,7 +176,7 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection followers: %v", err) return ErrInternalGeneral } if suspended { @@ -230,7 +230,7 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection following: %v", err) return ErrInternalGeneral } if suspended { @@ -272,7 +272,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection inbox: %v", err) return ErrInternalGeneral } if suspended { diff --git a/admin.go b/admin.go index 1d94987..65afd5f 100644 --- a/admin.go +++ b/admin.go @@ -230,28 +230,27 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque return nil } -func handleAdminToggleUserSuspended(app *App, u *User, w http.ResponseWriter, r *http.Request) error { +func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) username := vars["username"] if username == "" { return impart.HTTPError{http.StatusFound, "/admin/users"} } - userToToggle, err := app.db.GetUserForAuth(username) + user, err := app.db.GetUserForAuth(username) if err != nil { log.Error("failed to get user: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)} } - if userToToggle.Suspended { - err = app.db.SetUserSuspended(userToToggle.ID, false) + if user.Status == UserSuspended { + err = app.db.SetUserStatus(user.ID, UserActive) } else { - err = app.db.SetUserSuspended(userToToggle.ID, true) + err = app.db.SetUserStatus(user.ID, UserSuspended) } if err != nil { log.Error("toggle user suspended: %v", err) - return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user suspended: %v")} + return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v")} } - // TODO: invalidate sessions return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)} } diff --git a/collections.go b/collections.go index c4cefeb..38ec6f1 100644 --- a/collections.go +++ b/collections.go @@ -71,6 +71,7 @@ type ( CurrentPage int TotalPages int Format *CollectionFormat + Suspended bool } SubmittedCollection struct { // Data used for updating a given collection @@ -398,7 +399,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { } suspended, err := app.db.IsUserSuspended(userID) if err != nil { - log.Error("new collection: get user: %v", err) + log.Error("new collection: %v", err) return ErrInternalGeneral } if suspended { @@ -486,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error { res.Owner = u } } + // TODO: check suspended app.db.GetPostsCount(res, isCollOwner) // Strip non-public information res.Collection.ForPublic() @@ -738,14 +740,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("view collection: get owner: %v", err) + log.Error("view collection: %v", err) return ErrInternalGeneral } - if suspended { - return ErrCollectionNotFound - } - // Serve ActivityStreams data now, if requested if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { ac := c.PersonObject() @@ -802,6 +800,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro log.Error("Error getting user for collection: %v", err) } } + if !isOwner && suspended { + return ErrCollectionNotFound + } + displayPage.Suspended = isOwner && suspended displayPage.Owner = owner coll.Owner = displayPage.Owner @@ -853,10 +855,6 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e return err } - if u.Suspended { - return ErrCollectionNotFound - } - page := getCollectionPage(vars) c, err := processCollectionPermissions(app, cr, u, w, r) @@ -908,6 +906,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e log.Error("Error getting user for collection: %v", err) } } + if !isOwner && u.Status == UserSuspended { + return ErrCollectionNotFound + } + displayPage.Suspended = u.Status == UserSuspended displayPage.Owner = owner coll.Owner = displayPage.Owner // Add more data @@ -946,7 +948,7 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error collAlias := vars["alias"] isWeb := r.FormValue("web") == "1" - var u *User + u := &User{} if reqJSON && !isWeb { // Ensure an access token was given accessToken := r.Header.Get("Authorization") @@ -963,7 +965,7 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error suspended, err := app.db.IsUserSuspended(u.ID) if err != nil { - log.Error("existing collection: get user suspended status: %v", err) + log.Error("existing collection: %v", err) return ErrInternalGeneral } diff --git a/database.go b/database.go index b465e68..4b0c702 100644 --- a/database.go +++ b/database.go @@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u func (db *datastore) GetUserByID(id int64) (*User, error) { u := &User{ID: id} - err := db.QueryRow("SELECT username, password, email, created, suspended FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) + err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -313,16 +313,16 @@ func (db *datastore) GetUserByID(id int64) (*User, error) { func (db *datastore) IsUserSuspended(id int64) (bool, error) { u := &User{ID: id} - err := db.QueryRow("SELECT suspended FROM users WHERE id = ?", id).Scan(&u.Suspended) + err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) switch { case err == sql.ErrNoRows: - return false, ErrUserNotFound + return false, fmt.Errorf("is user suspended: %v", ErrUserNotFound) case err != nil: log.Error("Couldn't SELECT user password: %v", err) - return false, err + return false, fmt.Errorf("is user suspended: %v", err) } - return u.Suspended, nil + return u.Status == UserSuspended, nil } // DoesUserNeedAuth returns true if the user hasn't provided any methods for @@ -364,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) { func (db *datastore) GetUserForAuth(username string) (*User, error) { u := &User{Username: username} - err := db.QueryRow("SELECT id, password, email, created, suspended FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) + err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) switch { case err == sql.ErrNoRows: // Check if they've entered the wrong, unnormalized username @@ -387,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) { func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) { u := &User{ID: userID} - err := db.QueryRow("SELECT id, password, email, created, suspended FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) + err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -1650,7 +1650,7 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) { SELECT COUNT(*) FROM collections c LEFT JOIN users u ON u.id = c.owner_id - WHERE u.suspended = 0`).Scan(&collCount) + WHERE u.status = 0`).Scan(&collCount) if err != nil { log.Error("Unable to fetch collections count: %v", err) } @@ -1662,7 +1662,7 @@ func (db *datastore) GetTotalPosts() (postCount int64, err error) { SELECT COUNT(*) FROM posts p LEFT JOIN users u ON u.id = p.owner_id - WHERE u.Suspended = 0`).Scan(&postCount) + WHERE u.status = 0`).Scan(&postCount) if err != nil { log.Error("Unable to fetch posts count: %v", err) } @@ -2384,7 +2384,7 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) { limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage) } - rows, err := db.Query("SELECT id, username, created, suspended FROM users ORDER BY created DESC LIMIT " + limitStr) + rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr) if err != nil { log.Error("Failed selecting from users: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."} @@ -2394,7 +2394,7 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) { users := []User{} for rows.Next() { u := User{} - err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Suspended) + err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status) if err != nil { log.Error("Failed scanning GetAllUsers() row: %v", err) break @@ -2431,10 +2431,11 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) { return &t, nil } -func (db *datastore) SetUserSuspended(id int64, suspend bool) error { - _, err := db.Exec("UPDATE users SET suspended = ? WHERE id = ?", suspend, id) +// SetUserStatus changes a user's status in the database. see Users.UserStatus +func (db *datastore) SetUserStatus(id int64, status UserStatus) error { + _, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id) if err != nil { - return fmt.Errorf("failed to update user suspended status: %v", err) + return fmt.Errorf("failed to update user status: %v", err) } return nil } diff --git a/invites.go b/invites.go index adff49a..8f341ec 100644 --- a/invites.go +++ b/invites.go @@ -78,7 +78,7 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re muVal := r.FormValue("uses") expVal := r.FormValue("expires") - if u.Suspended { + if u.Status == UserSuspended { return ErrUserSuspended } diff --git a/migrations/migrations.go b/migrations/migrations.go index de3f487..0799f8e 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -58,7 +58,7 @@ func (m *migration) Migrate(db *datastore) error { var migrations = []Migration{ New("support user invites", supportUserInvites), // -> V1 (v0.8.0) New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0) - New("support users suspension", supportUserSuspension), // V2 -> V3 () + New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0) } // CurrentVer returns the current migration version the application is on diff --git a/migrations/v3.go b/migrations/v3.go index c7c00a9..b5351da 100644 --- a/migrations/v3.go +++ b/migrations/v3.go @@ -10,10 +10,10 @@ package migrations -func supportUserSuspension(db *datastore) error { +func supportUserStatus(db *datastore) error { t, err := db.Begin() - _, err = t.Exec(`ALTER TABLE users ADD COLUMN suspended ` + db.typeBool() + ` DEFAULT '0' NOT NULL`) + _, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`) if err != nil { t.Rollback() return err diff --git a/posts.go b/posts.go index 33d4638..15d93c8 100644 --- a/posts.go +++ b/posts.go @@ -383,7 +383,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { suspended, err := app.db.IsUserSuspended(ownerID.Int64) if err != nil { - log.Error("view post: get collection owner: %v", err) + log.Error("view post: %v", err) return ErrInternalGeneral } @@ -509,7 +509,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { } suspended, err := app.db.IsUserSuspended(userID) if err != nil { - log.Error("new post: get user: %v", err) + log.Error("new post: %v", err) return ErrInternalGeneral } if suspended { @@ -683,7 +683,7 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { suspended, err := app.db.IsUserSuspended(userID) if err != nil { - log.Error("existing post: get user: %v", err) + log.Error("existing post: %v", err) return ErrInternalGeneral } if suspended { @@ -886,7 +886,7 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error { suspended, err := app.db.IsUserSuspended(ownerID) if err != nil { - log.Error("add post: get user: %v", err) + log.Error("add post: %v", err) return ErrInternalGeneral } if suspended { @@ -989,7 +989,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { suspended, err := app.db.IsUserSuspended(userID) if err != nil { - log.Error("pin post: get user: %v", err) + log.Error("pin post: %v", err) return ErrInternalGeneral } if suspended { @@ -1063,7 +1063,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { } suspended, err := app.db.IsUserSuspended(ownerID) if err != nil { - log.Error("fetch post: get owner: %v", err) + log.Error("fetch post: %v", err) return ErrInternalGeneral } @@ -1333,13 +1333,10 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("view collection post: get owner: %v", err) + log.Error("view collection post: %v", err) return ErrInternalGeneral } - if suspended { - return ErrPostNotFound - } // Check collection permissions if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { return ErrPostNotFound @@ -1396,6 +1393,9 @@ Are you sure it was ever here?`, p.Collection = coll p.IsTopLevel = app.cfg.App.SingleUser + if !p.IsOwner && suspended { + return ErrPostNotFound + } // Check if post has been unpublished if p.Content == "" && p.Title.String == "" { return impart.HTTPError{http.StatusGone, "Post was unpublished."} @@ -1445,12 +1445,14 @@ Are you sure it was ever here?`, IsFound bool IsAdmin bool CanInvite bool + Suspended bool }{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, + Suspended: suspended, } tp.IsAdmin = u != nil && u.IsAdmin() tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) diff --git a/read.go b/read.go index 86664b5..6d0c8a7 100644 --- a/read.go +++ b/read.go @@ -71,7 +71,7 @@ func (app *App) FetchPublicPosts() (interface{}, error) { FROM collections c LEFT JOIN posts p ON p.collection_id = c.id LEFT JOIN users u ON u.id = p.owner_id - WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.suspended = 0 + WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.status = 0 ORDER BY p.created DESC`) if err != nil { log.Error("Failed selecting from posts: %v", err) diff --git a/routes.go b/routes.go index 510a539..1ff250f 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(handleAdminToggleUserSuspended)).Methods("POST") + write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).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/schema.sql b/schema.sql index 3a79736..b3fae97 100644 --- a/schema.sql +++ b/schema.sql @@ -225,7 +225,6 @@ CREATE TABLE IF NOT EXISTS `users` ( `password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, `email` varbinary(255) DEFAULT NULL, `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `suspended` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; diff --git a/sqlite.sql b/sqlite.sql index 920ffed..90989ed 100644 --- a/sqlite.sql +++ b/sqlite.sql @@ -214,8 +214,7 @@ CREATE TABLE IF NOT EXISTS `users` ( username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, email TEXT DEFAULT NULL, - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - suspended INTEGER NOT NULL DEFAULT 0 + created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- -------------------------------------------------------- diff --git a/templates.go b/templates.go index 6e9a008..968845d 100644 --- a/templates.go +++ b/templates.go @@ -11,10 +11,6 @@ package writefreely import ( - "github.com/dustin/go-humanize" - "github.com/writeas/web-core/l10n" - "github.com/writeas/web-core/log" - "github.com/writeas/writefreely/config" "html/template" "io" "io/ioutil" @@ -22,6 +18,11 @@ import ( "os" "path/filepath" "strings" + + "github.com/dustin/go-humanize" + "github.com/writeas/web-core/l10n" + "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/config" ) var ( @@ -63,6 +64,7 @@ func initTemplate(parentDir, name string) { filepath.Join(parentDir, templatesDir, name+".tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), + filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"), } if name == "collection" || name == "collection-tags" || name == "chorus-collection" { // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" @@ -86,6 +88,7 @@ func initPage(parentDir, path, key string) { path, filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), + filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"), )) } @@ -98,6 +101,7 @@ func initUserPage(parentDir, path, key string) { path, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), + filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"), )) } diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index bab2e31..58e514f 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -65,6 +65,9 @@ article time.dt-published { {{template "user-navigation" .}} + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}
    {{if .IsScheduled}}

    Scheduled

    {{end}}{{if .Title.String}}

    {{.FormattedDisplayTitle}}

    {{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}
    {{.HTMLContent}}
    {{ if .Collection.ShowFooterBranding }} diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl index e36d3b5..5f4d418 100644 --- a/templates/chorus-collection.tmpl +++ b/templates/chorus-collection.tmpl @@ -61,6 +61,9 @@ body#collection header nav.tabs a:first-child { {{template "user-navigation" .}} + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

    {{.DisplayTitle}}

    {{if .Description}}

    {{.Description}}

    {{end}} diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index 7075226..4398804 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -59,6 +59,9 @@
    + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}
    {{if .IsScheduled}}

    Scheduled

    {{end}}{{if .Title.String}}

    {{.FormattedDisplayTitle}}

    {{end}}
    {{.HTMLContent}}
    {{ if .Collection.ShowFooterBranding }} diff --git a/templates/collection-tags.tmpl b/templates/collection-tags.tmpl index 7cad3b7..5e2e2d3 100644 --- a/templates/collection-tags.tmpl +++ b/templates/collection-tags.tmpl @@ -53,6 +53,9 @@
    + {{if .Suspended}} + {{template "user-suspended"}} + {{end}} {{if .Posts}}
    {{else}}
    {{end}}

    {{.Tag}}

    {{template "posts" .}} diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 36a266b..5a33bba 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -62,6 +62,9 @@ {{end}}
    + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

    {{if .Posts}}{{else}}write.as {{end}}{{.DisplayTitle}}

    {{if .Description}}

    {{.Description}}

    {{end}} {{/*if not .Public/*}} diff --git a/templates/password-collection.tmpl b/templates/password-collection.tmpl index e0b755d..73c4465 100644 --- a/templates/password-collection.tmpl +++ b/templates/password-collection.tmpl @@ -25,6 +25,9 @@ + {{if .Suspended}} + {{template "user-supsended"}} + {{end}}

    {{.DisplayTitle}}

    diff --git a/templates/post.tmpl b/templates/post.tmpl index dd1375e..74135a3 100644 --- a/templates/post.tmpl +++ b/templates/post.tmpl @@ -36,6 +36,9 @@ + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

    {{.SiteName}}

    {{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 @@

    diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 36a266b..fa66f69 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -68,7 +68,7 @@ {{/*end*/}} {{if .PinnedPosts}} + {{range .PinnedPosts}}{{.PlainDisplayTitle}}{{end}} {{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 70/90] 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 71/90] 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 72/90] 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}} - +