refactor: username sub route (#39967)

Remove the workaround for chi router, do as much as possible in
middlewares

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
authored and GitHub committed 2026-10-24 16:27:56 +08:00
1 parent fc49368845
commit 1d8474ee1f
13 files changed
+103 -117

No files matched your search

+1 -1
View File
@@ -660,7 +660,7 @@ var (
} }
// These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc. // These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc.
// DO NOT add any new stuff! The paths with these names are processed by `/{username}` handler (UsernameSubRoute) manually. // HINT: OWNER-NAME-RESERVED-EXT: DO NOT add any new stuff! The paths with these names are processed by UsernamePreRoute handler manually.
reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom", "*.png"} reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom", "*.png"}
) )
+9 -6
View File
@@ -260,18 +260,21 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
return items, err return items, err
} }
const (
ExtNameRss = ".rss"
ExtNameAtom = ".atom"
ContentTypeRssXml = "application/rss+xml"
ContentTypeAtomXml = "application/atom+xml"
)
// GetFeedType return if it is a feed request and altered name and feed type. // GetFeedType return if it is a feed request and altered name and feed type.
func GetFeedType(name string, req *http.Request) (showFeed bool, feedType string) { func GetFeedType(name string, req *http.Request) (showFeed bool, feedType string) {
if strings.HasSuffix(name, ".rss") || if strings.HasSuffix(name, ExtNameRss) || strings.Contains(req.Header.Get("Accept"), ContentTypeRssXml) {
strings.Contains(req.Header.Get("Accept"), "application/rss+xml") {
return true, "rss" return true, "rss"
} }
if strings.HasSuffix(name, ExtNameAtom) || strings.Contains(req.Header.Get("Accept"), ContentTypeAtomXml) {
if strings.HasSuffix(name, ".atom") ||
strings.Contains(req.Header.Get("Accept"), "application/atom+xml") {
return true, "atom" return true, "atom"
} }
return false, "" return false, ""
} }
+5 -9
View File
@@ -9,7 +9,6 @@ import (
activities_model "gitea.dev/models/activities" activities_model "gitea.dev/models/activities"
"gitea.dev/models/organization" "gitea.dev/models/organization"
"gitea.dev/models/renderhelper" "gitea.dev/models/renderhelper"
user_model "gitea.dev/models/user"
"gitea.dev/modules/markup/markdown" "gitea.dev/modules/markup/markdown"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/services/context" "gitea.dev/services/context"
@@ -30,16 +29,13 @@ func ShowUserFeedAtom(ctx *context.Context) {
// showUserFeed show user activity as RSS / Atom feed // showUserFeed show user activity as RSS / Atom feed
func showUserFeed(ctx *context.Context, formatType string) { func showUserFeed(ctx *context.Context, formatType string) {
isOrganisation := ctx.ContextUser.IsOrganization() if !setting.Other.EnableFeed {
if !setting.Other.EnableFeed ||
isOrganisation && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
!isOrganisation && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(nil) ctx.NotFound(nil)
return return
} }
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
if ctx.IsSigned && isOrganisation && !includePrivate { if ctx.IsSigned && ctx.ContextUser.IsOrganization() && !includePrivate {
// When feed is requested by a member of the organization, // When feed is requested by a member of the organization,
// include the private repo's the member has access to. // include the private repo's the member has access to.
isOrgMember, err := organization.IsOrganizationMember(ctx, ctx.ContextUser.ID, ctx.Doer.ID) isOrgMember, err := organization.IsOrganizationMember(ctx, ctx.ContextUser.ID, ctx.Doer.ID)
@@ -54,7 +50,7 @@ func showUserFeed(ctx *context.Context, formatType string) {
RequestedUser: ctx.ContextUser, RequestedUser: ctx.ContextUser,
Actor: ctx.Doer, Actor: ctx.Doer,
IncludePrivate: includePrivate, IncludePrivate: includePrivate,
OnlyPerformedBy: !isOrganisation, OnlyPerformedBy: !ctx.ContextUser.IsOrganization(),
IncludeDeleted: false, IncludeDeleted: false,
Date: ctx.FormString("date"), Date: ctx.FormString("date"),
}) })
@@ -90,12 +86,12 @@ func showUserFeed(ctx *context.Context, formatType string) {
// writeFeed write a feeds.Feed as atom or rss to ctx.Resp // writeFeed write a feeds.Feed as atom or rss to ctx.Resp
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) { func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
if formatType == "atom" { if formatType == "atom" {
ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8") ctx.Resp.Header().Set("Content-Type", ContentTypeAtomXml+"; charset=utf-8")
if err := feed.WriteAtom(ctx.Resp); err != nil { if err := feed.WriteAtom(ctx.Resp); err != nil {
ctx.ServerError("Render Atom failed", err) ctx.ServerError("Render Atom failed", err)
} }
} else { } else {
ctx.Resp.Header().Set("Content-Type", "application/rss+xml;charset=utf-8") ctx.Resp.Header().Set("Content-Type", ContentTypeRssXml+"; charset=utf-8")
if err := feed.WriteRss(ctx.Resp); err != nil { if err := feed.WriteRss(ctx.Resp); err != nil {
ctx.ServerError("Render RSS failed", err) ctx.ServerError("Render RSS failed", err)
} }
+2 -6
View File
@@ -96,18 +96,14 @@ func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) strin
if len(repo.DefaultBranch) == 0 { if len(repo.DefaultBranch) == 0 {
return def return def
} }
if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil {
return def
}
// a token that was not granted repository read scope must not learn repository details, even when the // a token that was not granted repository read scope must not learn repository details, even when the
// account behind it could read the repo through the web UI // account behind it could read the repo through the web UI
if !goGetTokenCanReadRepo(ctx) { if !goGetTokenCanReadRepo(ctx) {
return def return def
} }
// the caller must be able to read the code and see the owner: a limited/private owner hides its repos // the caller must be able to read the code
// from anonymous/non-member callers even when the repo itself is public
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil || !perm.CanRead(unit.TypeCode) || !user_model.IsUserVisibleToViewer(ctx, repo.Owner, ctx.Doer) { if err != nil || !perm.CanRead(unit.TypeCode) {
return def return def
} }
return repo.DefaultBranch return repo.DefaultBranch
+5
View File
@@ -17,6 +17,7 @@ import (
"gitea.dev/modules/sitemap" "gitea.dev/modules/sitemap"
"gitea.dev/modules/structs" "gitea.dev/modules/structs"
"gitea.dev/modules/templates" "gitea.dev/modules/templates"
"gitea.dev/routers/web/feed"
"gitea.dev/routers/web/user" "gitea.dev/routers/web/user"
"gitea.dev/services/context" "gitea.dev/services/context"
) )
@@ -24,6 +25,10 @@ import (
const tplHome templates.TplName = "home" const tplHome templates.TplName = "home"
func Home(ctx *context.Context) { func Home(ctx *context.Context) {
if _, feedType := feed.GetFeedType(ctx.Req.URL.Path, ctx.Req); feedType != "" {
ctx.Resp.WriteHeader(http.StatusUnsupportedMediaType)
return
}
if ctx.IsSigned { if ctx.IsSigned {
user.Dashboard(ctx) user.Dashboard(ctx)
return return
+1 -9
View File
@@ -5,7 +5,6 @@ package org
import ( import (
"net/http" "net/http"
"strings"
"gitea.dev/models/db" "gitea.dev/models/db"
"gitea.dev/models/organization" "gitea.dev/models/organization"
@@ -25,14 +24,7 @@ const tplOrgHome templates.TplName = "org/home"
// Home show organization home page // Home show organization home page
func Home(ctx *context.Context) { func Home(ctx *context.Context) {
uname := ctx.PathParam("username") ctx.SetPathParam("org", ctx.PathParam("username"))
if strings.HasSuffix(uname, ".keys") || strings.HasSuffix(uname, ".gpg") {
ctx.NotFound(nil)
return
}
ctx.SetPathParam("org", uname)
context.OrgAssignment(context.OrgAssignmentOptions{})(ctx) context.OrgAssignment(context.OrgAssignmentOptions{})(ctx)
if ctx.Written() { if ctx.Written() {
return return
-7
View File
@@ -8,7 +8,6 @@ import (
"net/url" "net/url"
activities_model "gitea.dev/models/activities" activities_model "gitea.dev/models/activities"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/services/context" "gitea.dev/services/context"
) )
@@ -52,12 +51,6 @@ func DashboardHeatmap(ctx *context.Context) {
ctx.NotFound(nil) ctx.NotFound(nil)
return return
} }
if ctx.Org.Organization == nil && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
// this end point is mounted on different routes, so we need to
// check if the user is visible to the viewer when there is no organization context (no visibility check)
ctx.NotFound(nil)
return
}
var data []*activities_model.UserHeatmapData var data []*activities_model.UserHeatmapData
var err error var err error
if ctx.Org.Organization == nil { if ctx.Org.Organization == nil {
+41 -42
View File
@@ -6,8 +6,8 @@ package user
import ( import (
"bytes" "bytes"
"fmt"
"net/http" "net/http"
"path"
"regexp" "regexp"
"slices" "slices"
"sort" "sort"
@@ -43,6 +43,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/go-chi/chi/v5"
"xorm.io/builder" "xorm.io/builder"
) )
@@ -702,52 +703,50 @@ func ShowGPGKeys(ctx *context.Context) {
ctx.PlainTextBytes(http.StatusOK, buf.Bytes()) ctx.PlainTextBytes(http.StatusOK, buf.Bytes())
} }
func UsernameSubRoute(ctx *context.Context) { // UsernamePreRoute rewrites the route path if the username has a suffix, so that the correct handler can be called.
// WORKAROUND to support usernames with "." in it func UsernamePreRoute(next http.Handler) http.Handler {
// https://github.com/go-chi/chi/issues/781 // HINT: OWNER-NAME-RESERVED-EXT: use the reserved extensions to check and rewrite requests
username := ctx.PathParam("username") suffixes := container.SetOf(".png", ".keys", ".gpg", feed.ExtNameRss, feed.ExtNameAtom)
reloadParam := func(suffix string) (success bool) { rewriteRoutePath := func(rctx *chi.Context, r *http.Request) (ownerPath, rewrittenExt string) {
ctx.SetPathParam("username", strings.TrimSuffix(username, suffix)) if rctx.RouteMethod != "HEAD" && rctx.RouteMethod != "GET" {
context.UserAssignmentWeb()(ctx) return "", ""
if ctx.Written() { }
return false // if there is no slash in the path, then it is possible to be a username or a username with a suffix
possibleUsernameWithSuffix := rctx.RoutePath != "" && strings.IndexByte(rctx.RoutePath[1:], '/') == -1
if !possibleUsernameWithSuffix {
return "", ""
} }
// check view permissions // HINT: USERNAME-AVATAR-APPLE-TOUCH-ICON: skip rewriting the path if it is an apple touch icon request
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { isAppleTouchIcon := rctx.RoutePath == "/apple-touch-icon.png" || rctx.RoutePath == "/apple-touch-icon-precomposed.png"
ctx.NotFound(fmt.Errorf("%s", ctx.ContextUser.Name)) if isAppleTouchIcon {
return false return "", ""
} }
return true
// try to extract the suffix from the path, if it is a valid suffix, then rewrite the path to "/-/ownername.png/{username}"
ext := path.Ext(rctx.RoutePath)
if suffixes.Contains(ext) {
return rctx.RoutePath[:len(rctx.RoutePath)-len(ext)], ext
}
// also try to rewrite "/username" to "/-/ownername.rss/{username}" if the Accept header is for RSS or Atom
if strings.Contains(r.Header.Get("Accept"), feed.ContentTypeRssXml) {
return rctx.RoutePath, feed.ExtNameRss
} else if strings.Contains(r.Header.Get("Accept"), feed.ContentTypeAtomXml) {
return rctx.RoutePath, feed.ExtNameAtom
}
return "", ""
} }
switch {
case strings.HasSuffix(username, ".png"): return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if reloadParam(".png") { rctx := chi.RouteContext(r.Context())
AvatarByUsernameSize(ctx) if ownerPath, rewrittenExt := rewriteRoutePath(rctx, r); ownerPath != "/" && rewrittenExt != "" {
// Owner path can be something like "/" (site home) or "/explore" (endpoint of reserved username)
// It's safe to pass the owner path to the handler as is and just let the handler respond 4xx.
// Because the server doesn't respond avatar, keys or feeds for these endpoints.
rctx.RoutePath = "/-/ownername" + rewrittenExt + ownerPath
} }
case strings.HasSuffix(username, ".keys"): next.ServeHTTP(w, r)
if reloadParam(".keys") { })
ShowSSHKeys(ctx)
}
case strings.HasSuffix(username, ".gpg"):
if reloadParam(".gpg") {
ShowGPGKeys(ctx)
}
case strings.HasSuffix(username, ".rss"):
if reloadParam(".rss") {
feed.ShowUserFeedRSS(ctx)
}
case strings.HasSuffix(username, ".atom"):
if reloadParam(".atom") {
feed.ShowUserFeedAtom(ctx)
}
default:
context.UserAssignmentWeb()(ctx)
if !ctx.Written() {
ctx.Data["EnableFeed"] = setting.Other.EnableFeed
OwnerProfile(ctx)
}
}
} }
func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
+1 -24
View File
@@ -7,7 +7,6 @@ package user
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
activities_model "gitea.dev/models/activities" activities_model "gitea.dev/models/activities"
"gitea.dev/models/db" "gitea.dev/models/db"
@@ -21,7 +20,6 @@ import (
"gitea.dev/modules/optional" "gitea.dev/modules/optional"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/templates" "gitea.dev/modules/templates"
"gitea.dev/routers/web/feed"
"gitea.dev/routers/web/org" "gitea.dev/routers/web/org"
shared_user "gitea.dev/routers/web/shared/user" shared_user "gitea.dev/routers/web/shared/user"
"gitea.dev/services/context" "gitea.dev/services/context"
@@ -35,15 +33,7 @@ const (
// OwnerProfile render profile page for a user or a organization (aka, repo owner) // OwnerProfile render profile page for a user or a organization (aka, repo owner)
func OwnerProfile(ctx *context.Context) { func OwnerProfile(ctx *context.Context) {
if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") { ctx.Data["EnableFeed"] = setting.Other.EnableFeed
feed.ShowUserFeedRSS(ctx)
return
}
if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") {
feed.ShowUserFeedAtom(ctx)
return
}
if ctx.ContextUser.IsOrganization() { if ctx.ContextUser.IsOrganization() {
org.Home(ctx) org.Home(ctx)
} else { } else {
@@ -52,12 +42,6 @@ func OwnerProfile(ctx *context.Context) {
} }
func userProfile(ctx *context.Context) { func userProfile(ctx *context.Context) {
// check view permissions
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(fmt.Errorf("%s", ctx.ContextUser.Name))
return
}
ctx.Data["Title"] = ctx.ContextUser.DisplayName() ctx.Data["Title"] = ctx.ContextUser.DisplayName()
ctx.Data["PageIsUserProfile"] = true ctx.Data["PageIsUserProfile"] = true
@@ -322,13 +306,6 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
// ActionUserFollow is for follow/unfollow user request // ActionUserFollow is for follow/unfollow user request
func ActionUserFollow(ctx *context.Context) { func ActionUserFollow(ctx *context.Context) {
isOrg := ctx.ContextUser.IsOrganization()
if isOrg && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
!isOrg && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(nil)
return
}
var err error var err error
switch ctx.FormString("action") { switch ctx.FormString("action") {
case "follow": case "follow":
+19 -10
View File
@@ -258,15 +258,18 @@ func Routes() *web.Router {
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
routes.BeforeRouting(chi_middleware.GetHead) routes.BeforeRouting(chi_middleware.GetHead)
routes.BeforeRouting(user.UsernamePreRoute)
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest) routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest)
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc()) routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc())
routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars)) routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
routes.Methods("GET, HEAD", "/favicon.ico", misc.StaticRedirect("/assets/img/favicon.png"))
// HINT: USERNAME-AVATAR-APPLE-TOUCH-ICON: these two paths conflict with "/{username}.png"
routes.Methods("GET, HEAD", "/apple-touch-icon.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) routes.Methods("GET, HEAD", "/apple-touch-icon.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
routes.Methods("GET, HEAD", "/apple-touch-icon-precomposed.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) routes.Methods("GET, HEAD", "/apple-touch-icon-precomposed.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
routes.Methods("GET, HEAD", "/favicon.ico", misc.StaticRedirect("/assets/img/favicon.png"))
var mid []any var mid []any
@@ -906,13 +909,19 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, adminReq, ctxDataSet(reqctx.ContextData{"EnableOAuth2": setting.OAuth2.Enabled, "EnablePackages": setting.Packages.Enabled})) }, adminReq, ctxDataSet(reqctx.ContextData{"EnableOAuth2": setting.OAuth2.Enabled, "EnablePackages": setting.Packages.Enabled}))
// ***** END: Admin ***** // ***** END: Admin *****
m.Group("", func() { m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), webAuth.AllowBasic, webAuth.AllowOAuth2, optSignIn, 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) m.Group("", func() {
// "UsernamePreRoute" is used to rewrite username with a suffix like "/username.png" to "/-/ownername.png/username"
m.Get("/-/ownername.png/{username}", user.AvatarByUsernameSize)
m.Get("/-/ownername.keys/{username}", user.ShowSSHKeys)
m.Get("/-/ownername.gpg/{username}", user.ShowGPGKeys)
m.Get("/-/ownername.rss/{username}", feed.ShowUserFeedRSS)
m.Get("/-/ownername.atom/{username}", feed.ShowUserFeedAtom)
m.Get("/{username}", user.OwnerProfile)
}, webAuth.AllowBasic, optSignIn, context.UserAssignmentWeb(), context.ReqContextUserVisibleToDoer())
m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), context.ReqContextUserVisibleToDoer(), user.ActionUserFollow)
reqRepoAdmin := context.RequireRepoAdmin() reqRepoAdmin := context.RequireRepoAdmin()
reqRepoCodeWriter := context.RequireUnitWriter(unit.TypeCode) reqRepoCodeWriter := context.RequireUnitWriter(unit.TypeCode)
@@ -981,8 +990,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/{org}", func() { m.Group("/{org}", func() {
m.Get("/dashboard", user.Dashboard) m.Get("/dashboard", user.Dashboard)
m.Get("/dashboard/{team}", user.Dashboard) m.Get("/dashboard/{team}", user.Dashboard)
m.Get("/dashboard/-/heatmap", user.DashboardHeatmap) m.Get("/dashboard/-/heatmap", context.ReqContextUserVisibleToDoer(), user.DashboardHeatmap)
m.Get("/dashboard/-/heatmap/{team}", user.DashboardHeatmap) m.Get("/dashboard/-/heatmap/{team}", context.ReqContextUserVisibleToDoer(), user.DashboardHeatmap)
m.Get("/issues", user.Issues) m.Get("/issues", user.Issues)
m.Get("/issues/{team}", user.Issues) m.Get("/issues/{team}", user.Issues)
m.Get("/pulls", user.Pulls) m.Get("/pulls", user.Pulls)
@@ -1132,7 +1141,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/mentions-in-owner", reqAnyRepoUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), org.GetMentionsInOwner) m.Get("/mentions-in-owner", reqAnyRepoUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), org.GetMentionsInOwner)
m.Get("/repositories", org.Repositories) m.Get("/repositories", org.Repositories)
m.Get("/heatmap", user.DashboardHeatmap) m.Get("/heatmap", context.ReqContextUserVisibleToDoer(), user.DashboardHeatmap)
m.Group("/projects", func() { m.Group("/projects", func() {
m.Group("", func() { m.Group("", func() {
+12
View File
@@ -8,10 +8,22 @@ import (
"net/http" "net/http"
"strings" "strings"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/session" "gitea.dev/modules/session"
) )
func ReqContextUserVisibleToDoer() func(ctx *Context) {
return func(ctx *Context) {
isOrg := ctx.ContextUser.IsOrganization()
if isOrg && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
!isOrg && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(nil)
return
}
}
}
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes // UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
func UserAssignmentWeb() func(ctx *Context) { func UserAssignmentWeb() func(ctx *Context) {
return func(ctx *Context) { return func(ctx *Context) {
+5 -1
View File
@@ -134,6 +134,7 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
req = NewRequest(t, "GET", "/"+user31.Name) req = NewRequest(t, "GET", "/"+user31.Name)
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", "/"+user31.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound) MakeRequest(t, NewRequest(t, "GET", "/"+user31.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", "/"+user31.Name+".keys"), http.StatusNotFound)
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+user31.Name+"?action=follow"), http.StatusNotFound) strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+user31.Name+"?action=follow"), http.StatusNotFound)
oldName = user31.Name oldName = user31.Name
@@ -237,6 +238,7 @@ func testRenameReservedUsername(t *testing.T) {
func testExportUserGPGKeys(t *testing.T) { func testExportUserGPGKeys(t *testing.T) {
testExportUserGPGKeys := func(t *testing.T, user, expected string) { testExportUserGPGKeys := func(t *testing.T, user, expected string) {
t.Helper()
session := loginUser(t, user) session := loginUser(t, user)
t.Logf("Testing username %s export gpg keys", user) t.Logf("Testing username %s export gpg keys", user)
req := NewRequest(t, "GET", "/"+user+".gpg") req := NewRequest(t, "GET", "/"+user+".gpg")
@@ -324,7 +326,7 @@ func testGetUserRss(t *testing.T) {
user34 := "the_34-user.with.all.allowedChars" user34 := "the_34-user.with.all.allowedChars"
req := NewRequestf(t, "GET", "/%s.rss", user34) req := NewRequestf(t, "GET", "/%s.rss", user34)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
if assert.Equal(t, "application/rss+xml;charset=utf-8", resp.Header().Get("Content-Type")) { if assert.Equal(t, "application/rss+xml; charset=utf-8", resp.Header().Get("Content-Type")) {
rssDoc := NewHTMLParser(t, resp.Body).Find("channel") rssDoc := NewHTMLParser(t, resp.Body).Find("channel")
title, _ := rssDoc.ChildrenFiltered("title").Html() title, _ := rssDoc.ChildrenFiltered("title").Html()
assert.Equal(t, "Feed of &#34;the_1-user.with.all.allowedChars&#34;", title) assert.Equal(t, "Feed of &#34;the_1-user.with.all.allowedChars&#34;", title)
@@ -339,6 +341,8 @@ func testGetUserRss(t *testing.T) {
req = NewRequestf(t, "GET", "/non-existent-user.rss") req = NewRequestf(t, "GET", "/non-existent-user.rss")
session.MakeRequest(t, req, http.StatusNotFound) session.MakeRequest(t, req, http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", "/").SetHeader("Accept", "application/rss+xml"), http.StatusUnsupportedMediaType)
defer test.MockVariableValue(&setting.Other.EnableFeed, false)() defer test.MockVariableValue(&setting.Other.EnableFeed, false)()
MakeRequest(t, NewRequestf(t, "GET", "/%s.rss", user34), http.StatusNotFound) MakeRequest(t, NewRequestf(t, "GET", "/%s.rss", user34), http.StatusNotFound)
MakeRequest(t, NewRequestf(t, "GET", "/%s", user34).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound) MakeRequest(t, NewRequestf(t, "GET", "/%s", user34).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
+2 -2
View File
@@ -126,6 +126,6 @@ func testViewPageCurrentURL(t *testing.T) {
currentURL, _ = ctx.Data["CurrentURL"].(string) currentURL, _ = ctx.Data["CurrentURL"].(string)
}) })
defer web.RouteMockReset() defer web.RouteMockReset()
MakeRequest(t, NewRequest(t, "GET", "/any-page?k=v"), http.StatusNotFound) MakeRequest(t, NewRequest(t, "GET", "/user2?k=v"), http.StatusOK)
assert.Equal(t, "/subpath/any-page?k=v", currentURL) assert.Equal(t, "/subpath/user2?k=v", currentURL)
} }