code.gitea.io/gitea@v1.22.3/routers/web/user/profile.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package user 6 7 import ( 8 "fmt" 9 "net/http" 10 "path" 11 "strings" 12 13 activities_model "code.gitea.io/gitea/models/activities" 14 "code.gitea.io/gitea/models/db" 15 repo_model "code.gitea.io/gitea/models/repo" 16 user_model "code.gitea.io/gitea/models/user" 17 "code.gitea.io/gitea/modules/base" 18 "code.gitea.io/gitea/modules/git" 19 "code.gitea.io/gitea/modules/log" 20 "code.gitea.io/gitea/modules/markup" 21 "code.gitea.io/gitea/modules/markup/markdown" 22 "code.gitea.io/gitea/modules/optional" 23 "code.gitea.io/gitea/modules/setting" 24 "code.gitea.io/gitea/modules/util" 25 "code.gitea.io/gitea/routers/web/feed" 26 "code.gitea.io/gitea/routers/web/org" 27 shared_user "code.gitea.io/gitea/routers/web/shared/user" 28 "code.gitea.io/gitea/services/context" 29 ) 30 31 const ( 32 tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar" 33 tplFollowUnfollow base.TplName = "org/follow_unfollow" 34 ) 35 36 // OwnerProfile render profile page for a user or a organization (aka, repo owner) 37 func OwnerProfile(ctx *context.Context) { 38 if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") { 39 feed.ShowUserFeedRSS(ctx) 40 return 41 } 42 if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") { 43 feed.ShowUserFeedAtom(ctx) 44 return 45 } 46 47 if ctx.ContextUser.IsOrganization() { 48 org.Home(ctx) 49 } else { 50 userProfile(ctx) 51 } 52 } 53 54 func userProfile(ctx *context.Context) { 55 // check view permissions 56 if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { 57 ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name)) 58 return 59 } 60 61 ctx.Data["Title"] = ctx.ContextUser.DisplayName() 62 ctx.Data["PageIsUserProfile"] = true 63 64 // prepare heatmap data 65 if setting.Service.EnableUserHeatmap { 66 data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) 67 if err != nil { 68 ctx.ServerError("GetUserHeatmapDataByUser", err) 69 return 70 } 71 ctx.Data["HeatmapData"] = data 72 ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) 73 } 74 75 profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) 76 defer profileClose() 77 78 showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) 79 prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob) 80 // call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing 81 shared_user.PrepareContextForProfileBigAvatar(ctx) 82 ctx.HTML(http.StatusOK, tplProfile) 83 } 84 85 func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) { 86 // if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page 87 // if there is not a profile readme, the overview tab should be treated as the repositories tab 88 tab := ctx.FormString("tab") 89 if tab == "" || tab == "overview" { 90 if profileReadme != nil { 91 tab = "overview" 92 } else { 93 tab = "repositories" 94 } 95 } 96 ctx.Data["TabName"] = tab 97 ctx.Data["HasProfileReadme"] = profileReadme != nil 98 99 page := ctx.FormInt("page") 100 if page <= 0 { 101 page = 1 102 } 103 104 pagingNum := setting.UI.User.RepoPagingNum 105 topicOnly := ctx.FormBool("topic") 106 var ( 107 repos []*repo_model.Repository 108 count int64 109 total int 110 orderBy db.SearchOrderBy 111 ) 112 113 ctx.Data["SortType"] = ctx.FormString("sort") 114 switch ctx.FormString("sort") { 115 case "newest": 116 orderBy = db.SearchOrderByNewest 117 case "oldest": 118 orderBy = db.SearchOrderByOldest 119 case "recentupdate": 120 orderBy = db.SearchOrderByRecentUpdated 121 case "leastupdate": 122 orderBy = db.SearchOrderByLeastUpdated 123 case "reversealphabetically": 124 orderBy = db.SearchOrderByAlphabeticallyReverse 125 case "alphabetically": 126 orderBy = db.SearchOrderByAlphabetically 127 case "moststars": 128 orderBy = db.SearchOrderByStarsReverse 129 case "feweststars": 130 orderBy = db.SearchOrderByStars 131 case "mostforks": 132 orderBy = db.SearchOrderByForksReverse 133 case "fewestforks": 134 orderBy = db.SearchOrderByForks 135 case "size": 136 orderBy = db.SearchOrderByGitSize 137 case "reversesize": 138 orderBy = db.SearchOrderByGitSizeReverse 139 default: 140 ctx.Data["SortType"] = "recentupdate" 141 orderBy = db.SearchOrderByRecentUpdated 142 } 143 144 keyword := ctx.FormTrim("q") 145 ctx.Data["Keyword"] = keyword 146 147 language := ctx.FormTrim("language") 148 ctx.Data["Language"] = language 149 150 followers, numFollowers, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ 151 PageSize: pagingNum, 152 Page: page, 153 }) 154 if err != nil { 155 ctx.ServerError("GetUserFollowers", err) 156 return 157 } 158 ctx.Data["NumFollowers"] = numFollowers 159 following, numFollowing, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ 160 PageSize: pagingNum, 161 Page: page, 162 }) 163 if err != nil { 164 ctx.ServerError("GetUserFollowing", err) 165 return 166 } 167 ctx.Data["NumFollowing"] = numFollowing 168 169 archived := ctx.FormOptionalBool("archived") 170 ctx.Data["IsArchived"] = archived 171 172 fork := ctx.FormOptionalBool("fork") 173 ctx.Data["IsFork"] = fork 174 175 mirror := ctx.FormOptionalBool("mirror") 176 ctx.Data["IsMirror"] = mirror 177 178 template := ctx.FormOptionalBool("template") 179 ctx.Data["IsTemplate"] = template 180 181 private := ctx.FormOptionalBool("private") 182 ctx.Data["IsPrivate"] = private 183 184 switch tab { 185 case "followers": 186 ctx.Data["Cards"] = followers 187 total = int(numFollowers) 188 case "following": 189 ctx.Data["Cards"] = following 190 total = int(numFollowing) 191 case "activity": 192 date := ctx.FormString("date") 193 pagingNum = setting.UI.FeedPagingNum 194 items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ 195 RequestedUser: ctx.ContextUser, 196 Actor: ctx.Doer, 197 IncludePrivate: showPrivate, 198 OnlyPerformedBy: true, 199 IncludeDeleted: false, 200 Date: date, 201 ListOptions: db.ListOptions{ 202 PageSize: pagingNum, 203 Page: page, 204 }, 205 }) 206 if err != nil { 207 ctx.ServerError("GetFeeds", err) 208 return 209 } 210 ctx.Data["Feeds"] = items 211 ctx.Data["Date"] = date 212 213 total = int(count) 214 case "stars": 215 ctx.Data["PageIsProfileStarList"] = true 216 repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ 217 ListOptions: db.ListOptions{ 218 PageSize: pagingNum, 219 Page: page, 220 }, 221 Actor: ctx.Doer, 222 Keyword: keyword, 223 OrderBy: orderBy, 224 Private: ctx.IsSigned, 225 StarredByID: ctx.ContextUser.ID, 226 Collaborate: optional.Some(false), 227 TopicOnly: topicOnly, 228 Language: language, 229 IncludeDescription: setting.UI.SearchRepoDescription, 230 Archived: archived, 231 Fork: fork, 232 Mirror: mirror, 233 Template: template, 234 IsPrivate: private, 235 }) 236 if err != nil { 237 ctx.ServerError("SearchRepository", err) 238 return 239 } 240 241 total = int(count) 242 case "watching": 243 repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ 244 ListOptions: db.ListOptions{ 245 PageSize: pagingNum, 246 Page: page, 247 }, 248 Actor: ctx.Doer, 249 Keyword: keyword, 250 OrderBy: orderBy, 251 Private: ctx.IsSigned, 252 WatchedByID: ctx.ContextUser.ID, 253 Collaborate: optional.Some(false), 254 TopicOnly: topicOnly, 255 Language: language, 256 IncludeDescription: setting.UI.SearchRepoDescription, 257 Archived: archived, 258 Fork: fork, 259 Mirror: mirror, 260 Template: template, 261 IsPrivate: private, 262 }) 263 if err != nil { 264 ctx.ServerError("SearchRepository", err) 265 return 266 } 267 268 total = int(count) 269 case "overview": 270 if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { 271 log.Error("failed to GetBlobContent: %v", err) 272 } else { 273 if profileContent, err := markdown.RenderString(&markup.RenderContext{ 274 Ctx: ctx, 275 GitRepo: profileGitRepo, 276 Links: markup.Links{ 277 // Give the repo link to the markdown render for the full link of media element. 278 // the media link usually be like /[user]/[repoName]/media/branch/[branchName], 279 // Eg. /Tom/.profile/media/branch/main 280 // The branch shown on the profile page is the default branch, this need to be in sync with doc, see: 281 // https://docs.gitea.com/usage/profile-readme 282 Base: profileDbRepo.Link(), 283 BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), 284 }, 285 Metas: map[string]string{"mode": "document"}, 286 }, bytes); err != nil { 287 log.Error("failed to RenderString: %v", err) 288 } else { 289 ctx.Data["ProfileReadme"] = profileContent 290 } 291 } 292 default: // default to "repositories" 293 repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ 294 ListOptions: db.ListOptions{ 295 PageSize: pagingNum, 296 Page: page, 297 }, 298 Actor: ctx.Doer, 299 Keyword: keyword, 300 OwnerID: ctx.ContextUser.ID, 301 OrderBy: orderBy, 302 Private: ctx.IsSigned, 303 Collaborate: optional.Some(false), 304 TopicOnly: topicOnly, 305 Language: language, 306 IncludeDescription: setting.UI.SearchRepoDescription, 307 Archived: archived, 308 Fork: fork, 309 Mirror: mirror, 310 Template: template, 311 IsPrivate: private, 312 }) 313 if err != nil { 314 ctx.ServerError("SearchRepository", err) 315 return 316 } 317 318 total = int(count) 319 } 320 ctx.Data["Repos"] = repos 321 ctx.Data["Total"] = total 322 323 err = shared_user.LoadHeaderCount(ctx) 324 if err != nil { 325 ctx.ServerError("LoadHeaderCount", err) 326 return 327 } 328 329 pager := context.NewPagination(total, pagingNum, page, 5) 330 pager.SetDefaultParams(ctx) 331 pager.AddParamString("tab", tab) 332 if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { 333 pager.AddParamString("language", language) 334 } 335 if tab == "activity" { 336 if ctx.Data["Date"] != nil { 337 pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"])) 338 } 339 } 340 if archived.Has() { 341 pager.AddParamString("archived", fmt.Sprint(archived.Value())) 342 } 343 if fork.Has() { 344 pager.AddParamString("fork", fmt.Sprint(fork.Value())) 345 } 346 if mirror.Has() { 347 pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) 348 } 349 if template.Has() { 350 pager.AddParamString("template", fmt.Sprint(template.Value())) 351 } 352 if private.Has() { 353 pager.AddParamString("private", fmt.Sprint(private.Value())) 354 } 355 ctx.Data["Page"] = pager 356 } 357 358 // Action response for follow/unfollow user request 359 func Action(ctx *context.Context) { 360 var err error 361 switch ctx.FormString("action") { 362 case "follow": 363 err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser) 364 case "unfollow": 365 err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) 366 } 367 368 if err != nil { 369 log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err) 370 ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action"))) 371 return 372 } 373 374 if ctx.ContextUser.IsIndividual() { 375 shared_user.PrepareContextForProfileBigAvatar(ctx) 376 ctx.HTML(http.StatusOK, tplProfileBigAvatar) 377 return 378 } else if ctx.ContextUser.IsOrganization() { 379 ctx.Data["Org"] = ctx.ContextUser 380 ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) 381 ctx.HTML(http.StatusOK, tplFollowUnfollow) 382 return 383 } 384 log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type) 385 ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action"))) 386 }