summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/api/auth/authorize.go30
-rw-r--r--internal/api/auth/callback.go34
-rw-r--r--internal/api/auth/oob.go18
-rw-r--r--internal/api/auth/signin.go18
-rw-r--r--internal/api/model/status.go6
-rw-r--r--internal/api/util/errorhandling.go20
-rw-r--r--internal/api/util/opengraph.go (renamed from internal/web/opengraph.go)56
-rw-r--r--internal/api/util/opengraph_test.go (renamed from internal/web/opengraph_test.go)20
-rw-r--r--internal/api/util/template.go135
-rw-r--r--internal/oauth/server.go4
-rw-r--r--internal/processing/account/rss_test.go2
-rw-r--r--internal/router/router.go1
-rw-r--r--internal/router/template.go260
-rw-r--r--internal/router/template_test.go204
-rw-r--r--internal/text/emojify.go91
-rw-r--r--internal/typeutils/internaltofrontend.go6
-rw-r--r--internal/typeutils/internaltorss.go2
-rw-r--r--internal/typeutils/internaltorss_test.go2
-rw-r--r--internal/web/about.go42
-rw-r--r--internal/web/confirmemail.go49
-rw-r--r--internal/web/customcss.go30
-rw-r--r--internal/web/domain-blocklist.go49
-rw-r--r--internal/web/index.go (renamed from internal/web/base.go)43
-rw-r--r--internal/web/profile.go45
-rw-r--r--internal/web/robots.go2
-rw-r--r--internal/web/settings-panel.go44
-rw-r--r--internal/web/tag.go18
-rw-r--r--internal/web/thread.go30
-rw-r--r--internal/web/web.go19
29 files changed, 970 insertions, 310 deletions
diff --git a/internal/api/auth/authorize.go b/internal/api/auth/authorize.go
index 4977ae4f2..e4694de57 100644
--- a/internal/api/auth/authorize.go
+++ b/internal/api/auth/authorize.go
@@ -144,17 +144,25 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
return
}
- // the authorize template will display a form to the user where they can get some information
- // about the app that's trying to authorize, and the scope of the request.
- // They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
- c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
- "appname": app.Name,
- "appwebsite": app.Website,
- "redirect": redirect,
- "scope": scope,
- "user": acct.Username,
- "instance": instance,
- })
+ // The authorize template will display a form
+ // to the user where they can see some info
+ // about the app that's trying to authorize,
+ // and the scope of the request. They can then
+ // approve it if it looks OK to them, which
+ // will POST to the AuthorizePOSTHandler.
+ page := apiutil.WebPage{
+ Template: "authorize.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "appname": app.Name,
+ "appwebsite": app.Website,
+ "redirect": redirect,
+ "scope": scope,
+ "user": acct.Username,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
}
// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
diff --git a/internal/api/auth/callback.go b/internal/api/auth/callback.go
index 97b3ae279..d0fa78322 100644
--- a/internal/api/auth/callback.go
+++ b/internal/api/auth/callback.go
@@ -143,11 +143,17 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
return
}
- c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
- "instance": instance,
- "name": claims.Name,
- "preferredUsername": claims.PreferredUsername,
- })
+
+ page := apiutil.WebPage{
+ Template: "finalize.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "name": claims.Name,
+ "preferredUsername": claims.PreferredUsername,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
return
}
s.Set(sessionUserID, user.ID)
@@ -177,12 +183,18 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
- c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
- "instance": instance,
- "name": form.Name,
- "preferredUsername": form.Username,
- "error": err,
- })
+
+ page := apiutil.WebPage{
+ Template: "finalize.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "name": form.Name,
+ "preferredUsername": form.Username,
+ "error": err,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
}
// check if the username conforms to the spec
diff --git a/internal/api/auth/oob.go b/internal/api/auth/oob.go
index 5953524ab..8c7b1f2a5 100644
--- a/internal/api/auth/oob.go
+++ b/internal/api/auth/oob.go
@@ -21,7 +21,6 @@ import (
"context"
"errors"
"fmt"
- "net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -101,10 +100,15 @@ func (m *Module) OobHandler(c *gin.Context) {
// we're done with the session now, so just clear it out
m.clearSession(s)
- c.HTML(http.StatusOK, "oob.tmpl", gin.H{
- "instance": instance,
- "user": acct.Username,
- "oobToken": oobToken,
- "scope": scope,
- })
+ page := apiutil.WebPage{
+ Template: "oob.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "user": acct.Username,
+ "oobToken": oobToken,
+ "scope": scope,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
}
diff --git a/internal/api/auth/signin.go b/internal/api/auth/signin.go
index a6b503a83..a8713d05f 100644
--- a/internal/api/auth/signin.go
+++ b/internal/api/auth/signin.go
@@ -32,8 +32,8 @@ import (
"golang.org/x/crypto/bcrypt"
)
-// login just wraps a form-submitted username (we want an email) and password
-type login struct {
+// signIn just wraps a form-submitted username (we want an email) and password
+type signIn struct {
Email string `form:"username"`
Password string `form:"password"`
}
@@ -55,10 +55,12 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
return
}
- // no idp provider, use our own funky little sign in page
- c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{
- "instance": instance,
- })
+ page := apiutil.WebPage{
+ Template: "sign-in.tmpl",
+ Instance: instance,
+ }
+
+ apiutil.TemplateWebPage(c, page)
return
}
@@ -83,7 +85,7 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
func (m *Module) SignInPOSTHandler(c *gin.Context) {
s := sessions.Default(c)
- form := &login{}
+ form := &signIn{}
if err := c.ShouldBind(form); err != nil {
m.clearSession(s)
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
@@ -129,7 +131,7 @@ func (m *Module) ValidatePassword(ctx context.Context, email string, password st
}
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
- err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err)
+ err := fmt.Errorf("password hash didn't match for user %s during sign in attempt: %s", user.Email, err)
return incorrectPassword(err)
}
diff --git a/internal/api/model/status.go b/internal/api/model/status.go
index 128cd65bb..8ca41c767 100644
--- a/internal/api/model/status.go
+++ b/internal/api/model/status.go
@@ -116,6 +116,12 @@ type Status struct {
//
// swagger:ignore
WebPollOptions []WebPollOption `json:"-"`
+
+ // Status is from a local account.
+ // Always false for non-web statuses.
+ //
+ // swagger:ignore
+ Local bool `json:"-"`
}
/*
diff --git a/internal/api/util/errorhandling.go b/internal/api/util/errorhandling.go
index 8bb251040..848beff5b 100644
--- a/internal/api/util/errorhandling.go
+++ b/internal/api/util/errorhandling.go
@@ -50,10 +50,10 @@ func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context) (*api
panic(err)
}
- c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
- "instance": instance,
- "requestID": gtscontext.RequestID(ctx),
- })
+ template404Page(c,
+ instance,
+ gtscontext.RequestID(ctx),
+ )
default:
JSON(c, http.StatusNotFound, map[string]string{
"error": errWithCode.Safe(),
@@ -73,12 +73,12 @@ func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) (
panic(err)
}
- c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
- "instance": instance,
- "code": errWithCode.Code(),
- "error": errWithCode.Safe(),
- "requestID": gtscontext.RequestID(ctx),
- })
+ templateErrorPage(c,
+ instance,
+ errWithCode.Code(),
+ errWithCode.Safe(),
+ gtscontext.RequestID(ctx),
+ )
default:
JSON(c, errWithCode.Code(), map[string]string{
"error": errWithCode.Safe(),
diff --git a/internal/web/opengraph.go b/internal/api/util/opengraph.go
index 66b6c6eea..185dc8132 100644
--- a/internal/web/opengraph.go
+++ b/internal/api/util/opengraph.go
@@ -15,7 +15,7 @@
// 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
+package util
import (
"html"
@@ -28,10 +28,10 @@ import (
const maxOGDescriptionLength = 300
-// ogMeta represents supported OpenGraph Meta tags
+// OGMeta represents supported OpenGraph Meta tags
//
// see eg https://ogp.me/
-type ogMeta struct {
+type OGMeta struct {
// vanilla og tags
Title string // og:title
Type string // og:type
@@ -56,23 +56,23 @@ type ogMeta struct {
ProfileUsername string // profile:username
}
-// ogBase returns an *ogMeta suitable for serving at
+// 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 {
+func OGBase(instance *apimodel.InstanceV1) *OGMeta {
var locale string
if len(instance.Languages) > 0 {
locale = instance.Languages[0]
}
- og := &ogMeta{
+ og := &OGMeta{
Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial",
Type: "website",
Locale: locale,
URL: instance.URI,
SiteName: instance.AccountDomain,
- Description: parseDescription(instance.ShortDescription),
+ Description: ParseDescription(instance.ShortDescription),
Image: instance.Thumbnail,
ImageAlt: instance.ThumbnailDescription,
@@ -81,15 +81,15 @@ func ogBase(instance *apimodel.InstanceV1) *ogMeta {
return og
}
-// withAccount uses the given account to build an ogMeta
+// 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)
+func (og *OGMeta) WithAccount(account *apimodel.Account) *OGMeta {
+ og.Title = AccountTitle(account, og.SiteName)
og.Type = "profile"
og.URL = account.URL
if account.Note != "" {
- og.Description = parseDescription(account.Note)
+ og.Description = ParseDescription(account.Note)
} else {
og.Description = `content="This GoToSocial user hasn't written a bio yet!"`
}
@@ -102,11 +102,11 @@ func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta {
return og
}
-// withStatus uses the given status to build an ogMeta
+// 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)
+func (og *OGMeta) WithStatus(status *apimodel.Status) *OGMeta {
+ og.Title = "Post by " + AccountTitle(status.Account, og.SiteName)
og.Type = "article"
if status.Language != nil {
og.Locale = *status.Language
@@ -114,9 +114,9 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
og.URL = status.URL
switch {
case status.SpoilerText != "":
- og.Description = parseDescription("CW: " + status.SpoilerText)
+ og.Description = ParseDescription("CW: " + status.SpoilerText)
case status.Text != "":
- og.Description = parseDescription(status.Text)
+ og.Description = ParseDescription(status.Text)
default:
og.Description = og.Title
}
@@ -147,34 +147,38 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
return og
}
-// parseTitle parses a page title from account and accountDomain
-func parseTitle(account *apimodel.Account, accountDomain string) string {
+// AccountTitle parses a page title from account and accountDomain
+func AccountTitle(account *apimodel.Account, accountDomain string) string {
user := "@" + account.Acct + "@" + accountDomain
if len(account.DisplayName) == 0 {
return user
}
- return account.DisplayName + " (" + user + ")"
+ return account.DisplayName + ", " + user
}
-// parseDescription returns a string description which is
+// ParseDescription returns a string description which is
// safe to use as a template.HTMLAttr inside templates.
-func parseDescription(in string) string {
+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, `\`, "&bsol;")
- i = trim(i, maxOGDescriptionLength)
+ i = truncate(i, maxOGDescriptionLength)
return `content="` + i + `"`
}
-// trim strings trim s to specified length
-func trim(s string, length int) string {
- if len(s) < length {
+// truncate trims given string to
+// specified length (in runes).
+func truncate(s string, l int) string {
+ r := []rune(s)
+ if len(r) < l {
+ // No need
+ // to trim.
return s
}
- return s[:length]
+ return string(r[:l]) + "..."
}
diff --git a/internal/web/opengraph_test.go b/internal/api/util/opengraph_test.go
index 06e97cdce..2ecd6a740 100644
--- a/internal/web/opengraph_test.go
+++ b/internal/api/util/opengraph_test.go
@@ -15,7 +15,7 @@
// 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
+package util
import (
"fmt"
@@ -40,18 +40,18 @@ func (suite *OpenGraphTestSuite) TestParseDescription() {
for _, tt := range tests {
tt := tt
suite.Run(tt.name, func() {
- suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), parseDescription(tt.in))
+ suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), ParseDescription(tt.in))
})
}
}
func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
- baseMeta := ogBase(&apimodel.InstanceV1{
+ baseMeta := OGBase(&apimodel.InstanceV1{
AccountDomain: "example.org",
Languages: []string{"en"},
})
- accountMeta := baseMeta.withAccount(&apimodel.Account{
+ accountMeta := baseMeta.WithAccount(&apimodel.Account{
Acct: "example_account",
DisplayName: "example person!!",
URL: "https://example.org/@example_account",
@@ -59,8 +59,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
Username: "example_account",
})
- suite.EqualValues(ogMeta{
- Title: "example person!! (@example_account@example.org)",
+ suite.EqualValues(OGMeta{
+ Title: "example person!!, @example_account@example.org",
Type: "profile",
Locale: "en",
URL: "https://example.org/@example_account",
@@ -79,12 +79,12 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
}
func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
- baseMeta := ogBase(&apimodel.InstanceV1{
+ baseMeta := OGBase(&apimodel.InstanceV1{
AccountDomain: "example.org",
Languages: []string{"en"},
})
- accountMeta := baseMeta.withAccount(&apimodel.Account{
+ accountMeta := baseMeta.WithAccount(&apimodel.Account{
Acct: "example_account",
DisplayName: "example person!!",
URL: "https://example.org/@example_account",
@@ -92,8 +92,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
Username: "example_account",
})
- suite.EqualValues(ogMeta{
- Title: "example person!! (@example_account@example.org)",
+ suite.EqualValues(OGMeta{
+ Title: "example person!!, @example_account@example.org",
Type: "profile",
Locale: "en",
URL: "https://example.org/@example_account",
diff --git a/internal/api/util/template.go b/internal/api/util/template.go
new file mode 100644
index 000000000..b8c710c3c
--- /dev/null
+++ b/internal/api/util/template.go
@@ -0,0 +1,135 @@
+// 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 util
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+)
+
+// WebPage encapsulates variables for
+// rendering an HTML template within
+// a standard GtS "page" template.
+type WebPage struct {
+ // Name of the template for rendering
+ // the page. Eg., "example.tmpl".
+ Template string
+
+ // Instance model for rendering header,
+ // footer, and "about" information.
+ Instance *apimodel.InstanceV1
+
+ // OGMeta for rendering page
+ // "meta:og*" tags. Can be nil.
+ OGMeta *OGMeta
+
+ // Paths to CSS files to add to
+ // the page as "stylesheet" entries.
+ // Can be nil.
+ Stylesheets []string
+
+ // Paths to JS files to add to
+ // the page as "script" entries.
+ // Can be nil.
+ Javascript []string
+
+ // Extra parameters to pass to
+ // the template for rendering,
+ // eg., "account": *Account etc.
+ // Can be nil.
+ Extra map[string]any
+}
+
+// TemplateWebPage renders the given HTML template and
+// page params within the standard GtS "page" template.
+//
+// ogMeta, stylesheets, javascript, and any extra
+// properties will be provided to the template if
+// set, but can all be nil.
+func TemplateWebPage(
+ c *gin.Context,
+ page WebPage,
+) {
+ obj := map[string]any{
+ "instance": page.Instance,
+ "ogMeta": page.OGMeta,
+ "stylesheets": page.Stylesheets,
+ "javascript": page.Javascript,
+ }
+
+ for k, v := range page.Extra {
+ obj[k] = v
+ }
+
+ templatePage(c, page.Template, http.StatusOK, obj)
+}
+
+// templateErrorPage renders the given
+// HTTP code, error, and request ID
+// within the standard error template.
+func templateErrorPage(
+ c *gin.Context,
+ instance *apimodel.InstanceV1,
+ code int,
+ err string,
+ requestID string,
+) {
+ const errorTmpl = "error.tmpl"
+
+ obj := map[string]any{
+ "instance": instance,
+ "code": code,
+ "error": err,
+ "requestID": requestID,
+ }
+
+ templatePage(c, errorTmpl, code, obj)
+}
+
+// template404Page renders
+// a standard 404 page.
+func template404Page(
+ c *gin.Context,
+ instance *apimodel.InstanceV1,
+ requestID string,
+) {
+ const notFoundTmpl = "404.tmpl"
+
+ obj := map[string]any{
+ "instance": instance,
+ "requestID": requestID,
+ }
+
+ templatePage(c, notFoundTmpl, http.StatusNotFound, obj)
+}
+
+// render the given template inside
+// "page.tmpl" with the provided
+// code and template object.
+func templatePage(
+ c *gin.Context,
+ template string,
+ code int,
+ obj map[string]any,
+) {
+ const pageTmpl = "page.tmpl"
+ obj["pageContent"] = template
+ c.HTML(code, pageTmpl, obj)
+}
diff --git a/internal/oauth/server.go b/internal/oauth/server.go
index 97e6812c5..3e4519479 100644
--- a/internal/oauth/server.go
+++ b/internal/oauth/server.go
@@ -56,8 +56,8 @@ const (
OOBTokenPath = "/oauth/oob" // #nosec G101 else we get a hardcoded credentials warning
// HelpfulAdvice is a handy hint to users;
// particularly important during the login flow
- HelpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
- HelpfulAdviceGrant = "If you arrived at this error during a login/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client"
+ HelpfulAdvice = "If you arrived at this error during a sign in/oauth flow, please try clearing your session cookies and signing in again; if problems persist, make sure you're using the correct credentials"
+ HelpfulAdviceGrant = "If you arrived at this error during a sign in/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client"
)
// Server wraps some oauth2 server functions in an interface, exposing only what is needed
diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go
index e528c27e0..6ae285f9e 100644
--- a/internal/processing/account/rss_test.go
+++ b/internal/processing/account/rss_test.go
@@ -39,7 +39,7 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
fmt.Println(feed)
- suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" class=\"emoji\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
+ suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
}
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
diff --git a/internal/router/router.go b/internal/router/router.go
index f71dc97ef..b2fb7418e 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -83,7 +83,6 @@ func New(ctx context.Context) (*Router, error) {
// Attach functions used by HTML templating,
// and load HTML templates into the engine.
- LoadTemplateFunctions(engine)
if err := LoadTemplates(engine); err != nil {
return nil, err
}
diff --git a/internal/router/template.go b/internal/router/template.go
index d1f6f297c..981c3fcf4 100644
--- a/internal/router/template.go
+++ b/internal/router/template.go
@@ -18,52 +18,121 @@
package router
import (
+ "bytes"
"fmt"
"html/template"
"os"
"path/filepath"
"reflect"
+ "regexp"
"strings"
"time"
"unsafe"
"github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/render"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/regexes"
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-const (
- justTime = "15:04"
- dateYear = "Jan 02, 2006"
- dateTime = "Jan 02, 15:04"
- dateYearTime = "Jan 02, 2006, 15:04"
- monthYear = "Jan, 2006"
- badTimestamp = "bad timestamp"
-)
-
-// LoadTemplates loads html templates for use by the given engine
+// LoadTemplates loads templates found at `web-template-base-dir`
+// into the Gin engine, or errors if templates cannot be loaded.
+//
+// The special functions "include" and "includeAttr" will be added
+// to the template funcMap for use in any template. Use these "include"
+// functions when you need to pass a template through a pipeline.
+// Otherwise, prefer the built-in "template" function.
func LoadTemplates(engine *gin.Engine) error {
templateBaseDir := config.GetWebTemplateBaseDir()
if templateBaseDir == "" {
- return fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebTemplateBaseDirFlag())
+ return gtserror.Newf(
+ "%s cannot be empty and must be a relative or absolute path",
+ config.WebTemplateBaseDirFlag(),
+ )
}
- templateBaseDir, err := filepath.Abs(templateBaseDir)
+ templateDirAbs, err := filepath.Abs(templateBaseDir)
if err != nil {
- return fmt.Errorf("error getting absolute path of %s: %s", templateBaseDir, err)
+ return gtserror.Newf(
+ "error getting absolute path of web-template-base-dir %s: %w",
+ templateBaseDir, err,
+ )
+ }
+
+ indexTmplPath := filepath.Join(templateDirAbs, "index.tmpl")
+ if _, err := os.Stat(indexTmplPath); err != nil {
+ return gtserror.Newf(
+ "cannot find index.tmpl in web template directory %s: %w",
+ templateDirAbs, err,
+ )
+ }
+
+ // Bring base template into scope.
+ tmpl := template.New("base")
+
+ // Set additional "include" functions to render
+ // provided template name using the base template.
+ funcMap["include"] = func(name string, data any) (template.HTML, error) {
+ var buf strings.Builder
+ err := tmpl.ExecuteTemplate(&buf, name, data)
+
+ // Template was already escaped by
+ // ExecuteTemplate so we can trust it.
+ return noescape(buf.String()), err
}
- if _, err := os.Stat(filepath.Join(templateBaseDir, "index.tmpl")); err != nil {
- return fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err)
+ funcMap["includeAttr"] = func(name string, data any) (template.HTMLAttr, error) {
+ var buf strings.Builder
+ err := tmpl.ExecuteTemplate(&buf, name, data)
+
+ // Template was already escaped by
+ // ExecuteTemplate so we can trust it.
+ return noescapeAttr(buf.String()), err
}
- engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*"))
+ // Load functions into the base template, and
+ // associate other templates with base template.
+ templateGlob := filepath.Join(templateDirAbs, "*")
+ tmpl, err = tmpl.Funcs(funcMap).ParseGlob(templateGlob)
+ if err != nil {
+ return gtserror.Newf("error loading templates: %w", err)
+ }
+
+ // Almost done; teach the
+ // engine how to render.
+ engine.SetFuncMap(funcMap)
+ engine.HTMLRender = render.HTMLProduction{Template: tmpl}
+
return nil
}
+var funcMap = template.FuncMap{
+ "add": add,
+ "acctInstance": acctInstance,
+ "demojify": demojify,
+ "deref": deref,
+ "emojify": emojify,
+ "escape": escape,
+ "increment": increment,
+ "indent": indent,
+ "indentAttr": indentAttr,
+ "isNil": isNil,
+ "outdentPre": outdentPre,
+ "noescapeAttr": noescapeAttr,
+ "noescape": noescape,
+ "oddOrEven": oddOrEven,
+ "subtract": subtract,
+ "timestampPrecise": timestampPrecise,
+ "timestamp": timestamp,
+ "timestampVague": timestampVague,
+ "visibilityIcon": visibilityIcon,
+}
+
func oddOrEven(n int) string {
if n%2 == 0 {
return "even"
@@ -71,21 +140,40 @@ func oddOrEven(n int) string {
return "odd"
}
+// escape HTML escapes the given string,
+// returning a trusted template.
func escape(str string) template.HTML {
/* #nosec G203 */
return template.HTML(template.HTMLEscapeString(str))
}
+// noescape marks the given string as a
+// trusted template. The provided string
+// MUST have already passed through a
+// template or escaping function.
func noescape(str string) template.HTML {
/* #nosec G203 */
return template.HTML(str)
}
+// noescapeAttr marks the given string as a
+// trusted HTML attribute. The provided string
+// MUST have already passed through a template
+// or escaping function.
func noescapeAttr(str string) template.HTMLAttr {
/* #nosec G203 */
return template.HTMLAttr(str)
}
+const (
+ justTime = "15:04"
+ dateYear = "Jan 02, 2006"
+ dateTime = "Jan 02, 15:04"
+ dateYearTime = "Jan 02, 2006, 15:04"
+ monthYear = "Jan, 2006"
+ badTimestamp = "bad timestamp"
+)
+
func timestamp(stamp string) string {
t, err := util.ParseISO8601(stamp)
if err != nil {
@@ -127,38 +215,55 @@ func timestampVague(stamp string) string {
return t.Format(monthYear)
}
-type iconWithLabel struct {
- faIcon string
- label string
-}
-
func visibilityIcon(visibility apimodel.Visibility) template.HTML {
- var icon iconWithLabel
+ var (
+ label string
+ icon string
+ )
switch visibility {
case apimodel.VisibilityPublic:
- icon = iconWithLabel{"globe", "public"}
+ label = "public"
+ icon = "globe"
case apimodel.VisibilityUnlisted:
- icon = iconWithLabel{"unlock", "unlisted"}
+ label = "unlisted"
+ icon = "unlock"
case apimodel.VisibilityPrivate:
- icon = iconWithLabel{"lock", "private"}
+ label = "private"
+ icon = "lock"
case apimodel.VisibilityMutualsOnly:
- icon = iconWithLabel{"handshake-o", "mutuals only"}
+ label = "mutuals-only"
+ icon = "handshake-o"
case apimodel.VisibilityDirect:
- icon = iconWithLabel{"envelope", "direct"}
+ label = "direct"
+ icon = "envelope"
}
/* #nosec G203 */
- return template.HTML(fmt.Sprintf(`<i aria-label="Visibility: %v" class="fa fa-%v"></i>`, icon.label, icon.faIcon))
+ return template.HTML(fmt.Sprintf(
+ `<i aria-label="Visibility: %s" class="fa fa-%s"></i>`,
+ label, icon,
+ ))
}
-// text is a template.HTML to affirm that the input of this function is already escaped
-func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML {
- out := text.Emojify(emojis, string(inputText))
+// emojify replaces emojis in the given
+// html fragment with suitable <img> tags.
+//
+// The provided input must have been
+// escaped / templated already!
+func emojify(
+ emojis []apimodel.Emoji,
+ html template.HTML,
+) template.HTML {
+ return text.EmojifyWeb(emojis, html)
+}
- /* #nosec G203 */
- // (this is escaped above)
- return template.HTML(out)
+// demojify replaces emoji shortcodes in
+// the given fragment with empty strings.
+//
+// Output must then be escaped as appropriate.
+func demojify(input string) string {
+ return text.Demojify(input)
}
func acctInstance(acct string) string {
@@ -170,10 +275,79 @@ func acctInstance(acct string) string {
return ""
}
+// increment adds 1
+// to the given int.
func increment(i int) int {
return i + 1
}
+// add adds n2 to n1.
+func add(n1 int, n2 int) int {
+ return n1 + n2
+}
+
+// subtract subtracts n2 from n1.
+func subtract(n1 int, n2 int) int {
+ return n1 - n2
+}
+
+var (
+ indentRegex = regexp.MustCompile(`(?m)^`)
+ indentStr = " "
+ indentStrLen = len(indentStr)
+ indents = strings.Repeat(indentStr, 12)
+ indentPre = regexp.MustCompile(fmt.Sprintf(`(?Ums)^((?:%s)+)<pre>.*</pre>`, indentStr))
+)
+
+// indent appropriately indents the given html
+// by prepending each line with the indentStr.
+func indent(n int, html template.HTML) template.HTML {
+ out := indentRegex.ReplaceAllString(
+ string(html),
+ indents[:n*indentStrLen],
+ )
+ return noescape(out)
+}
+
+// indentAttr appropriately indents the given html
+// attribute by prepending each line with the indentStr.
+func indentAttr(n int, html template.HTMLAttr) template.HTMLAttr {
+ out := indentRegex.ReplaceAllString(
+ string(html),
+ indents[:n*indentStrLen],
+ )
+ return noescapeAttr(out)
+}
+
+// outdentPre outdents all `<pre></pre>` tags in the
+// given HTML so that they render correctly in code
+// blocks, even if they were indented before.
+func outdentPre(html template.HTML) template.HTML {
+ input := string(html)
+ output := regexes.ReplaceAllStringFunc(indentPre, input,
+ func(match string, buf *bytes.Buffer) string {
+ // Reuse the regex to pull out submatches.
+ matches := indentPre.FindAllStringSubmatch(match, -1)
+ if len(matches) != 1 {
+ return match
+ }
+
+ var (
+ indented = matches[0][0]
+ indent = matches[0][1]
+ )
+
+ // Outdent everything in the inner match, add
+ // a newline at the end to make it a bit neater.
+ outdented := strings.ReplaceAll(indented, indent, "")
+
+ // Replace original match with the outdented version.
+ return strings.ReplaceAll(match, indented, outdented)
+ },
+ )
+ return noescape(output)
+}
+
// isNil will safely check if 'v' is nil without
// dealing with weird Go interface nil bullshit.
func isNil(i interface{}) bool {
@@ -193,21 +367,3 @@ func deref(i any) any {
return vOf.Elem()
}
-
-func LoadTemplateFunctions(engine *gin.Engine) {
- engine.SetFuncMap(template.FuncMap{
- "escape": escape,
- "noescape": noescape,
- "noescapeAttr": noescapeAttr,
- "oddOrEven": oddOrEven,
- "visibilityIcon": visibilityIcon,
- "timestamp": timestamp,
- "timestampVague": timestampVague,
- "timestampPrecise": timestampPrecise,
- "emojify": emojify,
- "acctInstance": acctInstance,
- "increment": increment,
- "isNil": isNil,
- "deref": deref,
- })
-}
diff --git a/internal/router/template_test.go b/internal/router/template_test.go
new file mode 100644
index 000000000..19bf759e0
--- /dev/null
+++ b/internal/router/template_test.go
@@ -0,0 +1,204 @@
+// 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 router
+
+import (
+ "html/template"
+ "testing"
+)
+
+func TestOutdentPre(t *testing.T) {
+ const html = template.HTML(`
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+ <pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;should you follow me?&lt;/dt&gt;
+ &lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;age&lt;/dt&gt;
+ &lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;/section&gt;
+ </code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+ <pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;should you follow me?&lt;/dt&gt;
+ &lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;age&lt;/dt&gt;
+ &lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;/section&gt;
+ </code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+`)
+
+ const expected = template.HTML(`
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+<pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;should you follow me?&lt;/dt&gt;
+&lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;age&lt;/dt&gt;
+&lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+&lt;/section&gt;
+</code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+<pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;should you follow me?&lt;/dt&gt;
+&lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;age&lt;/dt&gt;
+&lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+&lt;/section&gt;
+</code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+`)
+
+ out := outdentPre(html)
+ if out != expected {
+ t.Fatalf("unexpected output:\n`%s`\n", out)
+ }
+}
diff --git a/internal/text/emojify.go b/internal/text/emojify.go
index 2100d5e81..23730eaf9 100644
--- a/internal/text/emojify.go
+++ b/internal/text/emojify.go
@@ -20,18 +20,76 @@ package text
import (
"bytes"
"html"
+ "html/template"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/regexes"
)
-// Emojify replaces shortcodes in `inputText` with the emoji in `emojis`.
-//
-// Callers should ensure that inputText and resulting text are escaped
-// appropriately depending on what they're used for.
-func Emojify(emojis []apimodel.Emoji, inputText string) string {
- emojisMap := make(map[string]apimodel.Emoji, len(emojis))
+// EmojifyWeb replaces emoji shortcodes like `:example:` in the given HTML
+// fragment with `<img>` tags suitable for rendering on the web frontend.
+func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML {
+ out := emojify(
+ emojis,
+ string(html),
+ func(url, code string, buf *bytes.Buffer) {
+ buf.WriteString(`<img src="`)
+ buf.WriteString(url)
+ buf.WriteString(`" title=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" alt=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" class="emoji" `)
+ // Lazy load emojis when
+ // they scroll into view.
+ buf.WriteString(`loading="lazy" `)
+ // Limit size to avoid showing
+ // huge emojis when unstyled.
+ buf.WriteString(`width="25" height="25"/>`)
+ },
+ )
+
+ // If input was safe,
+ // we can trust output.
+ return template.HTML(out) // #nosec G203
+}
+// EmojifyRSS replaces emoji shortcodes like `:example:` in the given text
+// fragment with `<img>` tags suitable for rendering as RSS content.
+func EmojifyRSS(emojis []apimodel.Emoji, text string) string {
+ return emojify(
+ emojis,
+ text,
+ func(url, code string, buf *bytes.Buffer) {
+ buf.WriteString(`<img src="`)
+ buf.WriteString(url)
+ buf.WriteString(`" title=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" alt=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" `)
+ // Limit size to avoid showing
+ // huge emojis in RSS readers.
+ buf.WriteString(`width="25" height="25"/>`)
+ },
+ )
+}
+
+// Demojify replaces emoji shortcodes like `:example:` in the given text
+// fragment with empty strings, essentially stripping them from the text.
+// This is useful for text used in OG Meta headers.
+func Demojify(text string) string {
+ return regexes.EmojiFinder.ReplaceAllString(text, "")
+}
+
+func emojify(
+ emojis []apimodel.Emoji,
+ input string,
+ write func(url, code string, buf *bytes.Buffer),
+) string {
+ // Build map of shortcodes. Normalize each
+ // shortcode by readding closing colons.
+ emojisMap := make(map[string]apimodel.Emoji, len(emojis))
for _, emoji := range emojis {
shortcode := ":" + emoji.Shortcode + ":"
emojisMap[shortcode] = emoji
@@ -39,27 +97,20 @@ func Emojify(emojis []apimodel.Emoji, inputText string) string {
return regexes.ReplaceAllStringFunc(
regexes.EmojiFinder,
- inputText,
+ input,
func(shortcode string, buf *bytes.Buffer) string {
- // Look for emoji according to this shortcode
+ // Look for emoji with this shortcode.
emoji, ok := emojisMap[shortcode]
if !ok {
return shortcode
}
- // Escape raw emoji content
- safeURL := html.EscapeString(emoji.URL)
- safeCode := html.EscapeString(emoji.Shortcode)
-
- // Write HTML emoji repr to buffer
- buf.WriteString(`<img src="`)
- buf.WriteString(safeURL)
- buf.WriteString(`" title=":`)
- buf.WriteString(safeCode)
- buf.WriteString(`:" alt=":`)
- buf.WriteString(safeCode)
- buf.WriteString(`:" class="emoji"/>`)
+ // Escape raw emoji content.
+ url := html.EscapeString(emoji.URL)
+ code := html.EscapeString(emoji.Shortcode)
+ // Write emoji repr to buffer.
+ write(url, code, buf)
return buf.String()
},
)
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 09d538a3d..9d1f7edeb 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -662,6 +662,10 @@ func (c *Converter) StatusToWebStatus(
return nil, err
}
+ // Whack a newline before and after each "pre" to make it easier to outdent it.
+ webStatus.Content = strings.ReplaceAll(webStatus.Content, "<pre>", "\n<pre>")
+ webStatus.Content = strings.ReplaceAll(webStatus.Content, "</pre>", "</pre>\n")
+
// Add additional information for template.
// Assume empty langs, hope for not empty language.
webStatus.LanguageTag = new(language.Language)
@@ -727,6 +731,8 @@ func (c *Converter) StatusToWebStatus(
a.Sensitive = webStatus.Sensitive
}
+ webStatus.Local = *s.Local
+
return webStatus, nil
}
diff --git a/internal/typeutils/internaltorss.go b/internal/typeutils/internaltorss.go
index e70b11aae..e1174caf6 100644
--- a/internal/typeutils/internaltorss.go
+++ b/internal/typeutils/internaltorss.go
@@ -151,7 +151,7 @@ func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*f
apiEmojis = append(apiEmojis, apiEmoji)
}
}
- content := text.Emojify(apiEmojis, s.Content)
+ content := text.EmojifyRSS(apiEmojis, s.Content)
return &feeds.Item{
Title: title,
diff --git a/internal/typeutils/internaltorss_test.go b/internal/typeutils/internaltorss_test.go
index ea6f3cc93..305015d28 100644
--- a/internal/typeutils/internaltorss_test.go
+++ b/internal/typeutils/internaltorss_test.go
@@ -81,7 +81,7 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem2() {
suite.Equal("62529", item.Enclosure.Length)
suite.Equal("image/jpeg", item.Enclosure.Type)
suite.Equal("http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", item.Enclosure.Url)
- suite.Equal("hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" class=\"emoji\"/> !", item.Content)
+ suite.Equal("hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !", item.Content)
}
func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() {
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/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)