mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
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:
13 files changed
+103
-117
No files matched your search
@@ -260,18 +260,21 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
|
||||
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.
|
||||
func GetFeedType(name string, req *http.Request) (showFeed bool, feedType string) {
|
||||
if strings.HasSuffix(name, ".rss") ||
|
||||
strings.Contains(req.Header.Get("Accept"), "application/rss+xml") {
|
||||
if strings.HasSuffix(name, ExtNameRss) || strings.Contains(req.Header.Get("Accept"), ContentTypeRssXml) {
|
||||
return true, "rss"
|
||||
}
|
||||
|
||||
if strings.HasSuffix(name, ".atom") ||
|
||||
strings.Contains(req.Header.Get("Accept"), "application/atom+xml") {
|
||||
if strings.HasSuffix(name, ExtNameAtom) || strings.Contains(req.Header.Get("Accept"), ContentTypeAtomXml) {
|
||||
return true, "atom"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/renderhelper"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
@@ -30,16 +29,13 @@ func ShowUserFeedAtom(ctx *context.Context) {
|
||||
|
||||
// showUserFeed show user activity as RSS / Atom feed
|
||||
func showUserFeed(ctx *context.Context, formatType string) {
|
||||
isOrganisation := ctx.ContextUser.IsOrganization()
|
||||
if !setting.Other.EnableFeed ||
|
||||
isOrganisation && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
|
||||
!isOrganisation && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
|
||||
if !setting.Other.EnableFeed {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
// include the private repo's the member has access to.
|
||||
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,
|
||||
Actor: ctx.Doer,
|
||||
IncludePrivate: includePrivate,
|
||||
OnlyPerformedBy: !isOrganisation,
|
||||
OnlyPerformedBy: !ctx.ContextUser.IsOrganization(),
|
||||
IncludeDeleted: false,
|
||||
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
|
||||
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
|
||||
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 {
|
||||
ctx.ServerError("Render Atom failed", err)
|
||||
}
|
||||
} 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 {
|
||||
ctx.ServerError("Render RSS failed", err)
|
||||
}
|
||||
|
||||
@@ -96,18 +96,14 @@ func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) strin
|
||||
if len(repo.DefaultBranch) == 0 {
|
||||
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
|
||||
// account behind it could read the repo through the web UI
|
||||
if !goGetTokenCanReadRepo(ctx) {
|
||||
return def
|
||||
}
|
||||
// the caller must be able to read the code and see the owner: a limited/private owner hides its repos
|
||||
// from anonymous/non-member callers even when the repo itself is public
|
||||
// the caller must be able to read the code
|
||||
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 repo.DefaultBranch
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"gitea.dev/modules/sitemap"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/routers/web/feed"
|
||||
"gitea.dev/routers/web/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
@@ -24,6 +25,10 @@ import (
|
||||
const tplHome templates.TplName = "home"
|
||||
|
||||
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 {
|
||||
user.Dashboard(ctx)
|
||||
return
|
||||
|
||||
@@ -5,7 +5,6 @@ package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
@@ -25,14 +24,7 @@ const tplOrgHome templates.TplName = "org/home"
|
||||
|
||||
// Home show organization home page
|
||||
func Home(ctx *context.Context) {
|
||||
uname := ctx.PathParam("username")
|
||||
|
||||
if strings.HasSuffix(uname, ".keys") || strings.HasSuffix(uname, ".gpg") {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetPathParam("org", uname)
|
||||
ctx.SetPathParam("org", ctx.PathParam("username"))
|
||||
context.OrgAssignment(context.OrgAssignmentOptions{})(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net/url"
|
||||
|
||||
activities_model "gitea.dev/models/activities"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
@@ -52,12 +51,6 @@ func DashboardHeatmap(ctx *context.Context) {
|
||||
ctx.NotFound(nil)
|
||||
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 err error
|
||||
if ctx.Org.Organization == nil {
|
||||
|
||||
+41
-42
@@ -6,8 +6,8 @@ package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -702,52 +703,50 @@ func ShowGPGKeys(ctx *context.Context) {
|
||||
ctx.PlainTextBytes(http.StatusOK, buf.Bytes())
|
||||
}
|
||||
|
||||
func UsernameSubRoute(ctx *context.Context) {
|
||||
// WORKAROUND to support usernames with "." in it
|
||||
// https://github.com/go-chi/chi/issues/781
|
||||
username := ctx.PathParam("username")
|
||||
reloadParam := func(suffix string) (success bool) {
|
||||
ctx.SetPathParam("username", strings.TrimSuffix(username, suffix))
|
||||
context.UserAssignmentWeb()(ctx)
|
||||
if ctx.Written() {
|
||||
return false
|
||||
// UsernamePreRoute rewrites the route path if the username has a suffix, so that the correct handler can be called.
|
||||
func UsernamePreRoute(next http.Handler) http.Handler {
|
||||
// HINT: OWNER-NAME-RESERVED-EXT: use the reserved extensions to check and rewrite requests
|
||||
suffixes := container.SetOf(".png", ".keys", ".gpg", feed.ExtNameRss, feed.ExtNameAtom)
|
||||
rewriteRoutePath := func(rctx *chi.Context, r *http.Request) (ownerPath, rewrittenExt string) {
|
||||
if rctx.RouteMethod != "HEAD" && rctx.RouteMethod != "GET" {
|
||||
return "", ""
|
||||
}
|
||||
// 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
|
||||
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
|
||||
ctx.NotFound(fmt.Errorf("%s", ctx.ContextUser.Name))
|
||||
return false
|
||||
// HINT: USERNAME-AVATAR-APPLE-TOUCH-ICON: skip rewriting the path if it is an apple touch icon request
|
||||
isAppleTouchIcon := rctx.RoutePath == "/apple-touch-icon.png" || rctx.RoutePath == "/apple-touch-icon-precomposed.png"
|
||||
if isAppleTouchIcon {
|
||||
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"):
|
||||
if reloadParam(".png") {
|
||||
AvatarByUsernameSize(ctx)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
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"):
|
||||
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)
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
|
||||
|
||||
@@ -7,7 +7,6 @@ package user
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/db"
|
||||
@@ -21,7 +20,6 @@ import (
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/routers/web/feed"
|
||||
"gitea.dev/routers/web/org"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
@@ -35,15 +33,7 @@ const (
|
||||
|
||||
// OwnerProfile render profile page for a user or a organization (aka, repo owner)
|
||||
func OwnerProfile(ctx *context.Context) {
|
||||
if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") {
|
||||
feed.ShowUserFeedRSS(ctx)
|
||||
return
|
||||
}
|
||||
if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") {
|
||||
feed.ShowUserFeedAtom(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["EnableFeed"] = setting.Other.EnableFeed
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
org.Home(ctx)
|
||||
} else {
|
||||
@@ -52,12 +42,6 @@ func OwnerProfile(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["PageIsUserProfile"] = true
|
||||
|
||||
@@ -322,13 +306,6 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
|
||||
// ActionUserFollow is for follow/unfollow user request
|
||||
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
|
||||
switch ctx.FormString("action") {
|
||||
case "follow":
|
||||
|
||||
+19
-10
@@ -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
|
||||
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.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", "/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", "/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-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
|
||||
|
||||
@@ -906,13 +909,19 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, adminReq, ctxDataSet(reqctx.ContextData{"EnableOAuth2": setting.OAuth2.Enabled, "EnablePackages": setting.Packages.Enabled}))
|
||||
// ***** END: Admin *****
|
||||
|
||||
m.Group("", func() {
|
||||
// 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.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), webAuth.AllowBasic, webAuth.AllowOAuth2, optSignIn, repo.GetAttachment)
|
||||
|
||||
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()
|
||||
reqRepoCodeWriter := context.RequireUnitWriter(unit.TypeCode)
|
||||
@@ -981,8 +990,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Group("/{org}", func() {
|
||||
m.Get("/dashboard", user.Dashboard)
|
||||
m.Get("/dashboard/{team}", user.Dashboard)
|
||||
m.Get("/dashboard/-/heatmap", user.DashboardHeatmap)
|
||||
m.Get("/dashboard/-/heatmap/{team}", user.DashboardHeatmap)
|
||||
m.Get("/dashboard/-/heatmap", context.ReqContextUserVisibleToDoer(), user.DashboardHeatmap)
|
||||
m.Get("/dashboard/-/heatmap/{team}", context.ReqContextUserVisibleToDoer(), user.DashboardHeatmap)
|
||||
m.Get("/issues", user.Issues)
|
||||
m.Get("/issues/{team}", user.Issues)
|
||||
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("/repositories", org.Repositories)
|
||||
m.Get("/heatmap", user.DashboardHeatmap)
|
||||
m.Get("/heatmap", context.ReqContextUserVisibleToDoer(), user.DashboardHeatmap)
|
||||
|
||||
m.Group("/projects", func() {
|
||||
m.Group("", func() {
|
||||
|
||||
Reference in new issue
Block a user