diff options
Diffstat (limited to '')
-rw-r--r-- | routers/web/user/profile.go | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go new file mode 100644 index 0000000..9cb392d --- /dev/null +++ b/routers/web/user/profile.go @@ -0,0 +1,385 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "errors" + "fmt" + "net/http" + "path" + "strings" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/feed" + "code.gitea.io/gitea/routers/web/org" + shared_user "code.gitea.io/gitea/routers/web/shared/user" + "code.gitea.io/gitea/services/context" + user_service "code.gitea.io/gitea/services/user" +) + +const ( + tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar" + tplFollowUnfollow base.TplName = "org/follow_unfollow" +) + +// 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 + } + + if ctx.ContextUser.IsOrganization() { + org.Home(ctx) + } else { + userProfile(ctx) + } +} + +func userProfile(ctx *context.Context) { + // check view permissions + if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { + ctx.NotFound("User not visible", nil) + return + } + + ctx.Data["Title"] = ctx.ContextUser.DisplayName() + ctx.Data["PageIsUserProfile"] = true + + // prepare heatmap data + if setting.Service.EnableUserHeatmap { + data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserHeatmapDataByUser", err) + return + } + ctx.Data["HeatmapData"] = data + ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) + } + + profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) + defer profileClose() + + showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) + prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob) + // call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing + shared_user.PrepareContextForProfileBigAvatar(ctx) + ctx.HTML(http.StatusOK, tplProfile) +} + +func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) { + // if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page + // if there is not a profile readme, the overview tab should be treated as the repositories tab + tab := ctx.FormString("tab") + if tab == "" || tab == "overview" { + if profileReadme != nil { + tab = "overview" + } else { + tab = "repositories" + } + } + ctx.Data["TabName"] = tab + ctx.Data["HasProfileReadme"] = profileReadme != nil + + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + pagingNum := setting.UI.User.RepoPagingNum + topicOnly := ctx.FormBool("topic") + var ( + repos []*repo_model.Repository + count int64 + total int + orderBy db.SearchOrderBy + ) + + sortOrder := ctx.FormString("sort") + if _, ok := repo_model.OrderByFlatMap[sortOrder]; !ok { + sortOrder = setting.UI.ExploreDefaultSort // TODO: add new default sort order for user home? + } + ctx.Data["SortType"] = sortOrder + orderBy = repo_model.OrderByFlatMap[sortOrder] + + keyword := ctx.FormTrim("q") + ctx.Data["Keyword"] = keyword + + language := ctx.FormTrim("language") + ctx.Data["Language"] = language + + followers, numFollowers, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ + PageSize: pagingNum, + Page: page, + }) + if err != nil { + ctx.ServerError("GetUserFollowers", err) + return + } + ctx.Data["NumFollowers"] = numFollowers + following, numFollowing, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ + PageSize: pagingNum, + Page: page, + }) + if err != nil { + ctx.ServerError("GetUserFollowing", err) + return + } + ctx.Data["NumFollowing"] = numFollowing + + archived := ctx.FormOptionalBool("archived") + ctx.Data["IsArchived"] = archived + + fork := ctx.FormOptionalBool("fork") + ctx.Data["IsFork"] = fork + + mirror := ctx.FormOptionalBool("mirror") + ctx.Data["IsMirror"] = mirror + + template := ctx.FormOptionalBool("template") + ctx.Data["IsTemplate"] = template + + private := ctx.FormOptionalBool("private") + ctx.Data["IsPrivate"] = private + + switch tab { + case "followers": + ctx.Data["Cards"] = followers + total = int(numFollowers) + ctx.Data["CardsTitle"] = ctx.TrN(total, "user.followers.title.one", "user.followers.title.few") + case "following": + ctx.Data["Cards"] = following + total = int(numFollowing) + ctx.Data["CardsTitle"] = ctx.TrN(total, "user.following.title.one", "user.following.title.few") + case "activity": + date := ctx.FormString("date") + pagingNum = setting.UI.FeedPagingNum + items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ + RequestedUser: ctx.ContextUser, + Actor: ctx.Doer, + IncludePrivate: showPrivate, + OnlyPerformedBy: true, + IncludeDeleted: false, + Date: date, + ListOptions: db.ListOptions{ + PageSize: pagingNum, + Page: page, + }, + }) + if err != nil { + ctx.ServerError("GetFeeds", err) + return + } + ctx.Data["Feeds"] = items + ctx.Data["Date"] = date + + total = int(count) + case "stars": + ctx.Data["PageIsProfileStarList"] = true + repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + PageSize: pagingNum, + Page: page, + }, + Actor: ctx.Doer, + Keyword: keyword, + OrderBy: orderBy, + Private: ctx.IsSigned, + StarredByID: ctx.ContextUser.ID, + Collaborate: optional.Some(false), + TopicOnly: topicOnly, + Language: language, + IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, + }) + if err != nil { + ctx.ServerError("SearchRepository", err) + return + } + + total = int(count) + case "watching": + repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + PageSize: pagingNum, + Page: page, + }, + Actor: ctx.Doer, + Keyword: keyword, + OrderBy: orderBy, + Private: ctx.IsSigned, + WatchedByID: ctx.ContextUser.ID, + Collaborate: optional.Some(false), + TopicOnly: topicOnly, + Language: language, + IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, + }) + if err != nil { + ctx.ServerError("SearchRepository", err) + return + } + + total = int(count) + case "overview": + if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("failed to GetBlobContent: %v", err) + } else { + if profileContent, err := markdown.RenderString(&markup.RenderContext{ + Ctx: ctx, + GitRepo: profileGitRepo, + Links: markup.Links{ + // Give the repo link to the markdown render for the full link of media element. + // the media link usually be like /[user]/[repoName]/media/branch/[branchName], + // Eg. /Tom/.profile/media/branch/main + // The branch shown on the profile page is the default branch, this need to be in sync with doc, see: + // https://docs.gitea.com/usage/profile-readme + Base: profileDbRepo.Link(), + BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), + }, + Metas: map[string]string{"mode": "document"}, + }, bytes); err != nil { + log.Error("failed to RenderString: %v", err) + } else { + ctx.Data["ProfileReadme"] = profileContent + } + } + default: // default to "repositories" + repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + PageSize: pagingNum, + Page: page, + }, + Actor: ctx.Doer, + Keyword: keyword, + OwnerID: ctx.ContextUser.ID, + OrderBy: orderBy, + Private: ctx.IsSigned, + Collaborate: optional.Some(false), + TopicOnly: topicOnly, + Language: language, + IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, + }) + if err != nil { + ctx.ServerError("SearchRepository", err) + return + } + + total = int(count) + } + ctx.Data["Repos"] = repos + ctx.Data["Total"] = total + + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + pager := context.NewPagination(total, pagingNum, page, 5) + pager.SetDefaultParams(ctx) + pager.AddParam(ctx, "tab", "TabName") + if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { + pager.AddParam(ctx, "language", "Language") + } + if tab == "activity" { + pager.AddParam(ctx, "date", "Date") + } + if archived.Has() { + pager.AddParamString("archived", fmt.Sprint(archived.Value())) + } + if fork.Has() { + pager.AddParamString("fork", fmt.Sprint(fork.Value())) + } + if mirror.Has() { + pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) + } + if template.Has() { + pager.AddParamString("template", fmt.Sprint(template.Value())) + } + if private.Has() { + pager.AddParamString("private", fmt.Sprint(private.Value())) + } + ctx.Data["Page"] = pager +} + +// Action response for follow/unfollow user request +func Action(ctx *context.Context) { + var err error + action := ctx.FormString("action") + + if ctx.ContextUser.IsOrganization() && (action == "block" || action == "unblock") { + log.Error("Cannot perform this action on an organization %q", ctx.FormString("action")) + ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action"))) + return + } + + switch action { + case "follow": + err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + case "unfollow": + err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + case "block": + err = user_service.BlockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + case "unblock": + err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + } + + if err != nil { + if !errors.Is(err, user_model.ErrBlockedByUser) { + log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err) + ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action"))) + return + } + + if ctx.ContextUser.IsOrganization() { + ctx.Flash.Error(ctx.Tr("org.follow_blocked_user"), true) + } else { + ctx.Flash.Error(ctx.Tr("user.follow_blocked_user"), true) + } + } + + if ctx.ContextUser.IsIndividual() { + shared_user.PrepareContextForProfileBigAvatar(ctx) + ctx.Data["IsHTMX"] = true + ctx.HTML(http.StatusOK, tplProfileBigAvatar) + return + } else if ctx.ContextUser.IsOrganization() { + ctx.Data["Org"] = ctx.ContextUser + ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + ctx.HTML(http.StatusOK, tplFollowUnfollow) + return + } + log.Error("Failed to apply action %q: unsupported context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type) + ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action"))) +} |