diff options
author | 2023-12-27 11:23:52 +0100 | |
---|---|---|
committer | 2023-12-27 11:23:52 +0100 | |
commit | 0ff52b71f2c0e970b1f0d43793c019bbed93e112 (patch) | |
tree | eff120472b4b6f837121536ada03f530d213b13e /internal/web | |
parent | [bugfix] :innocent: (#2476) (diff) | |
download | gotosocial-0ff52b71f2c0e970b1f0d43793c019bbed93e112.tar.xz |
[chore] Refactor HTML templates and CSS (#2480)
* [chore] Refactor HTML templates and CSS
* eslint
* ignore "Local"
* rss tests
* fiddle with OG just a tiny bit
* dick around with polls a bit more so SR stops saying "clickable"
* remove break
* oh lord
* don't lazy load avatar
* fix ogmeta tests
* clean up some cruft
* catch remaining calls to c.HTML
* fix error rendering + stack overflow in tag
* allow templating attributes
* fix indent
* set aria-hidden on status complementary content, since it's already present in the label anyway
* tidy up templating calls a little
* try to make styling a bit more consistent + readable
* fix up some remaining CSS issues
* fix up reports
Diffstat (limited to 'internal/web')
-rw-r--r-- | internal/web/about.go | 42 | ||||
-rw-r--r-- | internal/web/confirmemail.go | 49 | ||||
-rw-r--r-- | internal/web/customcss.go | 30 | ||||
-rw-r--r-- | internal/web/domain-blocklist.go | 49 | ||||
-rw-r--r-- | internal/web/index.go (renamed from internal/web/base.go) | 43 | ||||
-rw-r--r-- | internal/web/opengraph.go | 180 | ||||
-rw-r--r-- | internal/web/opengraph_test.go | 116 | ||||
-rw-r--r-- | internal/web/profile.go | 45 | ||||
-rw-r--r-- | internal/web/robots.go | 2 | ||||
-rw-r--r-- | internal/web/settings-panel.go | 44 | ||||
-rw-r--r-- | internal/web/tag.go | 18 | ||||
-rw-r--r-- | internal/web/thread.go | 30 | ||||
-rw-r--r-- | internal/web/web.go | 19 |
13 files changed, 222 insertions, 445 deletions
diff --git a/internal/web/about.go b/internal/web/about.go index 89bb13f0d..2bc558962 100644 --- a/internal/web/about.go +++ b/internal/web/about.go @@ -18,9 +18,10 @@ package web import ( - "net/http" + "context" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -31,20 +32,35 @@ const ( ) func (m *Module) aboutGETHandler(c *gin.Context) { - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - c.HTML(http.StatusOK, "about.tmpl", gin.H{ - "instance": instance, - "languages": config.GetInstanceLanguages().DisplayStrs(), - "ogMeta": ogBase(instance), - "blocklistExposed": config.GetInstanceExposeSuspendedWeb(), - "stylesheets": []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "about.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssAbout}, + Extra: map[string]any{ + "showStrap": true, + "blocklistExposed": config.GetInstanceExposeSuspendedWeb(), + "languages": config.GetInstanceLanguages().DisplayStrs(), }, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go index b0ece58cd..f15252bf7 100644 --- a/internal/web/confirmemail.go +++ b/internal/web/confirmemail.go @@ -18,39 +18,58 @@ package web import ( + "context" "errors" "net/http" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) func (m *Module) confirmEmailGETHandler(c *gin.Context) { - ctx := c.Request.Context() + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } - // if there's no token in the query, just serve the 404 web handler - token := c.Query(tokenParam) + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + // If there's no token in the query, + // just serve the 404 web handler. + token := c.Query("token") if token == "" { - apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGetV1) + errWithCode := gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) return } - user, errWithCode := m.processor.User().EmailConfirm(ctx, token) + user, errWithCode := m.processor.User().EmailConfirm(c.Request.Context(), token) if errWithCode != nil { - apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) return } - instance, err := m.processor.InstanceGetV1(ctx) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) - return + page := apiutil.WebPage{ + Template: "confirmed.tmpl", + Instance: instance, + Extra: map[string]any{ + "email": user.Email, + "username": user.Account.Username, + }, } - c.HTML(http.StatusOK, "confirmed.tmpl", gin.H{ - "instance": instance, - "email": user.Email, - "username": user.Account.Username, - }) + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/customcss.go b/internal/web/customcss.go index ef57e0033..b23ebce8e 100644 --- a/internal/web/customcss.go +++ b/internal/web/customcss.go @@ -18,9 +18,7 @@ package web import ( - "errors" "net/http" - "strings" "github.com/gin-gonic/gin" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" @@ -31,31 +29,29 @@ import ( const textCSSUTF8 = string(apiutil.TextCSS + "; charset=utf-8") func (m *Module) customCSSGETHandler(c *gin.Context) { - if !config.GetAccountsAllowCustomCSS() { - err := errors.New("accounts-allow-custom-css is not enabled on this instance") - apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGetV1) - return - } - if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil { apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) return } - // usernames on our instance will always be lowercase - username := strings.ToLower(c.Param(usernameKey)) - if username == "" { - err := errors.New("no account username specified") - apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) - return - } - - customCSS, errWithCode := m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), username) + targetUsername, errWithCode := apiutil.ParseWebUsername(c.Param(apiutil.WebUsernameKey)) if errWithCode != nil { apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } + // Retrieve customCSS if enabled on the instance. + // Else use an empty string, to help with caching + // when custom CSS gets toggled on or off. + var customCSS string + if config.GetAccountsAllowCustomCSS() { + customCSS, errWithCode = m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), targetUsername) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + } + c.Header(cacheControlHeader, cacheControlNoCache) c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS)) } diff --git a/internal/web/domain-blocklist.go b/internal/web/domain-blocklist.go index 8a88a0932..5d631e0f7 100644 --- a/internal/web/domain-blocklist.go +++ b/internal/web/domain-blocklist.go @@ -18,14 +18,14 @@ package web import ( + "context" "fmt" - "net/http" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" ) const ( @@ -33,37 +33,44 @@ const ( ) func (m *Module) domainBlockListGETHandler(c *gin.Context) { - authed, err := oauth.Authed(c, false, false, false, false) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - if !config.GetInstanceExposeSuspendedWeb() && (authed.Account == nil || authed.User == nil) { - err := fmt.Errorf("this instance does not expose the list of suspended domains publicly") - apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) return } - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) + if !config.GetInstanceExposeSuspendedWeb() { + err := fmt.Errorf("this instance does not publicy expose its blocklist") + apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), instanceGet) return } domainBlocks, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), true, false, false) if errWithCode != nil { - apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) return } - c.HTML(http.StatusOK, "domain-blocklist.tmpl", gin.H{ - "instance": instance, - "ogMeta": ogBase(instance), - "blocklist": domainBlocks, - "stylesheets": []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - }, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + page := apiutil.WebPage{ + Template: "domain-blocklist.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssFA}, + Javascript: []string{jsFrontend}, + Extra: map[string]any{"blocklist": domainBlocks}, + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/base.go b/internal/web/index.go index 5bc3c536a..25960cf7f 100644 --- a/internal/web/base.go +++ b/internal/web/index.go @@ -18,33 +18,50 @@ package web import ( + "context" "net/http" "strings" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) -func (m *Module) baseHandler(c *gin.Context) { - // if a landingPageUser is set in the config, redirect to that user's profile +func (m *Module) indexHandler(c *gin.Context) { + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + // If a landingPageUser is set in the config, redirect to + // that user's profile instead of rendering landing/index page. if landingPageUser := config.GetLandingPageUser(); landingPageUser != "" { c.Redirect(http.StatusFound, "/@"+strings.ToLower(landingPageUser)) return } - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) - return + page := apiutil.WebPage{ + Template: "index.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssAbout, cssIndex}, + Extra: map[string]any{"showStrap": true}, } - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "instance": instance, - "ogMeta": ogBase(instance), - "stylesheets": []string{ - distPathPrefix + "/index.css", - }, - }) + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/opengraph.go b/internal/web/opengraph.go deleted file mode 100644 index 66b6c6eea..000000000 --- a/internal/web/opengraph.go +++ /dev/null @@ -1,180 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package web - -import ( - "html" - "strconv" - "strings" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/text" -) - -const maxOGDescriptionLength = 300 - -// ogMeta represents supported OpenGraph Meta tags -// -// see eg https://ogp.me/ -type ogMeta struct { - // vanilla og tags - Title string // og:title - Type string // og:type - Locale string // og:locale - URL string // og:url - SiteName string // og:site_name - Description string // og:description - - // image tags - Image string // og:image - ImageWidth string // og:image:width - ImageHeight string // og:image:height - ImageAlt string // og:image:alt - - // article tags - ArticlePublisher string // article:publisher - ArticleAuthor string // article:author - ArticleModifiedTime string // article:modified_time - ArticlePublishedTime string // article:published_time - - // profile tags - ProfileUsername string // profile:username -} - -// ogBase returns an *ogMeta suitable for serving at -// the base root of an instance. It also serves as a -// foundation for building account / status ogMeta on -// top of. -func ogBase(instance *apimodel.InstanceV1) *ogMeta { - var locale string - if len(instance.Languages) > 0 { - locale = instance.Languages[0] - } - - og := &ogMeta{ - Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial", - Type: "website", - Locale: locale, - URL: instance.URI, - SiteName: instance.AccountDomain, - Description: parseDescription(instance.ShortDescription), - - Image: instance.Thumbnail, - ImageAlt: instance.ThumbnailDescription, - } - - return og -} - -// withAccount uses the given account to build an ogMeta -// struct specific to that account. It's suitable for serving -// at account profile pages. -func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta { - og.Title = parseTitle(account, og.SiteName) - og.Type = "profile" - og.URL = account.URL - if account.Note != "" { - og.Description = parseDescription(account.Note) - } else { - og.Description = `content="This GoToSocial user hasn't written a bio yet!"` - } - - og.Image = account.Avatar - og.ImageAlt = "Avatar for " + account.Username - - og.ProfileUsername = account.Username - - return og -} - -// withStatus uses the given status to build an ogMeta -// struct specific to that status. It's suitable for serving -// at status pages. -func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta { - og.Title = "Post by " + parseTitle(status.Account, og.SiteName) - og.Type = "article" - if status.Language != nil { - og.Locale = *status.Language - } - og.URL = status.URL - switch { - case status.SpoilerText != "": - og.Description = parseDescription("CW: " + status.SpoilerText) - case status.Text != "": - og.Description = parseDescription(status.Text) - default: - og.Description = og.Title - } - - if !status.Sensitive && len(status.MediaAttachments) > 0 { - a := status.MediaAttachments[0] - - og.ImageWidth = strconv.Itoa(a.Meta.Small.Width) - og.ImageHeight = strconv.Itoa(a.Meta.Small.Height) - - if a.PreviewURL != nil { - og.Image = *a.PreviewURL - } - - if a.Description != nil { - og.ImageAlt = *a.Description - } - } else { - og.Image = status.Account.Avatar - og.ImageAlt = "Avatar for " + status.Account.Username - } - - og.ArticlePublisher = status.Account.URL - og.ArticleAuthor = status.Account.URL - og.ArticlePublishedTime = status.CreatedAt - og.ArticleModifiedTime = status.CreatedAt - - return og -} - -// parseTitle parses a page title from account and accountDomain -func parseTitle(account *apimodel.Account, accountDomain string) string { - user := "@" + account.Acct + "@" + accountDomain - - if len(account.DisplayName) == 0 { - return user - } - - return account.DisplayName + " (" + user + ")" -} - -// parseDescription returns a string description which is -// safe to use as a template.HTMLAttr inside templates. -func parseDescription(in string) string { - i := text.SanitizeToPlaintext(in) - i = strings.ReplaceAll(i, "\n", " ") - i = strings.Join(strings.Fields(i), " ") - i = html.EscapeString(i) - i = strings.ReplaceAll(i, `\`, "\") - i = trim(i, maxOGDescriptionLength) - return `content="` + i + `"` -} - -// trim strings trim s to specified length -func trim(s string, length int) string { - if len(s) < length { - return s - } - - return s[:length] -} diff --git a/internal/web/opengraph_test.go b/internal/web/opengraph_test.go deleted file mode 100644 index 06e97cdce..000000000 --- a/internal/web/opengraph_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package web - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/suite" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" -) - -type OpenGraphTestSuite struct { - suite.Suite -} - -func (suite *OpenGraphTestSuite) TestParseDescription() { - tests := []struct { - name, in, exp string - }{ - {name: "shellcmd", in: `echo '\e]8;;http://example.com\e\This is a link\e]8;;\e'`, exp: `echo '\e]8;;http://example.com\e\This is a link\e]8;;\e'`}, - {name: "newlines", in: "test\n\ntest\ntest", exp: "test test test"}, - } - - for _, tt := range tests { - tt := tt - suite.Run(tt.name, func() { - suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), parseDescription(tt.in)) - }) - } -} - -func (suite *OpenGraphTestSuite) TestWithAccountWithNote() { - baseMeta := ogBase(&apimodel.InstanceV1{ - AccountDomain: "example.org", - Languages: []string{"en"}, - }) - - accountMeta := baseMeta.withAccount(&apimodel.Account{ - Acct: "example_account", - DisplayName: "example person!!", - URL: "https://example.org/@example_account", - Note: "<p>This is my profile, read it and weep! Weep then!</p>", - Username: "example_account", - }) - - suite.EqualValues(ogMeta{ - Title: "example person!! (@example_account@example.org)", - Type: "profile", - Locale: "en", - URL: "https://example.org/@example_account", - SiteName: "example.org", - Description: "content=\"This is my profile, read it and weep! Weep then!\"", - Image: "", - ImageWidth: "", - ImageHeight: "", - ImageAlt: "Avatar for example_account", - ArticlePublisher: "", - ArticleAuthor: "", - ArticleModifiedTime: "", - ArticlePublishedTime: "", - ProfileUsername: "example_account", - }, *accountMeta) -} - -func (suite *OpenGraphTestSuite) TestWithAccountNoNote() { - baseMeta := ogBase(&apimodel.InstanceV1{ - AccountDomain: "example.org", - Languages: []string{"en"}, - }) - - accountMeta := baseMeta.withAccount(&apimodel.Account{ - Acct: "example_account", - DisplayName: "example person!!", - URL: "https://example.org/@example_account", - Note: "", // <- empty - Username: "example_account", - }) - - suite.EqualValues(ogMeta{ - Title: "example person!! (@example_account@example.org)", - Type: "profile", - Locale: "en", - URL: "https://example.org/@example_account", - SiteName: "example.org", - Description: "content=\"This GoToSocial user hasn't written a bio yet!\"", - Image: "", - ImageWidth: "", - ImageHeight: "", - ImageAlt: "Avatar for example_account", - ArticlePublisher: "", - ArticleAuthor: "", - ArticleModifiedTime: "", - ArticlePublishedTime: "", - ProfileUsername: "example_account", - }, *accountMeta) -} - -func TestOpenGraphTestSuite(t *testing.T) { - suite.Run(t, &OpenGraphTestSuite{}) -} diff --git a/internal/web/profile.go b/internal/web/profile.go index b2c3bb944..b629c98b9 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -27,7 +27,6 @@ import ( "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" - "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -141,28 +140,28 @@ func (m *Module) profileGETHandler(c *gin.Context) { return } - stylesheets := []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/status.css", - distPathPrefix + "/profile.css", - } - if config.GetAccountsAllowCustomCSS() { - stylesheets = append(stylesheets, "/@"+targetAccount.Username+"/custom.css") - } - - c.HTML(http.StatusOK, "profile.tmpl", gin.H{ - "instance": instance, - "account": targetAccount, - "ogMeta": ogBase(instance).withAccount(targetAccount), - "rssFeed": rssFeed, - "robotsMeta": robotsMeta, - "statuses": statusResp.Items, - "statuses_next": statusResp.NextLink, - "pinned_statuses": pinnedStatuses, - "show_back_to_top": paging, - "stylesheets": stylesheets, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + page := apiutil.WebPage{ + Template: "profile.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount), + Stylesheets: []string{ + cssFA, cssStatus, cssThread, cssProfile, + // Custom CSS for this user last in cascade. + "/@" + targetAccount.Username + "/custom.css", + }, + Javascript: []string{jsFrontend}, + Extra: map[string]any{ + "account": targetAccount, + "rssFeed": rssFeed, + "robotsMeta": robotsMeta, + "statuses": statusResp.Items, + "statuses_next": statusResp.NextLink, + "pinned_statuses": pinnedStatuses, + "show_back_to_top": paging, + }, + } + + apiutil.TemplateWebPage(c, page) } // returnAPAccount returns an ActivityPub representation of diff --git a/internal/web/robots.go b/internal/web/robots.go index aee4d1a55..92bd33014 100644 --- a/internal/web/robots.go +++ b/internal/web/robots.go @@ -71,7 +71,7 @@ Crawl-delay: 500 # API endpoints. Disallow: /api/ -# Auth/login endpoints. +# Auth/Sign in endpoints. Disallow: /auth/ Disallow: /oauth/ Disallow: /check_your_email diff --git a/internal/web/settings-panel.go b/internal/web/settings-panel.go index 615f2d265..9de30d1f1 100644 --- a/internal/web/settings-panel.go +++ b/internal/web/settings-panel.go @@ -18,30 +18,44 @@ package web import ( - "net/http" + "context" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) func (m *Module) SettingsPanelHandler(c *gin.Context) { - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - c.HTML(http.StatusOK, "frontend.tmpl", gin.H{ - "instance": instance, - "stylesheets": []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/_colors.css", - distPathPrefix + "/base.css", - distPathPrefix + "/profile.css", - distPathPrefix + "/status.css", - distPathPrefix + "/settings-style.css", + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "frontend.tmpl", + Instance: instance, + Stylesheets: []string{ + cssFA, + cssProfile, // Used for rendering stub/fake profiles. + cssStatus, // Used for rendering stub/fake statuses. + cssSettings, }, - "javascript": []string{distPathPrefix + "/settings.js"}, - }) + Javascript: []string{jsSettings}, + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/tag.go b/internal/web/tag.go index 69591f114..5c3cd31a6 100644 --- a/internal/web/tag.go +++ b/internal/web/tag.go @@ -19,7 +19,6 @@ package web import ( "context" - "net/http" "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -56,16 +55,13 @@ func (m *Module) tagGETHandler(c *gin.Context) { return } - stylesheets := []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/status.css", - distPathPrefix + "/tag.css", + page := apiutil.WebPage{ + Template: "tag.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssFA, cssThread, cssTag}, + Extra: map[string]any{"tagName": tagName}, } - c.HTML(http.StatusOK, "tag.tmpl", gin.H{ - "instance": instance, - "ogMeta": ogBase(instance), - "tagName": tagName, - "stylesheets": stylesheets, - }) + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/thread.go b/internal/web/thread.go index 20145cfcd..4dcd1d221 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -28,7 +28,6 @@ import ( "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" - "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -139,22 +138,23 @@ func (m *Module) threadGETHandler(c *gin.Context) { return } - stylesheets := []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/status.css", - } - if config.GetAccountsAllowCustomCSS() { - stylesheets = append(stylesheets, "/@"+targetUsername+"/custom.css") + page := apiutil.WebPage{ + Template: "thread.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance).WithStatus(status), + Stylesheets: []string{ + cssFA, cssStatus, cssThread, + // Custom CSS for this user last in cascade. + "/@" + targetUsername + "/custom.css", + }, + Javascript: []string{jsFrontend}, + Extra: map[string]any{ + "status": status, + "context": context, + }, } - c.HTML(http.StatusOK, "thread.tmpl", gin.H{ - "instance": instance, - "status": status, - "context": context, - "ogMeta": ogBase(instance).withStatus(status), - "stylesheets": stylesheets, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + apiutil.TemplateWebPage(c, page) } // returnAPStatus returns an ActivityPub representation of target status, diff --git a/internal/web/web.go b/internal/web/web.go index 86e74d6f8..5e57d08c6 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -37,7 +37,7 @@ import ( const ( confirmEmailPath = "/" + uris.ConfirmEmailPath - profileGroupPath = "/@:" + usernameKey + profileGroupPath = "/@:username" statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group tagsPath = "/tags/:" + apiutil.TagNameKey customCSSPath = profileGroupPath + "/custom.css" @@ -49,15 +49,24 @@ const ( userPanelPath = settingsPathPrefix + "/user" adminPanelPath = settingsPathPrefix + "/admin" - tokenParam = "token" - usernameKey = "username" - cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives ifModifiedSinceHeader = "If-Modified-Since" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since ifNoneMatchHeader = "If-None-Match" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match eTagHeader = "ETag" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag lastModifiedHeader = "Last-Modified" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified + + cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css" + cssAbout = distPathPrefix + "/about.css" + cssIndex = distPathPrefix + "/index.css" + cssStatus = distPathPrefix + "/status.css" + cssThread = distPathPrefix + "/thread.css" + cssProfile = distPathPrefix + "/profile.css" + cssSettings = distPathPrefix + "/settings-style.css" + cssTag = distPathPrefix + "/tag.css" + + jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS. + jsSettings = distPathPrefix + "/settings.js" // Settings panel React application. ) type Module struct { @@ -99,7 +108,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) { profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler) // Attach individual web handlers which require no specific middlewares - r.AttachHandler(http.MethodGet, "/", m.baseHandler) // front-page + r.AttachHandler(http.MethodGet, "/", m.indexHandler) // front-page r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) |