Refactor more code in templates (#29236)

Follow #29165.

* Introduce JSONTemplate to help to render JSON templates
* Introduce JSEscapeSafe for templates. Now only use `{{ ... |
JSEscape}}` instead of `{{ ... | JSEscape | Safe}}`
* Simplify "UserLocationMapURL" useage

(cherry picked from commit 31bb9f3247388b993c61a10190cfd512408ce57e)
This commit is contained in:
wxiaoguang 2024-02-18 17:52:02 +08:00 committed by Earl Warren
parent bdf470785d
commit 81925ebb0c
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
11 changed files with 42 additions and 39 deletions

View file

@ -156,8 +156,8 @@ endif
FORGEJO_API_SPEC := public/assets/forgejo/api.v1.yml FORGEJO_API_SPEC := public/assets/forgejo/api.v1.yml
SWAGGER_SPEC := templates/swagger/v1_json.tmpl SWAGGER_SPEC := templates/swagger/v1_json.tmpl
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g
SWAGGER_EXCLUDE := code.gitea.io/sdk SWAGGER_EXCLUDE := code.gitea.io/sdk
SWAGGER_NEWLINE_COMMAND := -e '$$a\' SWAGGER_NEWLINE_COMMAND := -e '$$a\'
SWAGGER_SPEC_BRANDING := s|Gitea API|Forgejo API|g SWAGGER_SPEC_BRANDING := s|Gitea API|Forgejo API|g

View file

@ -90,6 +90,20 @@ func (ctx *Context) HTML(status int, name base.TplName) {
} }
} }
// JSONTemplate renders the template as JSON response
// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
func (ctx *Context) JSONTemplate(tmpl base.TplName) {
t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
if err != nil {
ctx.ServerError("unable to find template", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/json")
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
ctx.ServerError("unable to execute template", err)
}
}
// RenderToString renders the template content to a string // RenderToString renders the template content to a string
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) { func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
var buf strings.Builder var buf strings.Builder

View file

@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap {
"Safe": Safe, "Safe": Safe,
"Escape": Escape, "Escape": Escape,
"QueryEscape": url.QueryEscape, "QueryEscape": url.QueryEscape,
"JSEscape": template.JSEscapeString, "JSEscape": JSEscapeSafe,
"Str2html": Str2html, // TODO: rename it to SanitizeHTML "Str2html": Str2html, // TODO: rename it to SanitizeHTML
"URLJoin": util.URLJoin, "URLJoin": util.URLJoin,
"DotEscape": DotEscape, "DotEscape": DotEscape,
@ -214,6 +214,10 @@ func Escape(s any) template.HTML {
panic(fmt.Sprintf("unexpected type %T", s)) panic(fmt.Sprintf("unexpected type %T", s))
} }
func JSEscapeSafe(s string) template.HTML {
return template.HTML(template.JSEscapeString(s))
}
func RenderEmojiPlain(s any) any { func RenderEmojiPlain(s any) any {
switch v := s.(type) { switch v := s.(type) {
case string: case string:

View file

@ -52,3 +52,7 @@ func TestSubjectBodySeparator(t *testing.T) {
"", "",
"Insuficient\n--\nSeparators") "Insuficient\n--\nSeparators")
} }
func TestJSEscapeSafe(t *testing.T) {
assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`))
}

View file

@ -8,7 +8,7 @@
// //
// Schemes: https, http // Schemes: https, http
// BasePath: /api/v1 // BasePath: /api/v1
// Version: {{AppVer | JSEscape | Safe}} // Version: {{AppVer | JSEscape}}
// License: MIT http://opensource.org/licenses/MIT // License: MIT http://opensource.org/licenses/MIT
// //
// Consumes: // Consumes:

View file

@ -579,16 +579,8 @@ func GrantApplicationOAuth(ctx *context.Context) {
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) { func OIDCWellKnown(ctx *context.Context) {
t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil)
if err != nil {
ctx.ServerError("unable to find template", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/json")
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
if err = t.Execute(ctx.Resp, ctx.Data); err != nil { ctx.JSONTemplate("user/auth/oidc_wellknown")
ctx.ServerError("unable to execute template", err)
}
} }
// OIDCKeys generates the JSON Web Key Set // OIDCKeys generates the JSON Web Key Set

View file

@ -4,6 +4,8 @@
package user package user
import ( import (
"net/url"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
@ -37,7 +39,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
ctx.Data["IsBlocked"] = ctx.Doer != nil && user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID) ctx.Data["IsBlocked"] = ctx.Doer != nil && user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
// Show OpenID URIs // Show OpenID URIs
openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID) openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)

View file

@ -4,22 +4,10 @@
package web package web
import ( import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
) )
// tplSwaggerV1Json swagger v1 json template
const tplSwaggerV1Json base.TplName = "swagger/v1_json"
// SwaggerV1Json render swagger v1 json // SwaggerV1Json render swagger v1 json
func SwaggerV1Json(ctx *context.Context) { func SwaggerV1Json(ctx *context.Context) {
t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil) ctx.JSONTemplate("swagger/v1_json")
if err != nil {
ctx.ServerError("unable to find template", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/json")
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
ctx.ServerError("unable to execute template", err)
}
} }

View file

@ -31,9 +31,8 @@
<li> <li>
{{svg "octicon-location"}} {{svg "octicon-location"}}
<span class="gt-f1">{{.ContextUser.Location}}</span> <span class="gt-f1">{{.ContextUser.Location}}</span>
{{if .UserLocationMapURL}} {{if .ContextUserLocationMapURL}}
{{/* We presume that the UserLocationMapURL is safe, as it is provided by the site administrator. */}} <a href="{{.ContextUserLocationMapURL}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
<a href="{{.UserLocationMapURL | Safe}}{{.ContextUser.Location | QueryEscape}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
{{svg "octicon-link-external"}} {{svg "octicon-link-external"}}
</a> </a>
{{end}} {{end}}

View file

@ -19,9 +19,9 @@
"name": "MIT", "name": "MIT",
"url": "http://opensource.org/licenses/MIT" "url": "http://opensource.org/licenses/MIT"
}, },
"version": "{{AppVer | JSEscape | Safe}}" "version": "{{AppVer | JSEscape}}"
}, },
"basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1", "basePath": "{{AppSubUrl | JSEscape}}/api/v1",
"paths": { "paths": {
"/activitypub/user-id/{user-id}": { "/activitypub/user-id/{user-id}": {
"get": { "get": {

View file

@ -1,16 +1,16 @@
{ {
"issuer": "{{AppUrl | JSEscape | Safe}}", "issuer": "{{AppUrl | JSEscape}}",
"authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize", "authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize",
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token", "token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token",
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys", "jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys",
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo", "userinfo_endpoint": "{{AppUrl | JSEscape}}login/oauth/userinfo",
"introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect", "introspection_endpoint": "{{AppUrl | JSEscape}}login/oauth/introspect",
"response_types_supported": [ "response_types_supported": [
"code", "code",
"id_token" "id_token"
], ],
"id_token_signing_alg_values_supported": [ "id_token_signing_alg_values_supported": [
"{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}" "{{.SigningKey.SigningMethod.Alg | JSEscape}}"
], ],
"subject_types_supported": [ "subject_types_supported": [
"public" "public"