diff --git a/Dockerfile b/Dockerfile index f71b13e8f3c..9922cee9c41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,14 @@ FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src +COPY package.json pnpm-lock.yaml .npmrc ./ +RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile COPY --exclude=.git/ . . -RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend +RUN make frontend # Build backend for each target platform FROM docker.io/library/golang:1.26-alpine3.23 AS build-env -ARG GOPROXY=direct - ARG GITEA_VERSION ARG TAGS="sqlite sqlite_unlock_notify" ENV TAGS="bindata timetzdata $TAGS" @@ -22,14 +22,15 @@ RUN apk --no-cache add \ git WORKDIR ${GOPATH}/src/code.gitea.io/gitea +COPY go.mod go.sum ./ +RUN go mod download # Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded. # ".git" directory is mounted separately later only for version data extraction. COPY --exclude=.git/ . . COPY --from=frontend-build /src/public/assets public/assets # Build gitea, .git mount is required for version data -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target="/root/.cache/go-build" \ +RUN --mount=type=cache,target="/root/.cache/go-build" \ --mount=type=bind,source=".git/",target=".git/" \ make backend diff --git a/Dockerfile.rootless b/Dockerfile.rootless index bc210132c53..a1742e3d51f 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -3,14 +3,14 @@ FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src +COPY package.json pnpm-lock.yaml .npmrc ./ +RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile COPY --exclude=.git/ . . -RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend +RUN make frontend # Build backend for each target platform FROM docker.io/library/golang:1.26-alpine3.23 AS build-env -ARG GOPROXY=direct - ARG GITEA_VERSION ARG TAGS="sqlite sqlite_unlock_notify" ENV TAGS="bindata timetzdata $TAGS" @@ -22,13 +22,14 @@ RUN apk --no-cache add \ git WORKDIR ${GOPATH}/src/code.gitea.io/gitea +COPY go.mod go.sum ./ +RUN go mod download # See the comments in Dockerfile COPY --exclude=.git/ . . COPY --from=frontend-build /src/public/assets public/assets # Build gitea, .git mount is required for version data -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target="/root/.cache/go-build" \ +RUN --mount=type=cache,target="/root/.cache/go-build" \ --mount=type=bind,source=".git/",target=".git/" \ make backend diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 29943eb8580..22e7f14f39f 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -61,12 +61,14 @@ func (e *escapeStreamer) Text(data string) error { until = len(data) next = until } else { - until, next = nextIdxs[0]+pos, nextIdxs[1]+pos + until = min(nextIdxs[0]+pos, len(data)) + next = min(nextIdxs[1]+pos, len(data)) } // from pos until we know that the runes are not \r\t\n or even ' ' - runes := make([]rune, 0, next-until) - positions := make([]int, 0, next-until+1) + n := next - until + runes := make([]rune, 0, n) + positions := make([]int, 0, n+1) for pos < until { r, sz := utf8.DecodeRune(dataBytes[pos:]) diff --git a/modules/indexer/code/gitgrep/gitgrep.go b/modules/indexer/code/gitgrep/gitgrep.go index 6f6e0b47b9e..5fbd7201ef0 100644 --- a/modules/indexer/code/gitgrep/gitgrep.go +++ b/modules/indexer/code/gitgrep/gitgrep.go @@ -24,7 +24,7 @@ func indexSettingToGitGrepPathspecList() (list []string) { return list } -func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Repository, ref git.RefName, keyword string, searchMode indexer.SearchModeType) (searchResults []*code_indexer.Result, total int, err error) { +func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Repository, ref git.RefName, keyword string, searchMode indexer.SearchModeType) (searchResults []*code_indexer.Result, total int64, err error) { grepMode := git.GrepModeWords switch searchMode { case indexer.SearchModeExact: @@ -47,7 +47,7 @@ func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Rep return nil, 0, fmt.Errorf("gitRepo.GetRefCommitID: %w", err) } - total = len(res) + total = int64(len(res)) pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res)) pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res)) res = res[pageStart:pageEnd] diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index eb20b70e71e..009d659d761 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -130,7 +130,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res } // PerformSearch perform a search on a repository -func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) { +func PerformSearch(ctx context.Context, opts *SearchOptions) (int64, []*Result, []*SearchResultLanguages, error) { if opts == nil || len(opts.Keyword) == 0 { return 0, nil, nil, nil } @@ -149,5 +149,5 @@ func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, [] return 0, nil, nil, err } } - return int(total), displayResults, resultLanguages, nil + return total, displayResults, resultLanguages, nil } diff --git a/modules/markup/camo.go b/modules/markup/camo.go index 7e2583469d3..f07d62d4f91 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 c319374a38d..a2c5160674f 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 85bec5db20c..a94abb38831 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 1702950da8c..c84e0b90d81 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 f97c034cf3b..00cd51ca949 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 5f873d29852..62c4ae3e0a2 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']]", `

AltName

`, ) + // 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]]", `

some/path/Link #.jpg

`, diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 8d6b3b3c80e..261c4e780c9 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 9cc83095ffa..c3a95622ace 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 972e15308cf..045b728c8d1 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 1777d169c10..224fa38eeeb 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 50a38f544ba..7e7611b802d 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 f77ee659748..13575f52a6e 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 c1ee88fc84d..d2d4d364df0 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/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 59b95cdd807..ca85a2ddeea 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -89,7 +89,7 @@ func (p *templateErrorPrettier) handleGenericTemplateError(err error) string { return "" } tmplName, lineStr, message := groups[1], groups[2], groups[3] - return p.makeDetailedError(message, tmplName, lineStr, -1, "") + return p.makeDetailedError(message, tmplName, lineStr, "", "") } var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`) @@ -101,7 +101,7 @@ func (p *templateErrorPrettier) handleFuncNotDefinedError(err error) string { } tmplName, lineStr, message, funcName := groups[1], groups[2], groups[3], groups[4] funcName, _ = strconv.Unquote(`"` + funcName + `"`) - return p.makeDetailedError(message, tmplName, lineStr, -1, funcName) + return p.makeDetailedError(message, tmplName, lineStr, "", funcName) } var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`) @@ -113,7 +113,7 @@ func (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string { } tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4] unexpected, _ = strconv.Unquote(`"` + unexpected + `"`) - return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected) + return p.makeDetailedError(message, tmplName, lineStr, "", unexpected) } var reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`) @@ -124,7 +124,7 @@ func (p *templateErrorPrettier) handleExpectedEndError(err error) string { return "" } tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4] - return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected) + return p.makeDetailedError(message, tmplName, lineStr, "", unexpected) } var ( @@ -154,20 +154,20 @@ func HandleTemplateRenderingError(err error) string { const dashSeparator = "----------------------------------------------------------------------" -func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName string, lineNum, posNum any, target string) string { +func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName, lineNumStr, posNumStr, target string) string { code, layer, err := p.assets.ReadLayeredFile(tmplName + ".tmpl") if err != nil { return fmt.Sprintf("template error: %s, and unable to find template file %q", errMsg, tmplName) } - line, err := util.ToInt64(lineNum) + line, err := strconv.Atoi(lineNumStr) if err != nil { - return fmt.Sprintf("template error: %s, unable to parse template %q line number %q", errMsg, tmplName, lineNum) + return fmt.Sprintf("template error: %s, unable to parse template %q line number %s", errMsg, tmplName, lineNumStr) } - pos, err := util.ToInt64(posNum) + pos, err := strconv.Atoi(util.IfZero(posNumStr, "-1")) if err != nil { - return fmt.Sprintf("template error: %s, unable to parse template %q pos number %q", errMsg, tmplName, posNum) + return fmt.Sprintf("template error: %s, unable to parse template %q pos number %s", errMsg, tmplName, posNumStr) } - detail := extractErrorLine(code, int(line), int(pos), target) + detail := extractErrorLine(code, line, pos, target) var msg string if pos >= 0 { diff --git a/modules/util/url.go b/modules/util/url.go index 62370339c8d..6455b0b75c1 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/modules/web/handler.go b/modules/web/handler.go index 843b17e8d1b..d113bbba451 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -70,7 +70,8 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect.Value, fnInfo *routing.FuncInfo) []reflect.Value { defer func() { - if err := recover(); err != nil { + if recovered := recover(); recovered != nil { + err := fmt.Errorf("%v\n%s", recovered, log.Stack(2)) log.Error("unable to prepare handler arguments for %s: %v", fnInfo.String(), err) panic(err) } @@ -117,7 +118,17 @@ func hasResponseBeenWritten(argsIn []reflect.Value) bool { return false } -func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo *routing.FuncInfo) func(next http.Handler) http.Handler { +type middlewareProvider = func(next http.Handler) http.Handler + +func executeMiddlewaresHandler(w http.ResponseWriter, r *http.Request, middlewares []middlewareProvider, endpoint http.HandlerFunc) { + handler := endpoint + for i := len(middlewares) - 1; i >= 0; i-- { + handler = middlewares[i](handler).ServeHTTP + } + handler(w, r) +} + +func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo *routing.FuncInfo) middlewareProvider { return func(next http.Handler) http.Handler { h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { @@ -129,14 +140,14 @@ func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo // toHandlerProvider converts a handler to a handler provider // A handler provider is a function that takes a "next" http.Handler, it can be used as a middleware -func toHandlerProvider(handler any) func(next http.Handler) http.Handler { +func toHandlerProvider(handler any) middlewareProvider { funcInfo := routing.GetFuncInfo(handler) fn := reflect.ValueOf(handler) if fn.Type().Kind() != reflect.Func { panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type())) } - if hp, ok := handler.(func(next http.Handler) http.Handler); ok { + if hp, ok := handler.(middlewareProvider); ok { return wrapHandlerProvider(hp, funcInfo) } else if hp, ok := handler.(func(http.Handler) http.HandlerFunc); ok { return wrapHandlerProvider(hp, funcInfo) diff --git a/modules/web/router.go b/modules/web/router.go index 5374f82a23c..5ef18e96795 100644 --- a/modules/web/router.go +++ b/modules/web/router.go @@ -18,6 +18,13 @@ import ( "github.com/go-chi/chi/v5" ) +// PreMiddlewareProvider is a special middleware provider which will be executed +// before other middlewares on the same "routing" level (AfterRouting/Group/Methods/Any, but not BeforeRouting). +// A route can do something (e.g.: set middleware options) at the place where it is declared, +// and the code will be executed before other middlewares which are added before the declaration. +// Use cases: mark a route with some meta info, set some options for middlewares, etc. +type PreMiddlewareProvider func(next http.Handler) http.Handler + // Bind binding an obj to a handler's context data func Bind[T any](_ T) http.HandlerFunc { return func(resp http.ResponseWriter, req *http.Request) { @@ -41,7 +48,10 @@ func GetForm(dataStore reqctx.RequestDataStore) any { // Router defines a route based on chi's router type Router struct { - chiRouter *chi.Mux + chiRouter *chi.Mux + + afterRouting []any + curGroupPrefix string curMiddlewares []any } @@ -52,8 +62,9 @@ func NewRouter() *Router { return &Router{chiRouter: r} } -// Use supports two middlewares -func (r *Router) Use(middlewares ...any) { +// BeforeRouting adds middlewares which will be executed before the request path gets routed +// It should only be used for framework-level global middlewares when it needs to change request method & path. +func (r *Router) BeforeRouting(middlewares ...any) { for _, m := range middlewares { if !isNilOrFuncNil(m) { r.chiRouter.Use(toHandlerProvider(m)) @@ -61,7 +72,13 @@ func (r *Router) Use(middlewares ...any) { } } -// Group mounts a sub-Router along a `pattern` string. +// AfterRouting adds middlewares which will be executed after the request path gets routed +// It can see the routed path and resolved path parameters +func (r *Router) AfterRouting(middlewares ...any) { + r.afterRouting = append(r.afterRouting, middlewares...) +} + +// Group mounts a sub-router along a "pattern" string. func (r *Router) Group(pattern string, fn func(), middlewares ...any) { previousGroupPrefix := r.curGroupPrefix previousMiddlewares := r.curMiddlewares @@ -93,36 +110,54 @@ func isNilOrFuncNil(v any) bool { return r.Kind() == reflect.Func && r.IsNil() } -func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { - handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1) - for _, m := range curMiddlewares { - if !isNilOrFuncNil(m) { - handlerProviders = append(handlerProviders, toHandlerProvider(m)) +func wrapMiddlewareAppendPre(all []middlewareProvider, middlewares []any) []middlewareProvider { + for _, m := range middlewares { + if h, ok := m.(PreMiddlewareProvider); ok && h != nil { + all = append(all, toHandlerProvider(middlewareProvider(h))) } } + return all +} + +func wrapMiddlewareAppendNormal(all []middlewareProvider, middlewares []any) []middlewareProvider { + for _, m := range middlewares { + if _, ok := m.(PreMiddlewareProvider); !ok && !isNilOrFuncNil(m) { + all = append(all, toHandlerProvider(m)) + } + } + return all +} + +func wrapMiddlewareAndHandler(useMiddlewares, curMiddlewares, h []any) (_ []middlewareProvider, _ http.HandlerFunc, hasPreMiddlewares bool) { if len(h) == 0 { panic("no endpoint handler provided") } - for i, m := range h { - if !isNilOrFuncNil(m) { - handlerProviders = append(handlerProviders, toHandlerProvider(m)) - } else if i == len(h)-1 { - panic("endpoint handler can't be nil") - } + if isNilOrFuncNil(h[len(h)-1]) { + panic("endpoint handler can't be nil") } + + handlerProviders := make([]middlewareProvider, 0, len(useMiddlewares)+len(curMiddlewares)+len(h)+1) + handlerProviders = wrapMiddlewareAppendPre(handlerProviders, useMiddlewares) + handlerProviders = wrapMiddlewareAppendPre(handlerProviders, curMiddlewares) + handlerProviders = wrapMiddlewareAppendPre(handlerProviders, h) + hasPreMiddlewares = len(handlerProviders) > 0 + handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, useMiddlewares) + handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, curMiddlewares) + handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, h) + middlewares := handlerProviders[:len(handlerProviders)-1] handlerFunc := handlerProviders[len(handlerProviders)-1](nil).ServeHTTP mockPoint := RouterMockPoint(MockAfterMiddlewares) if mockPoint != nil { middlewares = append(middlewares, mockPoint) } - return middlewares, handlerFunc + return middlewares, handlerFunc, hasPreMiddlewares } // Methods adds the same handlers for multiple http "methods" (separated by ","). // If any method is invalid, the lower level router will panic. func (r *Router) Methods(methods, pattern string, h ...any) { - middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) + middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { methods := strings.SplitSeq(methods, ",") @@ -134,15 +169,19 @@ func (r *Router) Methods(methods, pattern string, h ...any) { } } -// Mount attaches another Router along ./pattern/* +// Mount attaches another Router along "/pattern/*" func (r *Router) Mount(pattern string, subRouter *Router) { - subRouter.Use(r.curMiddlewares...) - r.chiRouter.Mount(r.getPattern(pattern), subRouter.chiRouter) + handlerProviders := make([]middlewareProvider, 0, len(r.afterRouting)+len(r.curMiddlewares)) + handlerProviders = wrapMiddlewareAppendPre(handlerProviders, r.afterRouting) + handlerProviders = wrapMiddlewareAppendPre(handlerProviders, r.curMiddlewares) + handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, r.afterRouting) + handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, r.curMiddlewares) + r.chiRouter.With(handlerProviders...).Mount(r.getPattern(pattern), subRouter.chiRouter) } // Any delegate requests for all methods func (r *Router) Any(pattern string, h ...any) { - middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) + middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, h) r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc) } @@ -178,12 +217,16 @@ func (r *Router) Patch(pattern string, h ...any) { // ServeHTTP implements http.Handler func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // TODO: need to move it to the top-level common middleware, otherwise each "Mount" will cause it to be executed multiple times, which is inefficient. r.normalizeRequestPath(w, req, r.chiRouter) } // NotFound defines a handler to respond whenever a route could not be found. func (r *Router) NotFound(h http.HandlerFunc) { - r.chiRouter.NotFound(h) + middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, []any{h}) + r.chiRouter.NotFound(func(w http.ResponseWriter, r *http.Request) { + executeMiddlewaresHandler(w, r, middlewares, handlerFunc) + }) } func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) { diff --git a/modules/web/router_path.go b/modules/web/router_path.go index 64154c34a50..9e531346d13 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -27,11 +27,7 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) for _, m := range g.matchers { if m.matchPath(chiCtx, path) { chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern) - handler := m.handlerFunc - for i := len(m.middlewares) - 1; i >= 0; i-- { - handler = m.middlewares[i](handler).ServeHTTP - } - handler(resp, req) + executeMiddlewaresHandler(resp, req, m.middlewares, m.handlerFunc) return } } @@ -67,7 +63,7 @@ type routerPathMatcher struct { pattern string re *regexp.Regexp params []routerPathParam - middlewares []func(http.Handler) http.Handler + middlewares []middlewareProvider handlerFunc http.HandlerFunc } @@ -111,7 +107,10 @@ func isValidMethod(name string) bool { } func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher { - middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h) + middlewares, handlerFunc, hasPreMiddlewares := wrapMiddlewareAndHandler(nil, patternRegexp.middlewares, h) + if hasPreMiddlewares { + panic("pre-middlewares are not supported in router path matcher") + } p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} for method := range strings.SplitSeq(methods, ",") { method = strings.TrimSpace(method) diff --git a/modules/web/router_test.go b/modules/web/router_test.go index ab5fbb502c7..645e70b869d 100644 --- a/modules/web/router_test.go +++ b/modules/web/router_test.go @@ -30,6 +30,71 @@ func chiURLParamsToMap(chiCtx *chi.Context) map[string]string { return util.Iif(len(m) == 0, nil, m) } +type testResult struct { + method string + pathParams map[string]string + handlerMarks []string + chiRoutePattern *string +} + +type testRecorder struct { + res testResult +} + +func (r *testRecorder) reset() { + r.res = testResult{} +} + +func (r *testRecorder) handle(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { + mark := util.OptionalArg(optMark, "") + return func(resp http.ResponseWriter, req *http.Request) { + chiCtx := chi.RouteContext(req.Context()) + r.res.method = req.Method + r.res.pathParams = chiURLParamsToMap(chiCtx) + r.res.chiRoutePattern = new(chiCtx.RoutePattern()) + if mark != "" { + r.res.handlerMarks = append(r.res.handlerMarks, mark) + } + } +} + +func (r *testRecorder) provider(optMark ...string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + r.handle(optMark...)(resp, req) + next.ServeHTTP(resp, req) + }) + } +} + +func (r *testRecorder) stop(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { + mark := util.OptionalArg(optMark, "") + return func(resp http.ResponseWriter, req *http.Request) { + if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { + r.handle(stop)(resp, req) + resp.WriteHeader(http.StatusOK) + } else if mark != "" { + r.res.handlerMarks = append(r.res.handlerMarks, mark) + } + } +} + +func (r *testRecorder) test(t *testing.T, rt *Router, methodPath string, expected testResult) { + r.reset() + methodPathFields := strings.Fields(methodPath) + req, err := http.NewRequest(methodPathFields[0], methodPathFields[1], nil) + assert.NoError(t, err) + + buff := &bytes.Buffer{} + httpRecorder := httptest.NewRecorder() + httpRecorder.Body = buff + rt.ServeHTTP(httpRecorder, req) + if expected.chiRoutePattern == nil { + r.res.chiRoutePattern = nil + } + assert.Equal(t, expected, r.res) +} + func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() @@ -51,42 +116,10 @@ func TestPathProcessor(t *testing.T) { } func TestRouter(t *testing.T) { - buff := &bytes.Buffer{} - recorder := httptest.NewRecorder() - recorder.Body = buff - - type resultStruct struct { - method string - pathParams map[string]string - handlerMarks []string - chiRoutePattern *string - } - - var res resultStruct - h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { - mark := util.OptionalArg(optMark, "") - return func(resp http.ResponseWriter, req *http.Request) { - chiCtx := chi.RouteContext(req.Context()) - res.method = req.Method - res.pathParams = chiURLParamsToMap(chiCtx) - res.chiRoutePattern = new(chiCtx.RoutePattern()) - if mark != "" { - res.handlerMarks = append(res.handlerMarks, mark) - } - } - } - - stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { - mark := util.OptionalArg(optMark, "") - return func(resp http.ResponseWriter, req *http.Request) { - if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { - h(stop)(resp, req) - resp.WriteHeader(http.StatusOK) - } else if mark != "" { - res.handlerMarks = append(res.handlerMarks, mark) - } - } - } + type resultStruct = testResult + resRecorder := &testRecorder{} + h := resRecorder.handle + stopMark := resRecorder.stop r := NewRouter() r.NotFound(h("not-found:/")) @@ -123,15 +156,7 @@ func TestRouter(t *testing.T) { testRoute := func(t *testing.T, methodPath string, expected resultStruct) { t.Run(methodPath, func(t *testing.T) { - res = resultStruct{} - methodPathFields := strings.Fields(methodPath) - req, err := http.NewRequest(methodPathFields[0], methodPathFields[1], nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - if expected.chiRoutePattern == nil { - res.chiRoutePattern = nil - } - assert.Equal(t, expected, res) + resRecorder.test(t, r, methodPath, expected) }) } @@ -273,3 +298,39 @@ func TestRouteNormalizePath(t *testing.T) { testPath("/v2/", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) testPath("/v2/%2f", paths{EscapedPath: "/v2/%2f", RawPath: "/v2/%2f", Path: "/v2//"}) } + +func TestPreMiddlewareProvider(t *testing.T) { + resRecorder := &testRecorder{} + h := resRecorder.handle + p := resRecorder.provider + + root := NewRouter() + root.BeforeRouting(h("before-root")) + root.AfterRouting(h("root")) + root.Get("/a/1", h("mid"), PreMiddlewareProvider(p("pre-root")), h("end1")) + + sub := NewRouter() + sub.BeforeRouting(h("before-sub")) + sub.AfterRouting(h("sub")) + sub.Get("/2", h("mid"), PreMiddlewareProvider(p("pre-sub")), h("end2")) + sub.NotFound(h("not-found")) + + root.Mount("/a", sub) + + resRecorder.test(t, root, "GET /a/1", testResult{ + method: "GET", + handlerMarks: []string{"before-root", "pre-root", "root", "mid", "end1"}, + }) + resRecorder.test(t, root, "GET /a/2", testResult{ + method: "GET", + handlerMarks: []string{"before-root", "root", "before-sub", "pre-sub", "sub", "mid", "end2"}, + }) + resRecorder.test(t, root, "GET /no-such", testResult{ + method: "GET", + handlerMarks: []string{"before-root"}, + }) + resRecorder.test(t, root, "GET /a/no-such", testResult{ + method: "GET", + handlerMarks: []string{"before-root", "root", "before-sub", "sub", "not-found"}, + }) +} diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go index d3eb98f83db..838abea1587 100644 --- a/modules/web/routing/context.go +++ b/modules/web/routing/context.go @@ -44,7 +44,7 @@ func MarkLongPolling(resp http.ResponseWriter, req *http.Request) { } // UpdatePanicError updates a context's error info, a panic may be recovered by other middlewares, but we still need to know that. -func UpdatePanicError(ctx context.Context, err any) { +func UpdatePanicError(ctx context.Context, err error) { record, ok := ctx.Value(contextKey).(*requestRecord) if !ok { return diff --git a/modules/web/routing/logger_manager.go b/modules/web/routing/logger_manager.go index aa25ec3a271..2f767c3d669 100644 --- a/modules/web/routing/logger_manager.go +++ b/modules/web/routing/logger_manager.go @@ -5,11 +5,13 @@ package routing import ( "context" + "fmt" "net/http" "sync" "time" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" ) @@ -99,7 +101,7 @@ func (manager *requestRecordsManager) handler(next http.Handler) http.Handler { localPanicErr := recover() if localPanicErr != nil { record.lock.Lock() - record.panicError = localPanicErr + record.panicError = fmt.Errorf("%v\n%s", localPanicErr, log.Stack(2)) record.lock.Unlock() } diff --git a/modules/web/routing/requestrecord.go b/modules/web/routing/requestrecord.go index cc61fc4d348..888c3e5c2f4 100644 --- a/modules/web/routing/requestrecord.go +++ b/modules/web/routing/requestrecord.go @@ -24,5 +24,5 @@ type requestRecord struct { // mutable fields isLongPolling bool funcInfo *FuncInfo - panicError any + panicError error } diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 6bef6c03c06..d7e7203a857 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -103,7 +103,7 @@ func init() { func ArtifactsRoutes(prefix string) *web.Router { m := web.NewRouter() - m.Use(ArtifactContexter()) + m.AfterRouting(ArtifactContexter()) r := artifactRoutes{ prefix: prefix, diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 71fee23c920..7f81242db4a 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -94,7 +94,7 @@ func verifyAuth(r *web.Router, authMethods []auth.Method) { } authGroup := auth.NewGroup(authMethods...) - r.Use(func(ctx *context.Context) { + r.AfterRouting(func(ctx *context.Context) { var err error ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) if err != nil { @@ -111,7 +111,7 @@ func verifyAuth(r *web.Router, authMethods []auth.Method) { func CommonRoutes() *web.Router { r := web.NewRouter() - r.Use(context.PackageContexter()) + r.AfterRouting(context.PackageContexter()) verifyAuth(r, []auth.Method{ &auth.OAuth2{}, @@ -533,7 +533,7 @@ func CommonRoutes() *web.Router { func ContainerRoutes() *web.Router { r := web.NewRouter() - r.Use(context.PackageContexter()) + r.AfterRouting(context.PackageContexter()) verifyAuth(r, []auth.Method{ &auth.Basic{}, diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 92711409f00..9f1175a1ffe 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -50,7 +50,7 @@ func ListUnadoptedRepositories(ctx *context.APIContext) { return } - ctx.SetTotalCountHeader(int64(count)) + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, repoNames) } diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go index ad078347a40..1212babac45 100644 --- a/routers/api/v1/admin/email.go +++ b/routers/api/v1/admin/email.go @@ -51,7 +51,7 @@ func GetAllEmails(ctx *context.APIContext) { results[i] = convert.ToEmailSearch(emails[i]) } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &results) } diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index 6170e7343af..80e9e964c25 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -77,7 +77,7 @@ func ListHooks(ctx *context.APIContext) { } hooks[i] = h } - ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.SetLinkHeader(total, listOptions.PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, hooks) } diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 62afcb00d9a..6390bb7e829 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -117,7 +117,7 @@ func GetAllOrgs(ctx *context.APIContext) { orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(users[i])) } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &orgs) } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 6bed4106427..b9dd12f8ff6 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -534,7 +534,7 @@ func SearchUsers(ctx *context.APIContext) { results[i] = convert.ToUser(ctx, users[i], ctx.Doer) } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &results) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5a6da089dee..907f1dba273 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -77,7 +77,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -756,13 +755,9 @@ func buildAuthGroup() *auth.Group { &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API ) if setting.Service.EnableReverseProxyAuthAPI { - group.Add(&auth.ReverseProxy{}) + group.Add(&auth.ReverseProxy{}) // TODO: does it still make sense to support reverse proxy auth in API? } - - if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) { - group.Add(&auth.SSPI{}) // it MUST be the last, see the comment of SSPI - } - + // others: API doesn't support SSPI auth because the caller should use token return group } @@ -872,9 +867,9 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) { func Routes() *web.Router { m := web.NewRouter() - m.Use(securityHeaders()) + m.BeforeRouting(securityHeaders()) if setting.CORSConfig.Enabled { - m.Use(cors.Handler(cors.Options{ + m.BeforeRouting(cors.Handler(cors.Options{ AllowedOrigins: setting.CORSConfig.AllowDomain, AllowedMethods: setting.CORSConfig.Methods, AllowCredentials: setting.CORSConfig.AllowCredentials, @@ -882,14 +877,14 @@ func Routes() *web.Router { MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), })) } - m.Use(context.APIContexter()) - m.Use(checkDeprecatedAuthMethods) + m.AfterRouting(context.APIContexter()) + m.AfterRouting(checkDeprecatedAuthMethods) // Get user from session if logged in. - m.Use(apiAuth(buildAuthGroup())) + m.AfterRouting(apiAuth(buildAuthGroup())) - m.Use(verifyAuthWithOptions(&common.VerifyOptions{ + m.AfterRouting(verifyAuthWithOptions(&common.VerifyOptions{ SignInRequired: setting.Service.RequireSignInViewStrict, })) diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index 51695a52c8d..5e23e2285da 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -125,7 +125,7 @@ func ListRepoNotifications(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(totalCount), opts.PageSize) + ctx.SetLinkHeader(totalCount, opts.PageSize) ctx.SetTotalCountHeader(totalCount) ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl)) } diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 82cedd418b8..629a5ec2288 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -86,7 +86,7 @@ func ListNotifications(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(totalCount), opts.PageSize) + ctx.SetLinkHeader(totalCount, opts.PageSize) ctx.SetTotalCountHeader(totalCount) ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl)) } diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index d058964f52d..687e9fcbfb6 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -67,7 +67,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) { } } - ctx.SetLinkHeader(int(count), opts.PageSize) + ctx.SetLinkHeader(count, opts.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiSecrets) } @@ -240,7 +240,7 @@ func (Action) ListVariables(ctx *context.APIContext) { Description: v.Description, } } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index b72cafee0c7..e7311fc6fd3 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -45,7 +45,7 @@ func listMembers(ctx *context.APIContext, isMember bool) { apiMembers[i] = convert.ToUser(ctx, member, ctx.Doer) } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiMembers) } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index f229a84f66a..0f4da7966ca 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -48,7 +48,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) { apiOrgs[i] = convert.ToOrganization(ctx, orgs[i]) } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &apiOrgs) } @@ -221,7 +221,7 @@ func GetAll(ctx *context.APIContext) { orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(publicOrgs[i])) } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &orgs) } diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 211b7a15b25..3b5711eea32 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -70,7 +70,7 @@ func ListTeams(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiTeams) } @@ -111,7 +111,7 @@ func ListUserTeams(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiTeams) } @@ -411,7 +411,7 @@ func GetTeamMembers(ctx *context.APIContext) { members[i] = convert.ToUser(ctx, member, ctx.Doer) } - ctx.SetLinkHeader(ctx.Org.Team.NumMembers, listOptions.PageSize) + ctx.SetLinkHeader(int64(ctx.Org.Team.NumMembers), listOptions.PageSize) ctx.SetTotalCountHeader(int64(ctx.Org.Team.NumMembers)) ctx.JSON(http.StatusOK, members) } @@ -583,7 +583,7 @@ func GetTeamRepos(ctx *context.APIContext) { } repos[i] = convert.ToRepo(ctx, repo, permission) } - ctx.SetLinkHeader(team.NumRepos, listOptions.PageSize) + ctx.SetLinkHeader(int64(team.NumRepos), listOptions.PageSize) ctx.SetTotalCountHeader(int64(team.NumRepos)) ctx.JSON(http.StatusOK, repos) } @@ -827,7 +827,7 @@ func SearchTeam(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, map[string]any{ "ok": true, @@ -882,7 +882,7 @@ func ListTeamActivityFeeds(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) } diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 41b7f2a43f6..cee0daccaec 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -68,7 +68,7 @@ func ListPackages(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiPackages) } @@ -249,7 +249,7 @@ func ListPackageVersions(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiPackages) } diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 13da5aa8151..6f4d5d35727 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -91,7 +91,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) { Created: v.CreatedUnix.AsTime(), } } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiSecrets) } @@ -506,7 +506,7 @@ func (Action) ListVariables(ctx *context.APIContext) { } } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } @@ -811,7 +811,7 @@ func ListActionTasks(ctx *context.APIContext) { res.Entries[i] = convertedTask } - ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.SetLinkHeader(total, listOptions.PageSize) ctx.SetTotalCountHeader(total) // Duplicates api response field but it's better to set it for consistency ctx.JSON(http.StatusOK, &res) } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index eaa8afb1e13..c3b3fc10855 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -375,7 +375,7 @@ func ListBranches(ctx *context.APIContext) { } } - ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize) + ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize) ctx.SetTotalCountHeader(totalNumOfBranches) ctx.JSON(http.StatusOK, apiBranches) } diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 2a7efa0ea6f..008e5dc56da 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -290,7 +290,7 @@ func GetAllCommits(ctx *context.APIContext) { } } - ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize) + ctx.SetLinkHeader(commitsCountTotal, listOptions.PageSize) ctx.SetTotalCountHeader(commitsCountTotal) // kept for backwards compatibility diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 22324e19233..db205380e45 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -299,7 +299,7 @@ func SearchIssues(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(total), limit) + ctx.SetLinkHeader(total, limit) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues)) } @@ -527,7 +527,7 @@ func ListIssues(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.SetLinkHeader(total, listOptions.PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues)) } diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 6c66e719eb8..139779dcec9 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -81,7 +81,7 @@ func GetIssueDependencies(ctx *context.APIContext) { canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) - blockerIssues := make([]*issues_model.Issue, 0, listOptions.PageSize) + blockerIssues := make([]*issues_model.Issue, 0, min(listOptions.PageSize, setting.API.MaxResponseItems)) // 2. Get the issues this issue depends on, i.e. the `<#b>`: ` <- <#b>` blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions) @@ -140,7 +140,7 @@ func GetIssueDependencies(ctx *context.APIContext) { } blockerIssues = append(blockerIssues, &blocker.Issue) } - ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.SetLinkHeader(total, listOptions.PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues)) } diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index f11a1603c4c..0dc9013ff39 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -179,7 +179,7 @@ func ListPushMirrors(ctx *context.APIContext) { responsePushMirrors = append(responsePushMirrors, m) } } - ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize) + ctx.SetLinkHeader(int64(len(responsePushMirrors)), utils.GetListOptions(ctx).PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, responsePushMirrors) } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index e6f4dd62ce5..af2f6fae5d2 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -154,7 +154,7 @@ func ListPullRequests(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &apiPrs) } @@ -1449,7 +1449,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { apiCommits = append(apiCommits, apiCommit) } - ctx.SetLinkHeader(totalNumberOfCommits, listOptions.PageSize) + ctx.SetLinkHeader(int64(totalNumberOfCommits), listOptions.PageSize) ctx.SetTotalCountHeader(int64(totalNumberOfCommits)) ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page)) @@ -1591,7 +1591,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID)) } - ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize) + ctx.SetLinkHeader(int64(totalNumberOfFiles), listOptions.PageSize) ctx.SetTotalCountHeader(int64(totalNumberOfFiles)) ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page)) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index ff43628fa5e..349983806e5 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -202,7 +202,7 @@ func ListReleases(ctx *context.APIContext) { return } - ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize) + ctx.SetLinkHeader(filteredCount, listOptions.PageSize) ctx.SetTotalCountHeader(filteredCount) ctx.JSON(http.StatusOK, rels) } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index cfdcf7b374e..1b3d85346bd 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -230,7 +230,7 @@ func Search(ctx *context.APIContext) { } results[i] = convert.ToRepo(ctx, repo, permission) } - ctx.SetLinkHeader(int(count), opts.PageSize) + ctx.SetLinkHeader(count, opts.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, api.SearchResults{ OK: true, diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index e69d4468dea..2b0e52818fc 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -206,7 +206,7 @@ func getCommitStatuses(ctx *context.APIContext, commitID string) { apiStatuses = append(apiStatuses, convert.ToCommitStatus(ctx, status)) } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, apiStatuses) @@ -269,7 +269,7 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) { ctx.APIErrorInternal(fmt.Errorf("CountLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err)) return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) combiStatus := convert.ToCombinedStatus(ctx, refCommit.Commit.ID.String(), statuses, diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 90dd08394ec..a860665f511 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -333,7 +333,7 @@ func ListWikiPages(ctx *context.APIContext) { pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository)) } - ctx.SetLinkHeader(len(entries), limit) + ctx.SetLinkHeader(int64(len(entries)), limit) ctx.SetTotalCountHeader(int64(len(entries))) ctx.JSON(http.StatusOK, pages) } diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go index 108fca787b8..715e76c3557 100644 --- a/routers/api/v1/shared/action.go +++ b/routers/api/v1/shared/action.go @@ -79,7 +79,7 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { } res.Entries[i] = convertedWorkflowJob } - ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.SetLinkHeader(total, listOptions.PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &res) } @@ -185,7 +185,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { } res.Entries[i] = convertedRun } - ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.SetLinkHeader(total, listOptions.PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &res) } diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go index 19ad552e20f..5762c5abf17 100644 --- a/routers/api/v1/shared/block.go +++ b/routers/api/v1/shared/block.go @@ -36,7 +36,7 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) { users = append(users, convert.ToUser(ctx, b.Blockee, blocker)) } - ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.SetLinkHeader(total, listOptions.PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &users) } diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 069d5e39b60..573e2e4dd08 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -354,7 +354,7 @@ func ListVariables(ctx *context.APIContext) { } } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 48c624ced98..5b31a00b76f 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -31,7 +31,7 @@ func listUserFollowers(ctx *context.APIContext, u *user_model.User) { return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) responseAPIUsers(ctx, users) } @@ -97,7 +97,7 @@ func listUserFollowing(ctx *context.APIContext, u *user_model.User) { return } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) responseAPIUsers(ctx, users) } diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index de0ac7b1e4f..ca58346413a 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -94,7 +94,7 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) { } } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &apiKeys) } diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 6d0129681e0..e24a7543a14 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -47,7 +47,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { } } - ctx.SetLinkHeader(int(count), opts.PageSize) + ctx.SetLinkHeader(count, opts.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &apiRepos) } @@ -130,7 +130,7 @@ func ListMyRepos(ctx *context.APIContext) { results[i] = convert.ToRepo(ctx, repo, permission) } - ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize) + ctx.SetLinkHeader(count, opts.ListOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, &results) } diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 5c0d976527b..4464d539362 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -76,7 +76,7 @@ func GetStarredRepos(ctx *context.APIContext) { return } - ctx.SetLinkHeader(ctx.ContextUser.NumStars, utils.GetListOptions(ctx).PageSize) + ctx.SetLinkHeader(int64(ctx.ContextUser.NumStars), utils.GetListOptions(ctx).PageSize) ctx.SetTotalCountHeader(int64(ctx.ContextUser.NumStars)) ctx.JSON(http.StatusOK, &repos) } @@ -108,7 +108,7 @@ func GetMyStarredRepos(ctx *context.APIContext) { ctx.APIErrorInternal(err) } - ctx.SetLinkHeader(ctx.Doer.NumStars, utils.GetListOptions(ctx).PageSize) + ctx.SetLinkHeader(int64(ctx.Doer.NumStars), utils.GetListOptions(ctx).PageSize) ctx.SetTotalCountHeader(int64(ctx.Doer.NumStars)) ctx.JSON(http.StatusOK, &repos) } diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index f7b93017957..005770c5713 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -91,7 +91,7 @@ func Search(ctx *context.APIContext) { } } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetLinkHeader(maxResults, listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, map[string]any{ diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 1ce0f3f5292..751b0ae3bcb 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -71,7 +71,7 @@ func GetWatchedRepos(ctx *context.APIContext) { ctx.APIErrorInternal(err) } - ctx.SetLinkHeader(int(total), utils.GetListOptions(ctx).PageSize) + ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &repos) } @@ -100,7 +100,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) { if err != nil { ctx.APIErrorInternal(err) } - ctx.SetLinkHeader(int(total), utils.GetListOptions(ctx).PageSize) + ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, &repos) } diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 9f0447a80be..c4f21eac8b0 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -43,7 +43,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { return } } - ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetLinkHeader(count, listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiHooks) } diff --git a/routers/common/errpage.go b/routers/common/errpage.go index 2406cf443fa..07760bcd18b 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -53,18 +53,18 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in _, _ = io.Copy(w, outBuf) } -// RenderPanicErrorPage renders a 500 page, and it never panics -func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) { - combinedErr := fmt.Sprintf("%v\n%s", err, log.Stack(2)) - log.Error("PANIC: %s", combinedErr) +// renderPanicErrorPage renders a 500 page with the recovered panic value, it handles the stack trace, and it never panics +func renderPanicErrorPage(w http.ResponseWriter, req *http.Request, recovered any) { + combinedErr := fmt.Errorf("%v\n%s", recovered, log.Stack(2)) + log.Error("PANIC: %v", combinedErr) defer func() { if err := recover(); err != nil { - log.Error("Panic occurs again when rendering error page: %v. Stack:\n%s", err, log.Stack(2)) + log.Error("Panic occurs again when rendering error page: %v. Stack:\n%s", combinedErr, log.Stack(2)) } }() - routing.UpdatePanicError(req.Context(), err) + routing.UpdatePanicError(req.Context(), combinedErr) plainMsg := "Internal Server Error" ctxData := middleware.GetContextData(req.Context()) @@ -72,7 +72,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) { // Otherwise, the 500-page may cause new panics, eg: cache.GetContextWithData, it makes the developer&users couldn't find the original panic. user, _ := ctxData[middleware.ContextDataKeySignedUser].(*user_model.User) if !setting.IsProd || (user != nil && user.IsAdmin) { - plainMsg = "PANIC: " + combinedErr + plainMsg = "PANIC: " + combinedErr.Error() ctxData["ErrorMsg"] = plainMsg } renderServerErrorPage(w, req, http.StatusInternalServerError, tplStatus500, ctxData, plainMsg) diff --git a/routers/common/errpage_test.go b/routers/common/errpage_test.go index c50d45c2968..b81803c0155 100644 --- a/routers/common/errpage_test.go +++ b/routers/common/errpage_test.go @@ -22,7 +22,7 @@ func TestRenderPanicErrorPage(t *testing.T) { w := httptest.NewRecorder() req := &http.Request{URL: &url.URL{}, Header: http.Header{"Accept": []string{"text/html"}}} req = req.WithContext(reqctx.NewRequestContextForTest(t.Context())) - RenderPanicErrorPage(w, req, errors.New("fake panic error (for test only)")) + renderPanicErrorPage(w, req, errors.New("fake panic error (for test only)")) respContent := w.Body.String() assert.Contains(t, respContent, `class="page-content status-page-500"`) assert.Contains(t, respContent, ``) diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 6bf430d361d..9daffb04f1c 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -5,13 +5,13 @@ package common import ( "fmt" - "log" "net/http" "strings" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/routing" @@ -63,8 +63,8 @@ func RequestContextHandler() func(h http.Handler) http.Handler { }() defer func() { - if err := recover(); err != nil { - RenderPanicErrorPage(respWriter, req, err) // it should never panic + if recovered := recover(); recovered != nil { + renderPanicErrorPage(respWriter, req, recovered) // it should never panic, and it handles the stack trace internally } }() @@ -130,7 +130,7 @@ func MustInitSessioner() func(next http.Handler) http.Handler { Domain: setting.SessionConfig.Domain, }) if err != nil { - log.Fatalf("common.Sessioner failed: %v", err) + log.Fatal("common.Sessioner failed: %v", err) } return middleware } diff --git a/routers/init.go b/routers/init.go index 8874236a607..2ed7a57e5c8 100644 --- a/routers/init.go +++ b/routers/init.go @@ -180,8 +180,9 @@ func InitWebInstalled(ctx context.Context) { // NormalRoutes represents non install routes func NormalRoutes() *web.Router { r := web.NewRouter() - r.Use(common.ProtocolMiddlewares()...) - r.Use(common.MaintenanceModeHandler()) + r.BeforeRouting(common.ProtocolMiddlewares()...) + + r.AfterRouting(common.MaintenanceModeHandler()) r.Mount("/", web_routers.Routes()) r.Mount("/api/v1", apiv1.Routes()) diff --git a/routers/install/routes.go b/routers/install/routes.go index 0914c921c0c..6fd511cbd18 100644 --- a/routers/install/routes.go +++ b/routers/install/routes.go @@ -20,11 +20,12 @@ import ( // Routes registers the installation routes func Routes() *web.Router { base := web.NewRouter() - base.Use(common.ProtocolMiddlewares()...) + base.BeforeRouting(common.ProtocolMiddlewares()...) + base.Methods("GET, HEAD", "/assets/*", public.FileHandlerFunc()) r := web.NewRouter() - r.Use(common.MustInitSessioner(), installContexter()) + r.AfterRouting(common.MustInitSessioner(), installContexter()) r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall) diff --git a/routers/private/internal.go b/routers/private/internal.go index 2d5436468b6..d4918455f19 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -54,11 +54,11 @@ func bind[T any](_ T) any { // These APIs will be invoked by internal commands for example `gitea serv` and etc. func Routes() *web.Router { r := web.NewRouter() - r.Use(context.PrivateContexter()) - r.Use(authInternal) + r.AfterRouting(context.PrivateContexter()) + r.AfterRouting(authInternal) // Log the real ip address of the request from SSH is really helpful for diagnosing sometimes. // Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers. - r.Use(chi_middleware.RealIP) + r.AfterRouting(chi_middleware.RealIP) r.Get("/dummy", misc.DummyOK) r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index 51b3d584f42..d608278f285 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -93,7 +93,7 @@ func Emails(ctx *context.Context) { ctx.Data["Total"] = count ctx.Data["Emails"] = emails - pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go index e9d6abbe92e..a09aa934215 100644 --- a/routers/web/admin/notice.go +++ b/routers/web/admin/notice.go @@ -37,7 +37,7 @@ func Notices(ctx *context.Context) { ctx.Data["Total"] = total - ctx.Data["Page"] = context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5) + ctx.Data["Page"] = context.NewPagination(total, setting.UI.Admin.NoticePagingNum, page, 5) ctx.HTML(http.StatusOK, tplNotices) } diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 1904bfee11e..a0f983914d2 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -73,7 +73,7 @@ func Packages(ctx *context.Context) { ctx.Data["TotalBlobSize"] = totalBlobSize - totalUnreferencedBlobSize ctx.Data["TotalUnreferencedBlobSize"] = totalUnreferencedBlobSize - pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index fbc3f9f07db..1219690200e 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 faa712471f4..02e1b7acd2f 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 c9843146d45..79ff12bc8d3 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/explore/code.go b/routers/web/explore/code.go index 3cace7dbec9..fc428d9ec94 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -66,7 +66,7 @@ func Code(ctx *context.Context) { } var ( - total int + total int64 searchResults []*code_indexer.Result searchResultLanguages []*code_indexer.SearchResultLanguages ) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index ed12d0c52a2..0d5fea26977 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -137,7 +137,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { ctx.Data["Repos"] = repos ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - pager := context.NewPagination(int(count), opts.PageSize, page, 5) + pager := context.NewPagination(count, opts.PageSize, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index 9398dae490c..b88c13f7d4f 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -119,7 +119,7 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 43d318c1a1f..c738b68c8d3 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -6,12 +6,9 @@ package web import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/repo" - "code.gitea.io/gitea/services/context" ) -func addOwnerRepoGitHTTPRouters(m *web.Router) { - // Some users want to use "web-based git client" to access Gitea's repositories, - // so the CORS handler and OPTIONS method are used. +func addOwnerRepoGitHTTPRouters(m *web.Router, middlewares ...any) { m.Group("/{username}/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) @@ -25,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) - }, repo.HTTPGitEnabledHandler, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb()) + }, middlewares...) } diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 63ae6c683b5..e18a8de40f6 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -141,7 +141,7 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["Repos"] = repos ctx.Data["Total"] = count - pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5) + pager := context.NewPagination(count, setting.UI.User.RepoPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 61022d3f09a..6523bbf38d4 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -56,7 +56,7 @@ func Members(ctx *context.Context) { return } - pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.MembersPagingNum, page, 5) opts.ListOptions.Page = page opts.ListOptions.PageSize = setting.UI.MembersPagingNum members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts) diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index e01e615de6f..4cdf81c1559 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -114,12 +114,7 @@ func Projects(ctx *context.Context) { project.RenderedContent = renderUtils.MarkdownToHtml(project.Description) } - numPages := 0 - if total > 0 { - numPages = (int(total) - 1/setting.UI.IssuePagingNum) - } - - pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages) + pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index e91f96f7c63..8e8557305b2 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -341,7 +341,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale) - pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(total, opts.PageSize, opts.Page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 1d6961f6fc8..5e5cfec5c2b 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -84,7 +84,7 @@ func Branches(ctx *context.Context) { ctx.Data["CommitStatus"] = commitStatus ctx.Data["CommitStatuses"] = commitStatuses ctx.Data["DefaultBranchBranch"] = defaultBranch - pager := context.NewPagination(int(branchesCount), pageSize, page, 5) + pager := context.NewPagination(branchesCount, pageSize, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplBranch) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 27f5651ecb4..168d9594940 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -101,7 +101,7 @@ func Commits(ctx *context.Context) { ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["CommitCount"] = commitsCount - pager := context.NewPagination(int(commitsCount), pageSize, page, 5) + pager := context.NewPagination(commitsCount, pageSize, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) @@ -170,7 +170,7 @@ func Graph(ctx *context.Context) { divOnly := ctx.FormBool("div-only") queryParams := ctx.Req.URL.Query() queryParams.Del("div-only") - paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) + paginator := context.NewPagination(graphCommitsCount, setting.UI.GraphMaxCommitNum, page, 5) paginator.AddParamFromQuery(queryParams) ctx.Data["Page"] = paginator if divOnly { @@ -254,7 +254,7 @@ func FileHistory(ctx *context.Context) { ctx.Data["FileTreePath"] = ctx.Repo.TreePath ctx.Data["CommitCount"] = commitsCount - pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) + pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 171ccd77194..8fd98980c39 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/issue_list.go b/routers/web/repo/issue_list.go index 41b4b2aa17a..83ef515bde5 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -575,9 +575,9 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 } // prepare pager - total := int(issueStats.OpenCount + issueStats.ClosedCount) + total := issueStats.OpenCount + issueStats.ClosedCount if isShowClosed.Has() { - total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount)) + total = util.Iif(isShowClosed.Value(), issueStats.ClosedCount, issueStats.OpenCount) } page := max(ctx.FormInt("page"), 1) pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 09196d4bc20..b928be28673 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -88,7 +88,7 @@ func Milestones(ctx *context.Context) { ctx.Data["Keyword"] = keyword ctx.Data["IsShowClosed"] = isShowClosed - pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index d09a57c03fe..cfb788a5b27 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -64,7 +64,7 @@ func Packages(ctx *context.Context) { ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository - pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index e7a9e6ba124..c9bdc5be76e 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -66,13 +66,6 @@ func Projects(ctx *context.Context) { ctx.Data["OpenCount"] = repo.NumOpenProjects ctx.Data["ClosedCount"] = repo.NumClosedProjects - var total int - if !isShowClosed { - total = repo.NumOpenProjects - } else { - total = repo.NumClosedProjects - } - projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.IssuePagingNum, @@ -111,12 +104,7 @@ func Projects(ctx *context.Context) { ctx.Data["State"] = "open" } - numPages := 0 - if count > 0 { - numPages = (int(count) - 1/setting.UI.IssuePagingNum) - } - - pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) + pager := context.NewPagination(count, setting.UI.IssuePagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 891af4c2d5c..005106a32d8 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -187,7 +187,7 @@ func Releases(ctx *context.Context) { ctx.Data["Releases"] = releases numReleases := ctx.Data["NumReleases"].(int64) - pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) + pager := context.NewPagination(numReleases, listOptions.PageSize, listOptions.Page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplReleasesList) @@ -239,7 +239,7 @@ func TagsList(ctx *context.Context) { ctx.Data["Releases"] = releases ctx.Data["TagCount"] = count - pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index 12216fc6201..304a53abd83 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -32,7 +32,7 @@ func Search(ctx *context.Context) { page = 1 } - var total int + var total int64 var searchResults []*code_indexer.Result var searchResultLanguages []*code_indexer.SearchResultLanguages if setting.Indexer.RepoIndexerEnabled { diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index a3a60963d43..06c482e65c8 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -54,7 +54,7 @@ func LFSFiles(ctx *context.Context) { } ctx.Data["Total"] = total - pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.ExplorePagingNum, page, 5) ctx.Data["Title"] = ctx.Tr("repo.settings.lfs") ctx.Data["PageIsSettingsLFS"] = true lfsMetaObjects, err := git_model.GetLFSMetaObjects(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum) @@ -83,7 +83,7 @@ func LFSLocks(ctx *context.Context) { } ctx.Data["Total"] = total - pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.ExplorePagingNum, page, 5) ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks") ctx.Data["PageIsSettingsLFS"] = true lfsLocks, err := git_model.GetLFSLockByRepoID(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 8aeb1a0af8e..7136b87058f 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -345,7 +345,7 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOp if page <= 0 { page = 1 } - pager := context.NewPagination(total, setting.ItemsPerPage, page, 5) + pager := context.NewPagination(int64(total), setting.ItemsPerPage, page, 5) ctx.Data["Page"] = pager items, err := getter(db.ListOptions{ @@ -403,7 +403,7 @@ func Forks(ctx *context.Context) { return } - pager := context.NewPagination(int(total), pageSize, page, 5) + pager := context.NewPagination(total, pageSize, page, 5) ctx.Data["ShowRepoOwnerAvatar"] = true ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["Page"] = pager diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 33f4f7b77bb..e5b07633a21 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 @@ -371,7 +371,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) return nil, nil } - pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) + pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 9dca366123f..577dad822c2 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -159,7 +159,7 @@ func Runners(ctx *context.Context) { ctx.Data["RunnerRepoID"] = opts.RepoID ctx.Data["SortType"] = opts.Sort - pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) ctx.Data["Page"] = pager @@ -220,7 +220,7 @@ func RunnersEdit(ctx *context.Context) { } ctx.Data["Tasks"] = tasks - pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate) diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 11579c40a6e..b4de516b3f7 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -59,7 +59,7 @@ func CodeSearch(ctx *context.Context) { } var ( - total int + total int64 searchResults []*code_indexer.Result searchResultLanguages []*code_indexer.SearchResultLanguages ) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index afdba9a75f6..21ca0fc683e 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -301,15 +301,15 @@ func Milestones(ctx *context.Context) { return !showRepoIDs.Contains(v) }) - var pagerCount int + var pagerCount int64 if isShowClosed { ctx.Data["State"] = "closed" ctx.Data["Total"] = totalMilestoneStats.ClosedCount - pagerCount = int(milestoneStats.ClosedCount) + pagerCount = milestoneStats.ClosedCount } else { ctx.Data["State"] = "open" ctx.Data["Total"] = totalMilestoneStats.OpenCount - pagerCount = int(milestoneStats.OpenCount) + pagerCount = milestoneStats.OpenCount } ctx.Data["Milestones"] = milestones @@ -578,11 +578,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } // Will be posted to ctx.Data. - var shownIssues int + var shownIssues int64 if !isShowClosed { - shownIssues = int(issueStats.OpenCount) + shownIssues = issueStats.OpenCount } else { - shownIssues = int(issueStats.ClosedCount) + shownIssues = issueStats.ClosedCount } ctx.Data["IsShowClosed"] = isShowClosed diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index cf61b0a2f2c..3b7ecd062b3 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -61,11 +61,11 @@ func prepareUserNotificationsData(ctx *context.Context) { return } - pager := context.NewPagination(int(total), perPage, page, 5) + pager := context.NewPagination(total, perPage, page, 5) if pager.Paginater.Current() < page { // use the last page if the requested page is more than total pages page = pager.Paginater.Current() - pager = context.NewPagination(int(total), perPage, page, 5) + pager = context.NewPagination(total, perPage, page, 5) } statuses := []activities_model.NotificationStatus{queryStatus, activities_model.NotificationStatusPinned} @@ -286,7 +286,7 @@ func NotificationSubscriptions(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("notification.subscriptions") // redirect to last page if request page is more than total pages - pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5) + pager := context.NewPagination(count, setting.UI.IssuePagingNum, page, 5) if pager.Paginater.Current() < page { ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current())) return @@ -370,12 +370,11 @@ func NotificationWatching(ctx *context.Context) { ctx.ServerError("SearchRepository", err) return } - total := int(count) - ctx.Data["Total"] = total + ctx.Data["Total"] = count ctx.Data["Repos"] = repos // redirect to last page if request page is more than total pages - pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5) + pager := context.NewPagination(count, setting.UI.User.RepoPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 924a10041b0..2dad5be554f 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -127,7 +127,7 @@ func ListPackages(ctx *context.Context) { ctx.Data["IsOrganizationOwner"] = false } } - pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) @@ -412,7 +412,7 @@ func ListPackageVersions(ctx *context.Context) { ctx.Data["Total"] = total - pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) + pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index f5800550306..faf2f442a21 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -102,7 +102,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R var ( repos []*repo_model.Repository count int64 - total int + total int64 curRows int orderBy db.SearchOrderBy ) @@ -157,10 +157,10 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R switch tab { case "followers": ctx.Data["Cards"] = followers - total = int(numFollowers) + total = numFollowers case "following": ctx.Data["Cards"] = following - total = int(numFollowing) + total = numFollowing case "activity": if setting.Service.EnableUserHeatmap && activities_model.ActivityReadable(ctx.ContextUser, ctx.Doer) { ctx.Data["EnableHeatmap"] = true @@ -218,7 +218,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R return } - total = int(count) + total = count case "watching": repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -245,7 +245,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R return } - total = int(count) + total = count case "overview": if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { log.Error("failed to GetBlobContent: %v", err) @@ -273,7 +273,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R return } ctx.Data["Cards"] = orgs - total = int(count) + total = count default: // default to "repositories" repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -300,7 +300,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R return } - total = int(count) + total = count } ctx.Data["Repos"] = repos ctx.Data["Total"] = total diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 35303221fe4..81a4a558e96 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -222,7 +222,7 @@ func Organization(ctx *context.Context) { } ctx.Data["Orgs"] = orgs - pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(total, opts.PageSize, opts.Page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsOrganization) @@ -244,13 +244,13 @@ func Repos(ctx *context.Context) { if opts.Page <= 0 { opts.Page = 1 } - start := (opts.Page - 1) * opts.PageSize - end := start + opts.PageSize + start := int64((opts.Page - 1) * opts.PageSize) + end := start + int64(opts.PageSize) adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories) ctxUser := ctx.Doer - count := 0 + var count int64 if adoptOrDelete { repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum) @@ -310,12 +310,12 @@ func Repos(ctx *context.Context) { ctx.Data["Dirs"] = repoNames ctx.Data["ReposMap"] = repos } else { - repos, count64, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) + repos, reposCount, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) if err != nil { ctx.ServerError("GetUserRepositories", err) return } - count = int(count64) + count = reposCount for i := range repos { if repos[i].IsFork { diff --git a/routers/web/web.go b/routers/web/web.go index 182c6c595be..95a54b5244b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/metrics" "code.gitea.io/gitea/modules/public" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/structs" @@ -90,32 +91,64 @@ func optionsCorsHandler() func(next http.Handler) http.Handler { } } -// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored -// in the session (if there is a user id stored in session other plugins might return the user -// object for that id). -// -// The Session plugin is expected to be executed second, in order to skip authentication -// for users that have already signed in. -func buildAuthGroup() *auth_service.Group { - group := auth_service.NewGroup() - group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers - group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers - - if setting.Service.EnableReverseProxyAuth { - group.Add(&auth_service.ReverseProxy{}) // reverse-proxy should before Session, otherwise the header will be ignored if user has login - } - group.Add(&auth_service.Session{}) - - if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) { - group.Add(&auth_service.SSPI{}) // it MUST be the last, see the comment of SSPI - } - - return group +type AuthMiddleware struct { + AllowOAuth2 web.PreMiddlewareProvider + AllowBasic web.PreMiddlewareProvider + MiddlewareHandler func(*context.Context) } -func webAuth(authMethod auth_service.Method) func(*context.Context) { - return func(ctx *context.Context) { - ar, err := common.AuthShared(ctx.Base, ctx.Session, authMethod) +func newWebAuthMiddleware() *AuthMiddleware { + type keyAllowOAuth2 struct{} + type keyAllowBasic struct{} + webAuth := &AuthMiddleware{} + + middlewareSetContextValue := func(key, val any) web.PreMiddlewareProvider { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dataStore := reqctx.GetRequestDataStore(r.Context()) + dataStore.SetContextValue(key, val) + next.ServeHTTP(w, r) + }) + } + } + + webAuth.AllowBasic = middlewareSetContextValue(keyAllowBasic{}, true) + webAuth.AllowOAuth2 = middlewareSetContextValue(keyAllowOAuth2{}, true) + + enableSSPI := setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) + webAuth.MiddlewareHandler = func(ctx *context.Context) { + allowBasic := ctx.GetContextValue(keyAllowBasic{}) == true + allowOAuth2 := ctx.GetContextValue(keyAllowOAuth2{}) == true + + group := auth_service.NewGroup() + + // Most auth methods should ignore the user id stored in the session. + // If the auth succeeds, it must use the user id from the auth method to make sure the new login succeeds. + if allowOAuth2 { + group.Add(&auth_service.OAuth2{}) + } + if allowBasic { + group.Add(&auth_service.Basic{}) + } + + // Sessionless means the route's auth can be done without web ui, then it doesn't need to create a session + // For example: accessing git via http, access rss feeds, downloading attachments, etc + isSessionless := allowOAuth2 || allowBasic + + if setting.Service.EnableReverseProxyAuth { + // reverse-proxy should before Session, otherwise the header will be ignored if user has login + group.Add(&auth_service.ReverseProxy{CreateSession: !isSessionless}) + } + + // The Session plugin will skip authentication for users that have already signed in. + group.Add(&auth_service.Session{}) + + if enableSSPI { + // it MUST be the last, see the comment of SSPI + group.Add(&auth_service.SSPI{CreateSession: !isSessionless}) + } + + ar, err := common.AuthShared(ctx.Base, ctx.Session, group) if err != nil { log.Error("Failed to verify user: %v", err) ctx.HTTPError(http.StatusUnauthorized, "Failed to authenticate user") @@ -129,6 +162,7 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) { _ = ctx.Session.Delete("uid") } } + return webAuth } // verifyAuthWithOptions checks authentication according to options @@ -223,6 +257,9 @@ const RouterMockPointBeforeWebRoutes = "before-web-routes" func Routes() *web.Router { routes := web.NewRouter() + // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route + routes.BeforeRouting(chi_middleware.GetHead) + routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler routes.Methods("GET, HEAD, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc()) routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) @@ -260,10 +297,8 @@ func Routes() *web.Router { mid = append(mid, common.MustInitSessioner(), context.Contexter()) // Get user from session if logged in. - mid = append(mid, webAuth(buildAuthGroup())) - - // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route - mid = append(mid, chi_middleware.GetHead) + webAuth := newWebAuthMiddleware() + mid = append(mid, webAuth.MiddlewareHandler) if setting.API.EnableSwagger { // Note: The route is here but no in API routes because it renders a web page @@ -272,10 +307,12 @@ func Routes() *web.Router { mid = append(mid, goGet) mid = append(mid, common.PageGlobalData) + mid = append(mid, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes)) webRoutes := web.NewRouter() - webRoutes.Use(mid...) - webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes)) + webRoutes.AfterRouting(mid...) + registerWebRoutes(webRoutes, webAuth) + routes.Mount("", webRoutes) return routes } @@ -288,7 +325,7 @@ func Routes() *web.Router { var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: true}) // registerWebRoutes register routes -func registerWebRoutes(m *web.Router) { +func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { // required to be signed in or signed out reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) @@ -565,7 +602,7 @@ func registerWebRoutes(m *web.Router) { m.Methods("POST, OPTIONS", "/access_token", web.Bind(forms.AccessTokenForm{}), auth.AccessTokenOAuth) m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys) m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth) - }, optionsCorsHandler(), optSignInFromAnyOrigin) + }, optionsCorsHandler(), webAuth.AllowOAuth2, optSignInFromAnyOrigin) }, oauth2Enabled) m.Group("/user/settings", func() { @@ -816,8 +853,9 @@ func registerWebRoutes(m *web.Router) { // ***** END: Admin ***** m.Group("", func() { - m.Get("/{username}", user.UsernameSubRoute) - m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment) + // it handles "username.rss" in the handler, so allow basic auth as other rss/atom routes + m.Get("/{username}", webAuth.AllowBasic, user.UsernameSubRoute) + m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment) }, optSignIn) m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.ActionUserFollow) @@ -1188,7 +1226,7 @@ func registerWebRoutes(m *web.Router) { // end "/{username}/{reponame}/settings" // user/org home, including rss feeds like "/{username}/{reponame}.rss" - m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) + m.Get("/{username}/{reponame}", optSignIn, webAuth.AllowBasic, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) @@ -1389,8 +1427,8 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { // repo tags m.Group("/tags", func() { m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList) - m.Get(".rss", feedEnabled, repo.TagsListFeedRSS) - m.Get(".atom", feedEnabled, repo.TagsListFeedAtom) + m.Get(".rss", webAuth.AllowBasic, feedEnabled, repo.TagsListFeedRSS) + m.Get(".atom", webAuth.AllowBasic, feedEnabled, repo.TagsListFeedAtom) m.Get("/list", repo.GetTagList) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag) @@ -1400,13 +1438,13 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { // repo releases m.Group("/releases", func() { m.Get("", repo.Releases) - m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) - m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) + m.Get(".rss", webAuth.AllowBasic, feedEnabled, repo.ReleasesFeedRSS) + m.Get(".atom", webAuth.AllowBasic, feedEnabled, repo.ReleasesFeedAtom) m.Get("/tag/*", repo.SingleRelease) m.Get("/latest", repo.LatestRelease) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) - m.Get("/releases/attachments/{uuid}", repo.GetAttachment) - m.Get("/releases/download/{vTag}/{fileName}", repo.RedirectDownload) + m.Get("/releases/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment) + m.Get("/releases/download/{vTag}/{fileName}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.RedirectDownload) m.Group("/releases", func() { m.Get("/new", repo.NewRelease) m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) @@ -1423,7 +1461,7 @@ func registerWebRoutes(m *web.Router) { // end "/{username}/{reponame}": repo releases m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments - m.Get("/attachments/{uuid}", repo.GetAttachment) + m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment) }, optSignIn, context.RepoAssignment) // end "/{username}/{reponame}": compatibility with old attachments @@ -1492,7 +1530,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) }) m.Group("/workflows/{workflow_name}", func() { - m.Get("/badge.svg", actions.GetWorkflowBadge) + m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge) }) }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) // end "/{username}/{reponame}/actions" @@ -1578,7 +1616,7 @@ func registerWebRoutes(m *web.Router) { m.Group("/archive", func() { m.Get("/*", repo.Download) m.Post("/*", repo.InitiateDownload) - }, repo.MustBeNotEmpty, dlSourceEnabled) + }, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty, dlSourceEnabled) m.Group("/branches", func() { m.Get("/list", repo.GetBranchesList) @@ -1591,7 +1629,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownloadOrLFS) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownloadOrLFS) m.Get("/*", context.RepoRefByType(""), repo.SingleDownloadOrLFS) // "/*" route is deprecated, and kept for backward compatibility - }, repo.MustBeNotEmpty) + }, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty) m.Group("/raw", func() { m.Get("/blob/{sha}", repo.DownloadByID) @@ -1599,7 +1637,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownload) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownload) m.Get("/*", context.RepoRefByType(""), repo.SingleDownload) // "/*" route is deprecated, and kept for backward compatibility - }, repo.MustBeNotEmpty) + }, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty) m.Group("/render", func() { m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RenderFile) @@ -1632,8 +1670,8 @@ func registerWebRoutes(m *web.Router) { m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick) }, repo.MustBeNotEmpty) - m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeedRSS) - m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeedAtom) + m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), webAuth.AllowBasic, feedEnabled, feed.RenderBranchFeedRSS) + m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), webAuth.AllowBasic, feedEnabled, feed.RenderBranchFeedAtom) m.Group("/src", func() { m.Get("", func(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink) }) // there is no "{owner}/{repo}/src" page, so redirect to "{owner}/{repo}" to avoid 404 @@ -1660,9 +1698,14 @@ func registerWebRoutes(m *web.Router) { m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer) }, optSignIn, context.RepoAssignment) - common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters + // git lfs uses its own jwt key, and it handles the token & auth by itself, it conflicts with the general "OAuth2" auth method + // pattern: "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters + common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, webAuth.AllowBasic, repo.CorsHandler(), optSignInFromAnyOrigin) - addOwnerRepoGitHTTPRouters(m) // "/{username}/{reponame}/{git-paths}": git http support + // Some users want to use "web-based git client" to access Gitea's repositories, + // so the CORS handler and OPTIONS method are used. + // pattern: "/{username}/{reponame}/{git-paths}": git http support + addOwnerRepoGitHTTPRouters(m, repo.HTTPGitEnabledHandler, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb()) m.Group("/notifications", func() { m.Get("", user.Notifications) diff --git a/services/auth/auth.go b/services/auth/auth.go index 90e2115bc5f..ebe8277f77b 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -8,38 +8,16 @@ import ( "errors" "fmt" "net/http" - "regexp" - "strings" - "sync" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/webauthn" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/session" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" user_service "code.gitea.io/gitea/services/user" ) -type globalVarsStruct struct { - gitRawOrAttachPathRe *regexp.Regexp - lfsPathRe *regexp.Regexp - archivePathRe *regexp.Regexp - feedPathRe *regexp.Regexp - feedRefPathRe *regexp.Regexp -} - -var globalVars = sync.OnceValue(func() *globalVarsStruct { - return &globalVarsStruct{ - gitRawOrAttachPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`), - lfsPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/info/lfs/`), - archivePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/archive/`), - feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom" - feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..." - } -}) - type ErrUserAuthMessage string func (e ErrUserAuthMessage) Error() string { @@ -60,66 +38,6 @@ func Init() { webauthn.Init() } -type authPathDetector struct { - req *http.Request - vars *globalVarsStruct -} - -func newAuthPathDetector(req *http.Request) *authPathDetector { - return &authPathDetector{req: req, vars: globalVars()} -} - -// isAPIPath returns true if the specified URL is an API path -func (a *authPathDetector) isAPIPath() bool { - return strings.HasPrefix(a.req.URL.Path, "/api/") -} - -// isAttachmentDownload check if request is a file download (GET) with URL to an attachment -func (a *authPathDetector) isAttachmentDownload() bool { - return strings.HasPrefix(a.req.URL.Path, "/attachments/") && a.req.Method == http.MethodGet -} - -func (a *authPathDetector) isFeedRequest(req *http.Request) bool { - if !setting.Other.EnableFeed { - return false - } - if req.Method != http.MethodGet { - return false - } - return a.vars.feedPathRe.MatchString(req.URL.Path) || a.vars.feedRefPathRe.MatchString(req.URL.Path) -} - -// isContainerPath checks if the request targets the container endpoint -func (a *authPathDetector) isContainerPath() bool { - return strings.HasPrefix(a.req.URL.Path, "/v2/") -} - -func (a *authPathDetector) isGitRawOrAttachPath() bool { - return a.vars.gitRawOrAttachPathRe.MatchString(a.req.URL.Path) -} - -func (a *authPathDetector) isGitRawOrAttachOrLFSPath() bool { - if a.isGitRawOrAttachPath() { - return true - } - if setting.LFS.StartServer { - return a.vars.lfsPathRe.MatchString(a.req.URL.Path) - } - return false -} - -func (a *authPathDetector) isArchivePath() bool { - return a.vars.archivePathRe.MatchString(a.req.URL.Path) -} - -func (a *authPathDetector) isAuthenticatedTokenRequest() bool { - switch a.req.URL.Path { - case "/login/oauth/userinfo", "/login/oauth/introspect": - return true - } - return false -} - // handleSignIn clears existing session variables and stores new ones for the specified user object func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) { // We need to regenerate the session... diff --git a/services/auth/auth_test.go b/services/auth/auth_test.go deleted file mode 100644 index c45f312c90f..00000000000 --- a/services/auth/auth_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package auth - -import ( - "net/http" - "testing" - - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" - - "github.com/stretchr/testify/assert" -) - -func Test_isGitRawOrLFSPath(t *testing.T) { - tests := []struct { - path string - - want bool - }{ - { - "/owner/repo/git-upload-pack", - true, - }, - { - "/owner/repo/git-receive-pack", - true, - }, - { - "/owner/repo/info/refs", - true, - }, - { - "/owner/repo/HEAD", - true, - }, - { - "/owner/repo/objects/info/alternates", - true, - }, - { - "/owner/repo/objects/info/http-alternates", - true, - }, - { - "/owner/repo/objects/info/packs", - true, - }, - { - "/owner/repo/objects/info/blahahsdhsdkla", - true, - }, - { - "/owner/repo/objects/01/23456789abcdef0123456789abcdef01234567", - true, - }, - { - "/owner/repo/objects/pack/pack-123456789012345678921234567893124567894.pack", - true, - }, - { - "/owner/repo/objects/pack/pack-0123456789abcdef0123456789abcdef0123456.idx", - true, - }, - { - "/owner/repo/raw/branch/foo/fanaso", - true, - }, - { - "/owner/repo/stars", - false, - }, - { - "/notowner", - false, - }, - { - "/owner/repo", - false, - }, - { - "/owner/repo/commit/123456789012345678921234567893124567894", - false, - }, - { - "/owner/repo/releases/download/tag/repo.tar.gz", - true, - }, - { - "/owner/repo/attachments/6d92a9ee-5d8b-4993-97c9-6181bdaa8955", - true, - }, - } - - defer test.MockVariableValue(&setting.LFS.StartServer)() - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - req, _ := http.NewRequest(http.MethodPost, "http://localhost"+tt.path, nil) - setting.LFS.StartServer = false - assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) - - setting.LFS.StartServer = true - assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) - }) - } - - lfsTests := []string{ - "/owner/repo/info/lfs/", - "/owner/repo/info/lfs/objects/batch", - "/owner/repo/info/lfs/objects/oid/filename", - "/owner/repo/info/lfs/objects/oid", - "/owner/repo/info/lfs/objects", - "/owner/repo/info/lfs/verify", - "/owner/repo/info/lfs/locks", - "/owner/repo/info/lfs/locks/verify", - "/owner/repo/info/lfs/locks/123/unlock", - } - for _, tt := range lfsTests { - t.Run(tt, func(t *testing.T) { - req, _ := http.NewRequest(http.MethodPost, tt, nil) - setting.LFS.StartServer = false - got := newAuthPathDetector(req).isGitRawOrAttachOrLFSPath() - assert.Equalf(t, setting.LFS.StartServer, got, "isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, globalVars().gitRawOrAttachPathRe.MatchString(tt)) - - setting.LFS.StartServer = true - got = newAuthPathDetector(req).isGitRawOrAttachOrLFSPath() - assert.Equalf(t, setting.LFS.StartServer, got, "isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer) - }) - } -} - -func Test_isFeedRequest(t *testing.T) { - tests := []struct { - want bool - path string - }{ - {true, "/user.rss"}, - {true, "/user/repo.atom"}, - {false, "/user/repo"}, - {false, "/use/repo/file.rss"}, - - {true, "/org/repo/rss/branch/xxx"}, - {true, "/org/repo/atom/tag/xxx"}, - {false, "/org/repo/branch/main/rss/any"}, - {false, "/org/atom/any"}, - } - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - req, _ := http.NewRequest(http.MethodGet, "http://localhost"+tt.path, nil) - assert.Equal(t, tt.want, newAuthPathDetector(req).isFeedRequest(req)) - }) - } -} diff --git a/services/auth/basic.go b/services/auth/basic.go index 3161d7f33d4..dda6451c365 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -41,13 +41,6 @@ func (b *Basic) Name() string { } func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, passwd string }) { - // Basic authentication should only fire on API, Feed, Download, Archives or on Git or LFSPaths - // Not all feed (rss/atom) clients feature the ability to add cookies or headers, so we need to allow basic auth for feeds - detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() { - return ret - } - authHeader := req.Header.Get("Authorization") if authHeader == "" { return ret diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 86903b0ce17..877237e96e7 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -152,13 +152,6 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS // If verification is successful returns an existing user object. // Returns nil if verification fails. func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { - // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs - detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isAuthenticatedTokenRequest() && - !detector.isGitRawOrAttachPath() && !detector.isArchivePath() { - return nil, nil //nolint:nilnil // the auth method is not applicable - } - token, ok := parseToken(req) if !ok { return nil, nil //nolint:nilnil // the auth method is not applicable diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go index 064b263a676..e9b08d75f27 100644 --- a/services/auth/reverseproxy.go +++ b/services/auth/reverseproxy.go @@ -29,7 +29,9 @@ const ReverseProxyMethodName = "reverse_proxy" // On successful authentication the proxy is expected to populate the username in the // "setting.ReverseProxyAuthUser" header. Optionally it can also populate the email of the // user in the "setting.ReverseProxyAuthEmail" header. -type ReverseProxy struct{} +type ReverseProxy struct { + CreateSession bool +} // getUserName extracts the username from the "setting.ReverseProxyAuthUser" header func (r *ReverseProxy) getUserName(req *http.Request) string { @@ -115,9 +117,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da } } - // Make sure requests to API paths, attachment downloads, git and LFS do not create a new session - detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isGitRawOrAttachOrLFSPath() { + if r.CreateSession { if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) { handleSignIn(w, req, sess, user) } diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 64507539359..c21978f55ae 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -46,7 +46,9 @@ var ( // The SSPI plugin is expected to be executed last, as it returns 401 status code if negotiation // fails (or if negotiation should continue), which would prevent other authentication methods // to execute at all. -type SSPI struct{} +type SSPI struct { + CreateSession bool +} // Name represents the name of auth method func (s *SSPI) Name() string { @@ -118,9 +120,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, } } - // Make sure requests to API paths and PWA resources do not create a new session - detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isAttachmentDownload() { + if s.CreateSession { handleSignIn(w, req, sess, user) } @@ -147,18 +147,9 @@ func (s *SSPI) getConfig(ctx context.Context) (*sspi.Source, error) { } func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) { - shouldAuth = false - path := strings.TrimSuffix(req.URL.Path, "/") - if path == "/user/login" { - if req.FormValue("user_name") != "" && req.FormValue("password") != "" { - shouldAuth = false - } else if req.FormValue("auth_with_sspi") == "1" { - shouldAuth = true - } - } else { - detector := newAuthPathDetector(req) - shouldAuth = detector.isAPIPath() || detector.isAttachmentDownload() - } + // SSPI is only applicable for login requests with "auth_with_sspi" form value set to "1" + // See the template code with "auth_with_sspi" + shouldAuth = req.URL.Path == "/user/login" && req.FormValue("auth_with_sspi") == "1" return shouldAuth } diff --git a/services/context/api.go b/services/context/api.go index abd1f9f67e3..b49bf9b42c6 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -163,7 +163,7 @@ func GetAPIContext(req *http.Request) *APIContext { return req.Context().Value(apiContextKey).(*APIContext) } -func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string { +func genAPILinks(curURL *url.URL, total int64, pageSize, curPage int) []string { page := NewPagination(total, pageSize, curPage, 0) paginater := page.Paginater links := make([]string, 0, 4) @@ -204,7 +204,8 @@ func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string { } // SetLinkHeader sets pagination link header by given total number and page size. -func (ctx *APIContext) SetLinkHeader(total, pageSize int) { +// "count" is usually from database result "count int64", so it also uses int64, +func (ctx *APIContext) SetLinkHeader(total int64, pageSize int) { links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page")) if len(links) > 0 { diff --git a/services/context/captcha.go b/services/context/captcha.go index 79278180b76..b1129a05b20 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/pagination.go b/services/context/pagination.go index 21efab8b121..da27e617570 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -6,6 +6,7 @@ package context import ( "fmt" "html/template" + "math" "net/http" "net/url" "slices" @@ -22,11 +23,13 @@ type Pagination struct { } // NewPagination creates a new instance of the Pagination struct. +// "total" is usually from database result "count int64", so it also uses int64 // "pagingNum" is "page size" or "limit", "current" is "page" // total=-1 means only showing prev/next -func NewPagination(total, pagingNum, current, numPages int) *Pagination { +func NewPagination(total int64, pagingNum, current, numPages int) *Pagination { + totalInt := int(min(total, int64(math.MaxInt))) p := &Pagination{} - p.Paginater = paginator.New(total, pagingNum, current, numPages) + p.Paginater = paginator.New(totalInt, pagingNum, current, numPages) return p } diff --git a/services/context/repo.go b/services/context/repo.go index 674da577b98..c39ab551f28 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 e1cd30705e7..391960b3698 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 bf17024d2d0..d809e3778d3 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/feed/feed.go b/services/feed/feed.go index 1dbd2e0e26f..8d39a34fbc9 100644 --- a/services/feed/feed.go +++ b/services/feed/feed.go @@ -18,10 +18,10 @@ import ( "code.gitea.io/gitea/modules/util" ) -func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int, error) { +func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) { opts.DontCount = opts.RequestedTeam == nil && opts.Date == "" results, cnt, err := activities_model.GetFeeds(ctx, opts) - return results, util.Iif(opts.DontCount, -1, int(cnt)), err + return results, util.Iif(opts.DontCount, -1, cnt), err } // GetFeeds returns actions according to the provided options diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 64e7f3f02bd..f25659e1100 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -240,16 +240,15 @@ func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, re type unadoptedRepositories struct { repositories []string - index int - start int - end int + count int64 + start, end int64 } func (unadopted *unadoptedRepositories) add(repository string) { - if unadopted.index >= unadopted.start && unadopted.index < unadopted.end { + if unadopted.count >= unadopted.start && unadopted.count < unadopted.end { unadopted.repositories = append(unadopted.repositories, repository) } - unadopted.index++ + unadopted.count++ } func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesToCheck []string, unadopted *unadoptedRepositories) error { @@ -291,7 +290,7 @@ func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesT } // ListUnadoptedRepositories lists all the unadopted repositories that match the provided query -func ListUnadoptedRepositories(ctx context.Context, query string, opts *db.ListOptions) ([]string, int, error) { +func ListUnadoptedRepositories(ctx context.Context, query string, opts *db.ListOptions) ([]string, int64, error) { globUser, _ := glob.Compile("*") globRepo, _ := glob.Compile("*") @@ -311,12 +310,12 @@ func ListUnadoptedRepositories(ctx context.Context, query string, opts *db.ListO } var repoNamesToCheck []string - start := (opts.Page - 1) * opts.PageSize + start := int64((opts.Page - 1) * opts.PageSize) unadopted := &unadoptedRepositories{ repositories: make([]string, 0, opts.PageSize), start: start, - end: start + opts.PageSize, - index: 0, + end: start + int64(opts.PageSize), + count: 0, } var userName string @@ -372,5 +371,5 @@ func ListUnadoptedRepositories(ctx context.Context, query string, opts *db.ListO return nil, 0, err } - return unadopted.repositories, unadopted.index, nil + return unadopted.repositories, unadopted.count, nil } diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go index 46f2f484175..a7de9180859 100644 --- a/services/repository/adopt_test.go +++ b/services/repository/adopt_test.go @@ -20,20 +20,20 @@ import ( ) func TestCheckUnadoptedRepositories_Add(t *testing.T) { - start := 10 - end := 20 + const start = 10 + const end = 20 unadopted := &unadoptedRepositories{ start: start, end: end, - index: 0, + count: 0, } - total := 30 + const total = 30 for range total { unadopted.add("something") } - assert.Equal(t, total, unadopted.index) + assert.EqualValues(t, total, unadopted.count) assert.Len(t, unadopted.repositories, end-start) } @@ -64,7 +64,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) { err = checkUnadoptedRepositories(t.Context(), userName, []string{repoName}, unadopted) assert.NoError(t, err) assert.Empty(t, unadopted.repositories) - assert.Equal(t, 0, unadopted.index) + assert.Zero(t, unadopted.count) } func TestListUnadoptedRepositories_ListOptions(t *testing.T) { @@ -78,13 +78,13 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) { opts := db.ListOptions{Page: 1, PageSize: 1} repoNames, count, err := ListUnadoptedRepositories(t.Context(), "", &opts) assert.NoError(t, err) - assert.Equal(t, 2, count) + assert.EqualValues(t, 2, count) assert.Equal(t, unadoptedList[0], repoNames[0]) opts = db.ListOptions{Page: 2, PageSize: 1} repoNames, count, err = ListUnadoptedRepositories(t.Context(), "", &opts) assert.NoError(t, err) - assert.Equal(t, 2, count) + assert.EqualValues(t, 2, count) assert.Equal(t, unadoptedList[1], repoNames[0]) } diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go index fc032244b5c..a4dd3c709d5 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 e621c04b438..e4ecbd06c4e 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"}}

{{if and .Repository .CanWritePackages}} - {{$packagesUrl := URLJoin .Owner.HomeLink "-" "packages"}} + {{$packagesUrl := print .Owner.HomeLink "/-/packages"}}

{{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 9cd4b2a1223..bc91adb64f7 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}}

{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink "?bypass-blame-ignore=true"}}

diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl index 6779948da5e..48d6d18b06a 100644 --- a/templates/user/auth/captcha.tmpl +++ b/templates/user/auth/captcha.tmpl @@ -10,7 +10,7 @@
- + {{else if eq .CaptchaType "hcaptcha"}}
diff --git a/tests/integration/migrate_test.go b/tests/integration/migrate_test.go index 5521c786d9e..8c8f053ede3 100644 --- a/tests/integration/migrate_test.go +++ b/tests/integration/migrate_test.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/migrations" @@ -126,7 +127,7 @@ func Test_MigrateFromGiteaToGitea(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) - resp, err := http.Get("https://gitea.com/gitea") + resp, err := httplib.NewRequest("https://gitea.com/gitea/test_repo.git", "GET").SetReadWriteTimeout(5 * time.Second).Response() if err != nil || resp.StatusCode != http.StatusOK { if resp != nil { resp.Body.Close()