From f541f72224eb294eec4d4de81073145c7e18a931 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 21 Jul 2019 15:15:52 -0400 Subject: [PATCH 1/6] Style collection 404 page like rest of blog This displays the "page is missing" text within the same page as any other blog post, keeping customizations, pinned pages, and general blog navigation. Ref T493 --- posts.go | 41 +++++++++++++++++++++++++++++----- templates/collection-post.tmpl | 6 +++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/posts.go b/posts.go index 07902ce..518a07e 100644 --- a/posts.go +++ b/posts.go @@ -1295,14 +1295,32 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error coll.Owner = owner } + postFound := true p, err := app.db.GetPost(slug, coll.ID) if err != nil { - if err == ErrCollectionPageNotFound && slug == "feed" { - // User tried to access blog feed without a trailing slash, and - // there's no post with a slug "feed" - return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/feed/"} + if err == ErrCollectionPageNotFound { + postFound = false + + if slug == "feed" { + // User tried to access blog feed without a trailing slash, and + // there's no post with a slug "feed" + return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/feed/"} + } + + po := &Post{ + Slug: null.NewString(slug, true), + Font: "norm", + Language: zero.NewString("en", true), + RTL: zero.NewBool(false, true), + Content: `

This page is missing.

+ +Are you sure it was ever here?`, + } + pp := po.processPost() + p = &pp + } else { + return err } - return err } p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64 p.Collection = coll @@ -1324,11 +1342,18 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error contentType = "text/markdown" } w.Header().Set("Content-Type", fmt.Sprintf("%s; charset=utf-8", contentType)) + if !postFound { + w.WriteHeader(http.StatusNotFound) + } + if isMarkdown && p.Title.String != "" { fmt.Fprintf(w, "# %s\n\n", p.Title.String) } fmt.Fprint(w, p.Content) } else if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { + if !postFound { + w.WriteHeader(http.StatusNotFound) + } p.extractData() ap := p.ActivityObject() ap.Context = []interface{}{activitystreams.Namespace} @@ -1345,14 +1370,20 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error IsPinned bool IsCustomDomain bool PinnedPosts *[]PublicPost + IsFound bool }{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, IsCustomDomain: cr.isCustomDomain, + IsFound: postFound, } tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll) tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) + + if !postFound { + w.WriteHeader(http.StatusNotFound) + } if err := templates["collection-post"].ExecuteTemplate(w, "post", tp); err != nil { log.Error("Error in collection-post template: %v", err) } diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index 06bfa67..7075226 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -8,6 +8,7 @@ + {{ if .IsFound }} @@ -29,6 +30,7 @@ {{range .Images}}{{else}}{{end}} + {{ end }} {{if .Collection.StyleSheet}}{{end}} {{if .Collection.RenderMathJax}} @@ -50,14 +52,14 @@ {{if .PinnedPosts}} {{range .PinnedPosts}}{{.PlainDisplayTitle}}{{end}} {{end}} - {{ if .IsOwner }}{{largeNumFmt .Views}} {{pluralize "view" "views" .Views}} + {{ if and .IsOwner .IsFound }}{{largeNumFmt .Views}} {{pluralize "view" "views" .Views}} Edit {{if .IsPinned}}Unpin{{end}} {{ end }} -
{{if .IsScheduled}}

Scheduled

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

{{.FormattedDisplayTitle}}

{{end}}
{{.HTMLContent}}
+
{{if .IsScheduled}}

Scheduled

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

{{.FormattedDisplayTitle}}

{{end}}
{{.HTMLContent}}
{{ if .Collection.ShowFooterBranding }} From f6f116d6726c4f6a42929aaf0a5263866f0a32fc Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 14:02:53 -0400 Subject: [PATCH 2/6] Fix missing hostname when publishing via API This fixes a bug that occurred only when publishing via API and authenticating via token (rather than cookie). Previously, the instance's hostname wouldn't be added to the Collection that got passed around after retrieving the owned post, meaning an incomplete URL was returned in the API response, and federation failed due to the missing host. --- database.go | 5 +++-- posts.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/database.go b/database.go index 1ea9872..2f9dcfa 100644 --- a/database.go +++ b/database.go @@ -73,7 +73,7 @@ type writestore interface { GetAnonymousPosts(u *User) (*[]PublicPost, error) GetUserPosts(u *User) (*[]PublicPost, error) - CreateOwnedPost(post *SubmittedPost, accessToken, collAlias string) (*PublicPost, error) + CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) CreatePost(userID, collID int64, post *SubmittedPost) (*Post, error) UpdateOwnedPost(post *AuthenticatedPost, userID int64) error GetEditablePost(id, editToken string) (*PublicPost, error) @@ -541,7 +541,7 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, return u.String(), nil } -func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias string) (*PublicPost, error) { +func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) { var userID, collID int64 = -1, -1 var coll *Collection var err error @@ -555,6 +555,7 @@ func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias if err != nil { return nil, err } + coll.hostName = hostName if coll.OwnerID != userID { return nil, ErrForbiddenCollection } diff --git a/posts.go b/posts.go index 07902ce..1b4f910 100644 --- a/posts.go +++ b/posts.go @@ -556,7 +556,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { var coll *Collection var err error if accessToken != "" { - newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias) + newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host) } else { //return ErrNotLoggedIn // TODO: verify user is logged in From 4faf41ae7ffd8aafa7167c0ec2e0cfe218547cfb Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 14:16:44 -0400 Subject: [PATCH 3/6] Log missing hostName in Collection.RedirectingCanonicalURL This is the crucial part where the hostName is needed for federation and API clients. This change at least lets us know when we mess up like this so the issue is easier to catch in the future. --- collections.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/collections.go b/collections.go index 1a8ceca..d874e3e 100644 --- a/collections.go +++ b/collections.go @@ -211,6 +211,10 @@ func (c *Collection) DisplayCanonicalURL() string { } func (c *Collection) RedirectingCanonicalURL(isRedir bool) string { + if c.hostName == "" { + // If this is true, the human programmers screwed up. So ask for a bug report and fail, fail, fail + log.Error("[PROGRAMMER ERROR] WARNING: Collection.hostName is empty! Federation and many other things will fail! If you're seeing this in the wild, please report this bug and let us know what you were doing just before this: https://github.com/writeas/writefreely/issues/new?template=bug_report.md") + } if isSingleUser { return c.hostName + "/" } From 5953a50f4a7deb9b420e11407983a414f28bf503 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 1 Aug 2019 16:12:22 -0400 Subject: [PATCH 4/6] Let Apper set request log format This adds a new ReqLog() func to the Apper interface that'll return the log message for incoming requests. Ref T649 --- app.go | 6 ++++++ handle.go | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app.go b/app.go index 79b7145..4b9e840 100644 --- a/app.go +++ b/app.go @@ -118,6 +118,8 @@ type Apper interface { SaveConfig(*config.Config) error LoadKeys() error + + ReqLog(r *http.Request, status int, timeSince time.Duration) string } // App returns the App @@ -177,6 +179,10 @@ func (app *App) LoadKeys() error { return nil } +func (app *App) ReqLog(r *http.Request, status int, timeSince time.Duration) string { + return fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, timeSince, r.UserAgent()) +} + // handleViewHome shows page at root path. Will be the Pad if logged in and the // catch-all landing page otherwise. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { diff --git a/handle.go b/handle.go index 4f137b9..1e5e8f6 100644 --- a/handle.go +++ b/handle.go @@ -137,7 +137,7 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc { status = http.StatusInternalServerError } - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u := getUserSession(h.app.App(), r) @@ -175,7 +175,7 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc { status = http.StatusInternalServerError } - log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u := getUserSession(h.app.App(), r) @@ -213,7 +213,7 @@ func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc { status = http.StatusInternalServerError } - log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u := getUserSession(h.app.App(), r) @@ -295,7 +295,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF status = 500 } - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u, err := a(h.app.App(), r) @@ -381,7 +381,7 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevelFunc) http.HandlerFunc { status = 500 } - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() var session *sessions.Session @@ -440,7 +440,7 @@ func (h *Handler) CollectionPostOrStatic(w http.ResponseWriter, r *http.Request) start := time.Now() status := 200 defer func() { - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() // Serve static file @@ -472,7 +472,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevelFunc) http.HandlerFunc { status = 500 } - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() if ul(h.app.App().cfg) != UserLevelNoneType { @@ -530,7 +530,7 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc { status = 500 } - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() // TODO: do any needed authentication @@ -562,7 +562,7 @@ func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc { status = 500 } - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() if h.app.App().cfg.App.Private { @@ -619,7 +619,7 @@ func (h *Handler) Download(f dataHandlerFunc, ul UserLevelFunc) http.HandlerFunc status = 500 } - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() data, filename, err := f(h.app.App(), w, r) @@ -682,7 +682,7 @@ func (h *Handler) Redirect(url string, ul UserLevelFunc) http.HandlerFunc { status = sendRedirect(w, http.StatusFound, url) - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) return nil }()) @@ -799,7 +799,7 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc { } // TODO: log actual status code returned - log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) + log.Info(h.app.ReqLog(r, status, time.Since(start))) }() if h.app.App().cfg.App.Private { From 35906118d0f122b92592aa023ebcb586c829a207 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 7 Aug 2019 10:18:40 -0400 Subject: [PATCH 5/6] Return only 404 on ActivityPub coll post request Ref T493 --- handle.go | 4 ++++ posts.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/handle.go b/handle.go index 4f137b9..56d5e57 100644 --- a/handle.go +++ b/handle.go @@ -721,6 +721,10 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er return } else if err.Status == http.StatusNotFound { w.WriteHeader(err.Status) + if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { + // This is a fediverse request; simply return the header + return + } h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) return } else if err.Status == http.StatusInternalServerError { diff --git a/posts.go b/posts.go index 518a07e..3811253 100644 --- a/posts.go +++ b/posts.go @@ -1352,7 +1352,7 @@ Are you sure it was ever here?`, fmt.Fprint(w, p.Content) } else if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { if !postFound { - w.WriteHeader(http.StatusNotFound) + return ErrCollectionPageNotFound } p.extractData() ap := p.ActivityObject() From 582f041748b62bfa79b9ee8416299f5429b38791 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 7 Aug 2019 10:26:36 -0400 Subject: [PATCH 6/6] Return plainer message on coll .txt post 404 Ref T493 --- posts.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/posts.go b/posts.go index 3811253..ca1be25 100644 --- a/posts.go +++ b/posts.go @@ -1344,8 +1344,10 @@ Are you sure it was ever here?`, w.Header().Set("Content-Type", fmt.Sprintf("%s; charset=utf-8", contentType)) if !postFound { w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "Post not found.") + // TODO: return error instead, so status is correctly reflected in logs + return nil } - if isMarkdown && p.Title.String != "" { fmt.Fprintf(w, "# %s\n\n", p.Title.String) }