diff --git a/modules/markup/camo.go b/modules/markup/camo.go index 7e2583469d..f07d62d4f9 100644 --- a/modules/markup/camo.go +++ b/modules/markup/camo.go @@ -11,7 +11,6 @@ import ( "strings" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) // CamoEncode encodes a lnk to fit with the go-camo and camo proxy links. The purposes of camo-proxy are: @@ -27,7 +26,7 @@ func CamoEncode(link string) string { macSum := b64encode(mac.Sum(nil)) encodedURL := b64encode([]byte(link)) - return util.URLJoin(setting.Camo.ServerURL, macSum, encodedURL) + return strings.TrimSuffix(setting.Camo.ServerURL, "/") + "/" + macSum + "/" + encodedURL } func b64encode(data []byte) string { diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index c319374a38..a2c5160674 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -4,13 +4,13 @@ package markup import ( + "fmt" "slices" "strings" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/references" - "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -219,7 +219,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { continue } - link := "/:root/" + util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash) + link := fmt.Sprintf("/:root/%s/%s/commit/%s", ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], hash) replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) start = 0 node = node.NextSibling.NextSibling @@ -236,7 +236,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { } refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - linkHref := "/:root/" + util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha) + linkHref := fmt.Sprintf("/:root/%s/%s/commit/%s", ref.Owner, ref.Name, ref.CommitSha) link := createLink(ctx, linkHref, refText, "commit") replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index 85bec5db20..a94abb3883 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -4,6 +4,7 @@ package markup import ( + "fmt" "strconv" "strings" @@ -162,7 +163,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) issuePath := util.Iif(ref.IsPull, "pulls", "issues") - linkHref := "/:root/" + util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue) + linkHref := fmt.Sprintf("/:root/%s/%s/%s/%s", issueOwner, issueRepo, issuePath, ref.Issue) // at the moment, only render the issue index in a full line (or simple line) as icon+title // otherwise it would be too noisy for "take #1 as an example" in a sentence diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 1702950da8..c84e0b90d8 100644 --- a/modules/markup/html_link.go +++ b/modules/markup/html_link.go @@ -113,16 +113,17 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { } childNode.Parent = linkNode absoluteLink := IsFullURLString(link) - if !absoluteLink { + // FIXME: it should be fully refactored in the future, it uses various hacky approaches to guess how to encode a path for wiki + // When a link contains "/", then we assume that the user has provided a well-encoded link. + if !absoluteLink && !strings.Contains(link, "/") { + // So only guess for links without "/". if image { link = strings.ReplaceAll(link, " ", "+") } else { // the hacky wiki name encoding: space to "-" link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-" } - if !strings.Contains(link, "/") { - link = url.PathEscape(link) // FIXME: it doesn't seem right and it might cause double-escaping - } + link = url.PathEscape(link) } if image { title := props["title"] diff --git a/modules/markup/html_mention.go b/modules/markup/html_mention.go index f97c034cf3..00cd51ca94 100644 --- a/modules/markup/html_mention.go +++ b/modules/markup/html_mention.go @@ -4,6 +4,7 @@ package markup import ( + "fmt" "strings" "code.gitea.io/gitea/modules/references" @@ -26,14 +27,11 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { loc.End += start mention := node.Data[loc.Start:loc.End] teams, ok := ctx.RenderOptions.Metas["teams"] - // FIXME: util.URLJoin may not be necessary here: - // - setting.AppURL is defined to have a terminal '/' so unless mention[1:] - // is an AppSubURL link we can probably fallback to concatenation. - // team mention should follow @orgName/teamName style + if ok && strings.Contains(mention, "/") { mentionOrgAndTeam := strings.Split(mention, "/") if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { - link := "/:root/" + util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]) + link := fmt.Sprintf("/:root/org/%s/teams/%s", ctx.RenderOptions.Metas["org"], mentionOrgAndTeam[1]) replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) node = node.NextSibling.NextSibling start = 0 diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 5f873d2985..62c4ae3e0a 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -389,7 +389,7 @@ func TestRender_ShortLinks(t *testing.T) { imgurl := util.URLJoin(tree, "Link.jpg") otherImgurl := util.URLJoin(tree, "Link+Other.jpg") encodedImgurl := util.URLJoin(tree, "Link+%23.jpg") - notencodedImgurl := util.URLJoin(tree, "some", "path", "Link+#.jpg") + notencodedImgurl := util.URLJoin(tree, "some", "path", "Link%20#.jpg") renderableFileURL := util.URLJoin(tree, "markdown_file.md") unrenderableFileURL := util.URLJoin(tree, "file.zip") favicon := "http://google.com/favicon.ico" @@ -466,6 +466,8 @@ func TestRender_ShortLinks(t *testing.T) { "[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]", `
`, ) + // FIXME: it's unable to resolve: [[link?k=v]] + // FIXME: it is a wrong test case, it is not an image, but a link with anchor "#.jpg" test( "[[some/path/Link #.jpg]]", ``, diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 8d6b3b3c80..261c4e780c 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -23,7 +22,6 @@ const ( AppURL = "http://localhost:3000/" testRepoOwnerName = "user13" testRepoName = "repo11" - FullURL = AppURL + testRepoOwnerName + "/" + testRepoName + "/" ) // these values should match the const above @@ -47,8 +45,9 @@ func TestRender_StandardLinks(t *testing.T) { func TestRender_Images(t *testing.T) { setting.AppURL = AppURL + const baseLink = "http://localhost:3000/user13/repo11" render := func(input, expected string) { - buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input) + buffer, err := markdown.RenderString(markup.NewTestRenderContext(baseLink), input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) } @@ -56,7 +55,7 @@ func TestRender_Images(t *testing.T) { url := "../../.images/src/02/train.jpg" title := "Train" href := "https://gitea.io" - result := util.URLJoin(FullURL, url) + result := baseLink + "/.images/src/02/train.jpg" // resolved link should not go out of the base link // hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now render( @@ -88,6 +87,7 @@ func TestRender_Images(t *testing.T) { } func TestTotal_RenderString(t *testing.T) { + const FullURL = AppURL + testRepoOwnerName + "/" + testRepoName + "/" setting.AppURL = AppURL defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() diff --git a/modules/markup/render_link.go b/modules/markup/render_link.go index 9cc83095ff..c3a95622ac 100644 --- a/modules/markup/render_link.go +++ b/modules/markup/render_link.go @@ -5,28 +5,47 @@ package markup import ( "context" + "net/url" + "path" "strings" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) +// resolveLinkRelative tries to resolve the link relative to the "{base}/{cur}", and returns the final link. +// It only resolves the link, doesn't do any sanitization or validation, invalid links will be returned as is. func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute bool) (finalLink string) { - if IsFullURLString(link) { - return link + linkURL, err := url.Parse(link) + if err != nil { + return link // invalid URL, return as is } + if linkURL.Scheme != "" || linkURL.Host != "" { + return link // absolute URL, return as is + } + if strings.HasPrefix(link, "/") { if strings.HasPrefix(link, base) && strings.Count(base, "/") >= 4 { - // a trick to tolerate that some users were using absolute paths (the old gitea's behavior) + // a trick to tolerate that some users were using absolute paths (the old Gitea's behavior) + // if the link is likely "{base}/src/main" while "{base}" is something like "/owner/repo" finalLink = link } else { - finalLink = util.URLJoin(base, "./", link) + // need to resolve the link relative to "{base}" + cur = "" + } + } // else: link is relative to "{base}/{cur}" + + if finalLink == "" { + finalLink = strings.TrimSuffix(base, "/") + path.Join("/"+cur, "/"+linkURL.EscapedPath()) + finalLink = strings.TrimSuffix(finalLink, "/") + if linkURL.RawQuery != "" { + finalLink += "?" + linkURL.RawQuery + } + if linkURL.Fragment != "" { + finalLink += "#" + linkURL.Fragment } - } else { - finalLink = util.URLJoin(base, "./", cur, link) } - finalLink = strings.TrimSuffix(finalLink, "/") + if absolute { finalLink = httplib.MakeAbsoluteURL(ctx, finalLink) } diff --git a/modules/markup/render_link_test.go b/modules/markup/render_link_test.go index 972e15308c..045b728c8d 100644 --- a/modules/markup/render_link_test.go +++ b/modules/markup/render_link_test.go @@ -18,8 +18,16 @@ func TestResolveLinkRelative(t *testing.T) { assert.Equal(t, "/a/b", resolveLinkRelative(ctx, "/a", "b", "", false)) assert.Equal(t, "/a/b/c", resolveLinkRelative(ctx, "/a", "b", "c", false)) assert.Equal(t, "/a/c", resolveLinkRelative(ctx, "/a", "b", "/c", false)) + assert.Equal(t, "/a/c#id", resolveLinkRelative(ctx, "/a", "b", "/c#id", false)) + assert.Equal(t, "/a/%2f?k=/", resolveLinkRelative(ctx, "/a", "b", "/%2f/?k=/", false)) + assert.Equal(t, "/a/b/c?k=v#id", resolveLinkRelative(ctx, "/a", "b", "c/?k=v#id", false)) + assert.Equal(t, "%invalid", resolveLinkRelative(ctx, "/a", "b", "%invalid", false)) assert.Equal(t, "http://localhost:3000/a", resolveLinkRelative(ctx, "/a", "", "", true)) + // absolute link is returned as is + assert.Equal(t, "mailto:user@domain.com", resolveLinkRelative(ctx, "/a", "", "mailto:user@domain.com", false)) + assert.Equal(t, "http://other/path/", resolveLinkRelative(ctx, "/a", "", "http://other/path/", false)) + // some users might have used absolute paths a lot, so if the prefix overlaps and has enough slashes, we should tolerate it assert.Equal(t, "/owner/repo/foo/owner/repo/foo/bar/xxx", resolveLinkRelative(ctx, "/owner/repo/foo", "", "/owner/repo/foo/bar/xxx", false)) assert.Equal(t, "/owner/repo/foo/bar/xxx", resolveLinkRelative(ctx, "/owner/repo/foo/bar", "", "/owner/repo/foo/bar/xxx", false)) diff --git a/modules/recaptcha/recaptcha.go b/modules/recaptcha/recaptcha.go index 1777d169c1..224fa38eee 100644 --- a/modules/recaptcha/recaptcha.go +++ b/modules/recaptcha/recaptcha.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) // Response is the structure of JSON returned from API @@ -24,17 +23,16 @@ type Response struct { ErrorCodes []ErrorCode `json:"error-codes"` } -const apiURL = "api/siteverify" - // Verify calls Google Recaptcha API to verify token func Verify(ctx context.Context, response string) (bool, error) { post := url.Values{ "secret": {setting.Service.RecaptchaSecret}, "response": {response}, } + + reqURL := strings.TrimSuffix(setting.Service.RecaptchaURL, "/") + "/api/siteverify" // Basically a copy of http.PostForm, but with a context - req, err := http.NewRequestWithContext(ctx, http.MethodPost, - util.URLJoin(setting.Service.RecaptchaURL, apiURL), strings.NewReader(post.Encode())) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(post.Encode())) if err != nil { return false, fmt.Errorf("Failed to create CAPTCHA request: %w", err) } diff --git a/modules/setting/server.go b/modules/setting/server.go index 50a38f544b..7e7611b802 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" ) // Scheme describes protocol types @@ -163,7 +162,7 @@ func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte { } // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash -func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { +func MakeAbsoluteAssetURL(appURL *url.URL, staticURLPrefix string) string { parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) if err != nil { log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err) @@ -171,11 +170,12 @@ func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { if err == nil && parsedPrefix.Hostname() == "" { if staticURLPrefix == "" { - return strings.TrimSuffix(appURL, "/") + return strings.TrimSuffix(appURL.String(), "/") } // StaticURLPrefix is just a path - return util.URLJoin(appURL, strings.TrimSuffix(staticURLPrefix, "/")) + appHostURL := &url.URL{Scheme: appURL.Scheme, Host: appURL.Host} + return appHostURL.String() + "/" + strings.Trim(staticURLPrefix, "/") } return strings.TrimSuffix(staticURLPrefix, "/") @@ -316,7 +316,7 @@ func loadServerFrom(rootCfg ConfigProvider) { Domain = urlHostname } - AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix) + AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix) AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed) manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) diff --git a/modules/setting/setting_test.go b/modules/setting/setting_test.go index f77ee65974..13575f52a6 100644 --- a/modules/setting/setting_test.go +++ b/modules/setting/setting_test.go @@ -4,6 +4,7 @@ package setting import ( + "net/url" "testing" "code.gitea.io/gitea/modules/json" @@ -12,18 +13,26 @@ import ( ) func TestMakeAbsoluteAssetURL(t *testing.T) { - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234", "https://localhost:2345")) - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345")) - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345/")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo/")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/foo/")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo", "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar/")) + appURL1, _ := url.Parse("https://localhost:1234") + appURL2, _ := url.Parse("https://localhost:1234/") + appURLSub1, _ := url.Parse("https://localhost:1234/foo") + appURLSub2, _ := url.Parse("https://localhost:1234/foo/") + + // static URL is an absolute URL, so should be used + assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345")) + assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345/")) + + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL2, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo/")) + + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub2, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo/")) + + assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar")) + assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub2, "/bar")) + assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar/")) } func TestMakeManifestData(t *testing.T) { diff --git a/modules/templates/helper.go b/modules/templates/helper.go index c1ee88fc84..d2d4d364df 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -37,7 +37,6 @@ func NewFuncMap() template.FuncMap { "QueryEscape": queryEscape, "QueryBuild": QueryBuild, "SanitizeHTML": SanitizeHTML, - "URLJoin": util.URLJoin, "DotEscape": dotEscape, "PathEscape": url.PathEscape, diff --git a/modules/util/url.go b/modules/util/url.go index 62370339c8..6455b0b75c 100644 --- a/modules/util/url.go +++ b/modules/util/url.go @@ -20,6 +20,8 @@ func PathEscapeSegments(path string) string { } // URLJoin joins url components, like path.Join, but preserving contents +// Deprecated: it has unclear behaviors, should not be used anymore. It is only used in some tests. +// Need to be removed in the future. func URLJoin(base string, elems ...string) string { if !strings.HasSuffix(base, "/") { base += "/" diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index fbc3f9f07d..1219690200 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -45,6 +45,33 @@ const ( TplActivatePrompt templates.TplName = "user/auth/activate_prompt" // for showing a message for user activation ) +type CommonAuthOptions struct { + EnableCaptcha bool +} + +func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) { + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth + + // for OpenID Connect + ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp + ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + + if opt.EnableCaptcha { + ctx.Data["EnableCaptcha"] = true + ctx.Data["RecaptchaAPIScriptURL"] = strings.TrimSuffix(setting.Service.RecaptchaURL, "/") + "/api.js" + ctx.Data["CaptchaType"] = setting.Service.CaptchaType + ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey + ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey + ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey + ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL + ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey + if setting.Service.CaptchaType == setting.ImageCaptcha { + ctx.Data["Captcha"] = context.GetImageCaptcha() + } + } +} + // autoSignIn reads cookie and try to auto-login. func autoSignIn(ctx *context.Context) (bool, error) { isSucceed := false @@ -199,12 +226,10 @@ func prepareSignInPageData(ctx *context.Context) { ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsLogin"] = true ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) - ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm - ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth - if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { - context.SetCaptchaData(ctx) - } + prepareCommonAuthPageData(ctx, CommonAuthOptions{ + EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin, + }) } // SignIn render sign in page @@ -442,50 +467,51 @@ func buildSignOutRedirectURL(ctx *context.Context) string { return setting.AppSubURL + "/" } -// SignUp render the register page -func SignUp(ctx *context.Context) { +func prepareSignUpPageData(ctx *context.Context) bool { ctx.Data["Title"] = ctx.Tr("sign_up") ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" + ctx.Data["PageIsSignUp"] = true - hasUsers, _ := user_model.HasUsers(ctx) + hasUsers, err := user_model.HasUsers(ctx) + if err != nil { + ctx.ServerError("HasUsers", err) + return false + } ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { - ctx.ServerError("UserSignUp", err) - return + ctx.ServerError("GetOAuth2Providers", err) + return false } - ctx.Data["OAuth2Providers"] = oauth2Providers - context.SetCaptchaData(ctx) - ctx.Data["PageIsSignUp"] = true + prepareCommonAuthPageData(ctx, CommonAuthOptions{ + EnableCaptcha: setting.Service.EnableCaptcha, + }) // Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration - rememberAuthRedirectLink(ctx) + return true +} +// SignUp render the register page +func SignUp(ctx *context.Context) { + if !prepareSignUpPageData(ctx) { + return + } + rememberAuthRedirectLink(ctx) ctx.HTML(http.StatusOK, tplSignUp) } // SignUpPost response for sign up information submission func SignUpPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.RegisterForm) - ctx.Data["Title"] = ctx.Tr("sign_up") - - ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" - - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) - if err != nil { - ctx.ServerError("UserSignUp", err) + if !prepareSignUpPageData(ctx) { return } - ctx.Data["OAuth2Providers"] = oauth2Providers - context.SetCaptchaData(ctx) - - ctx.Data["PageIsSignUp"] = true + form := web.GetForm(ctx).(*forms.RegisterForm) // Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration { diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index faa712471f..02e1b7acd2 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -24,30 +24,27 @@ import ( var tplLinkAccount templates.TplName = "user/auth/link_account" -// LinkAccount shows the page where the user can decide to login or create a new account -func LinkAccount(ctx *context.Context) { - // FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again. +func prepareLinkAccountPageData(ctx *context.Context) { + // TODO Make insecure passwords optional for local accounts also, once email-based Second-Factor Auth is available ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration + ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha - ctx.Data["Captcha"] = context.GetImageCaptcha() - ctx.Data["CaptchaType"] = setting.Service.CaptchaType - ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL - ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey - ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey - ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey - ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL - ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey - ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration - ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration - ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm - ctx.Data["ShowRegistrationButton"] = false - ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth // use this to set the right link into the signIn and signUp templates in the link_account template ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" + ctx.Data["ShowRegistrationButton"] = false + ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + + prepareCommonAuthPageData(ctx, CommonAuthOptions{ + EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha, + }) +} + +// LinkAccount shows the page where the user can decide to login or create a new account +func LinkAccount(ctx *context.Context) { + prepareLinkAccountPageData(ctx) linkAccountData := oauth2GetLinkAccountData(ctx) @@ -126,28 +123,10 @@ func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl // LinkAccountPostSignIn handle the coupling of external account with another account using signIn func LinkAccountPostSignIn(ctx *context.Context) { signInForm := web.GetForm(ctx).(*forms.SignInForm) - ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration - ctx.Data["Title"] = ctx.Tr("link_account") - ctx.Data["LinkAccountMode"] = true - ctx.Data["LinkAccountModeSignIn"] = true - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha - ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL - ctx.Data["Captcha"] = context.GetImageCaptcha() - ctx.Data["CaptchaType"] = setting.Service.CaptchaType - ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey - ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey - ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey - ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL - ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey - ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration - ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration - ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm - ctx.Data["ShowRegistrationButton"] = false - ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth - // use this to set the right link into the signIn and signUp templates in the link_account template - ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" - ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" + ctx.Data["LinkAccountModeSignIn"] = true + + prepareLinkAccountPageData(ctx) linkAccountData := oauth2GetLinkAccountData(ctx) if linkAccountData == nil { @@ -218,30 +197,10 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData // LinkAccountPostRegister handle the creation of a new account for an external account using signUp func LinkAccountPostRegister(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RegisterForm) - // TODO Make insecure passwords optional for local accounts also, - // once email-based Second-Factor Auth is available - ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration - ctx.Data["Title"] = ctx.Tr("link_account") - ctx.Data["LinkAccountMode"] = true - ctx.Data["LinkAccountModeRegister"] = true - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha - ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL - ctx.Data["Captcha"] = context.GetImageCaptcha() - ctx.Data["CaptchaType"] = setting.Service.CaptchaType - ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey - ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey - ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey - ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL - ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey - ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration - ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration - ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm - ctx.Data["ShowRegistrationButton"] = false - ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth - // use this to set the right link into the signIn and signUp templates in the link_account template - ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" - ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" + ctx.Data["LinkAccountModeRegister"] = true + + prepareLinkAccountPageData(ctx) linkAccountData := oauth2GetLinkAccountData(ctx) if linkAccountData == nil { diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index c9843146d4..79ff12bc8d 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -229,19 +229,26 @@ func signInOpenIDVerify(ctx *context.Context) { } } -// ConnectOpenID shows a form to connect an OpenID URI to an existing account -func ConnectOpenID(ctx *context.Context) { - oid, _ := ctx.Session.Get("openid_verified_uri").(string) +func prepareConnectOpenIDPageData(ctx *context.Context) (oid string) { + oid, _ = ctx.Session.Get("openid_verified_uri").(string) if oid == "" { ctx.Redirect(setting.AppSubURL + "/user/login/openid") - return + return "" } ctx.Data["Title"] = "OpenID connect" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsOpenIDConnect"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp - ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration ctx.Data["OpenID"] = oid + prepareCommonAuthPageData(ctx, CommonAuthOptions{EnableCaptcha: false}) + return oid +} + +// ConnectOpenID shows a form to connect an OpenID URI to an existing account +func ConnectOpenID(ctx *context.Context) { + oid := prepareConnectOpenIDPageData(ctx) + if oid == "" { + return + } userName, _ := ctx.Session.Get("openid_determined_username").(string) if userName != "" { ctx.Data["user_name"] = userName @@ -252,16 +259,10 @@ func ConnectOpenID(ctx *context.Context) { // ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account func ConnectOpenIDPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ConnectOpenIDForm) - oid, _ := ctx.Session.Get("openid_verified_uri").(string) + oid := prepareConnectOpenIDPageData(ctx) if oid == "" { - ctx.Redirect(setting.AppSubURL + "/user/login/openid") return } - ctx.Data["Title"] = "OpenID connect" - ctx.Data["PageIsSignIn"] = true - ctx.Data["PageIsOpenIDConnect"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp - ctx.Data["OpenID"] = oid u, _, err := auth.UserSignIn(ctx, form.UserName, form.Password) if err != nil { @@ -287,28 +288,29 @@ func ConnectOpenIDPost(ctx *context.Context) { handleSignIn(ctx, u, remember) } -// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI -func RegisterOpenID(ctx *context.Context) { - oid, _ := ctx.Session.Get("openid_verified_uri").(string) +func prepareRegisterOpenIDPageData(ctx *context.Context) (oid string) { + oid, _ = ctx.Session.Get("openid_verified_uri").(string) if oid == "" { ctx.Redirect(setting.AppSubURL + "/user/login/openid") - return + return "" } ctx.Data["Title"] = "OpenID signup" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsOpenIDRegister"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp - ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha - ctx.Data["Captcha"] = context.GetImageCaptcha() - ctx.Data["CaptchaType"] = setting.Service.CaptchaType - ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey - ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey - ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL - ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey - ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL - ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["OpenID"] = oid + prepareCommonAuthPageData(ctx, CommonAuthOptions{ + EnableCaptcha: setting.Service.EnableCaptcha, + }) + return oid +} + +// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI +func RegisterOpenID(ctx *context.Context) { + oid := prepareRegisterOpenIDPageData(ctx) + if oid == "" { + return + } + userName, _ := ctx.Session.Get("openid_determined_username").(string) if userName != "" { ctx.Data["user_name"] = userName @@ -322,19 +324,12 @@ func RegisterOpenID(ctx *context.Context) { // RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI func RegisterOpenIDPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.SignUpOpenIDForm) - oid, _ := ctx.Session.Get("openid_verified_uri").(string) + oid := prepareRegisterOpenIDPageData(ctx) if oid == "" { - ctx.Redirect(setting.AppSubURL + "/user/login/openid") return } - ctx.Data["Title"] = "OpenID signup" - ctx.Data["PageIsSignIn"] = true - ctx.Data["PageIsOpenIDRegister"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp - context.SetCaptchaData(ctx) - ctx.Data["OpenID"] = oid + form := web.GetForm(ctx).(*forms.SignUpOpenIDForm) if setting.Service.AllowOnlyInternalRegistration { ctx.HTTPError(http.StatusForbidden) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 171ccd7719..8fd98980c3 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -218,7 +218,8 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditor } // redirect to the newly updated file - redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath)) + redirectTo := ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(parsed.NewBranchName) + "/" + util.PathEscapeSegments(treePath) + redirectTo = strings.TrimSuffix(redirectTo, "/") ctx.JSONRedirect(redirectTo) } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 680eb03892..e5b07633a2 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -238,7 +238,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if isRaw { - ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName))) + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/raw/" + string(pageName)) } if entry == nil || ctx.Written() { return nil, nil diff --git a/services/context/captcha.go b/services/context/captcha.go index 79278180b7..b1129a05b2 100644 --- a/services/context/captcha.go +++ b/services/context/captcha.go @@ -45,22 +45,6 @@ func GetImageCaptcha() *captcha.Captcha { return cpt } -// SetCaptchaData sets common captcha data -func SetCaptchaData(ctx *Context) { - if !setting.Service.EnableCaptcha { - return - } - ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha - ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL - ctx.Data["Captcha"] = GetImageCaptcha() - ctx.Data["CaptchaType"] = setting.Service.CaptchaType - ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey - ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey - ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey - ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL - ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey -} - const ( gRecaptchaResponseField = "g-recaptcha-response" hCaptchaResponseField = "h-captcha-response" diff --git a/services/context/repo.go b/services/context/repo.go index 674da577b9..c39ab551f2 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -961,7 +961,8 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { } // If short commit ID add canonical link header if len(refShortName) < ctx.Repo.GetObjectFormat().FullLength() { - canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)) + // FIXME: the dirty hack of "strings.Replace" should be fixed + canonicalURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1) ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL)) } } else { diff --git a/services/convert/convert.go b/services/convert/convert.go index e1cd30705e..391960b369 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -203,8 +203,8 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo // ToTag convert a git.Tag to an api.Tag func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag { - tarballURL := util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz") - zipballURL := util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip") + tarballURL := repo.HTMLURL() + "/archive/" + url.PathEscape(t.Name+".tar.gz") + zipballURL := repo.HTMLURL() + "/archive/" + url.PathEscape(t.Name+".zip") // Archive URLs are "" if the download feature is disabled if setting.Repository.DisableDownloadSourceArchives { @@ -713,7 +713,7 @@ func ToAnnotatedTag(ctx context.Context, repo *repo_model.Repository, t *git.Tag SHA: t.ID.String(), Object: ToAnnotatedTagObject(repo, c), Message: t.Message, - URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()), + URL: repo.APIURL() + "/git/tags/" + t.ID.String(), Tagger: ToCommitUser(t.Tagger), Verification: ToVerification(ctx, c), } @@ -724,7 +724,7 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api. return &api.AnnotatedTagObject{ SHA: commit.ID.String(), Type: string(git.ObjectCommit), - URL: util.URLJoin(repo.APIURL(), "git/commits", commit.ID.String()), + URL: repo.APIURL() + "/git/commits/" + commit.ID.String(), } } diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go index bf17024d2d..d809e3778d 100644 --- a/services/convert/git_commit.go +++ b/services/convert/git_commit.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" ctx "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/gitdiff" ) @@ -34,7 +33,7 @@ func ToCommitUser(sig *git.Signature) *api.CommitUser { func ToCommitMeta(repo *repo_model.Repository, tag *git.Tag) *api.CommitMeta { return &api.CommitMeta{ SHA: tag.Object.String(), - URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()), + URL: repo.APIURL() + "/git/commits/" + tag.ID.String(), Created: tag.Tagger.When, } } @@ -58,7 +57,7 @@ func ToPayloadCommit(ctx context.Context, repo *repo_model.Repository, c *git.Co return &api.PayloadCommit{ ID: c.ID.String(), Message: c.Message(), - URL: util.URLJoin(repo.HTMLURL(), "commit", c.ID.String()), + URL: repo.HTMLURL() + "/commit/" + c.ID.String(), Author: &api.PayloadUser{ Name: c.Author.Name, Email: c.Author.Email, diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go index fc032244b5..a4dd3c709d 100644 --- a/services/wiki/wiki_path.go +++ b/services/wiki/wiki_path.go @@ -165,7 +165,7 @@ func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_mod _, title := WebPathToUserTitle(wikiName) return &api.WikiPageMetaData{ Title: title, - HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL), + HTMLURL: repo.HTMLURL() + "/wiki/" + subURL, SubURL: subURL, LastCommit: convert.ToWikiCommit(lastCommit), } diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index e621c04b43..e4ecbd06c4 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -44,7 +44,7 @@ {{svg "octicon-package" 48}}{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl}}
{{end}}{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 9cd4b2a122..bc91adb64f 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -1,5 +1,5 @@ {{if or .UsesIgnoreRevs .FaultyIgnoreRevsFile}} - {{$revsFileLink := URLJoin .RepoLink "src" .RefTypeNameSubURL "/.git-blame-ignore-revs"}} + {{$revsFileLink := print .RepoLink "/src/" .RefTypeNameSubURL "/.git-blame-ignore-revs"}} {{if .UsesIgnoreRevs}}