code.gitea.io/gitea@v1.22.3/routers/web/user/home.go (about) 1 // Copyright 2014 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 "bytes" 9 "fmt" 10 "net/http" 11 "regexp" 12 "slices" 13 "sort" 14 "strconv" 15 "strings" 16 17 activities_model "code.gitea.io/gitea/models/activities" 18 asymkey_model "code.gitea.io/gitea/models/asymkey" 19 "code.gitea.io/gitea/models/db" 20 issues_model "code.gitea.io/gitea/models/issues" 21 "code.gitea.io/gitea/models/organization" 22 repo_model "code.gitea.io/gitea/models/repo" 23 "code.gitea.io/gitea/models/unit" 24 user_model "code.gitea.io/gitea/models/user" 25 "code.gitea.io/gitea/modules/base" 26 "code.gitea.io/gitea/modules/container" 27 issue_indexer "code.gitea.io/gitea/modules/indexer/issues" 28 "code.gitea.io/gitea/modules/log" 29 "code.gitea.io/gitea/modules/markup" 30 "code.gitea.io/gitea/modules/markup/markdown" 31 "code.gitea.io/gitea/modules/optional" 32 "code.gitea.io/gitea/modules/setting" 33 "code.gitea.io/gitea/routers/web/feed" 34 "code.gitea.io/gitea/services/context" 35 issue_service "code.gitea.io/gitea/services/issue" 36 pull_service "code.gitea.io/gitea/services/pull" 37 38 "github.com/keybase/go-crypto/openpgp" 39 "github.com/keybase/go-crypto/openpgp/armor" 40 "xorm.io/builder" 41 ) 42 43 const ( 44 tplDashboard base.TplName = "user/dashboard/dashboard" 45 tplIssues base.TplName = "user/dashboard/issues" 46 tplMilestones base.TplName = "user/dashboard/milestones" 47 tplProfile base.TplName = "user/profile" 48 ) 49 50 // getDashboardContextUser finds out which context user dashboard is being viewed as . 51 func getDashboardContextUser(ctx *context.Context) *user_model.User { 52 ctxUser := ctx.Doer 53 orgName := ctx.Params(":org") 54 if len(orgName) > 0 { 55 ctxUser = ctx.Org.Organization.AsUser() 56 ctx.Data["Teams"] = ctx.Org.Teams 57 } 58 ctx.Data["ContextUser"] = ctxUser 59 60 orgs, err := organization.GetUserOrgsList(ctx, ctx.Doer) 61 if err != nil { 62 ctx.ServerError("GetUserOrgsList", err) 63 return nil 64 } 65 ctx.Data["Orgs"] = orgs 66 67 return ctxUser 68 } 69 70 // Dashboard render the dashboard page 71 func Dashboard(ctx *context.Context) { 72 ctxUser := getDashboardContextUser(ctx) 73 if ctx.Written() { 74 return 75 } 76 77 var ( 78 date = ctx.FormString("date") 79 page = ctx.FormInt("page") 80 ) 81 82 // Make sure page number is at least 1. Will be posted to ctx.Data. 83 if page <= 1 { 84 page = 1 85 } 86 87 ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Locale.TrString("dashboard") 88 ctx.Data["PageIsDashboard"] = true 89 ctx.Data["PageIsNews"] = true 90 cnt, _ := organization.GetOrganizationCount(ctx, ctxUser) 91 ctx.Data["UserOrgsCount"] = cnt 92 ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled 93 ctx.Data["Date"] = date 94 95 var uid int64 96 if ctxUser != nil { 97 uid = ctxUser.ID 98 } 99 100 ctx.PageData["dashboardRepoList"] = map[string]any{ 101 "searchLimit": setting.UI.User.RepoPagingNum, 102 "uid": uid, 103 } 104 105 if setting.Service.EnableUserHeatmap { 106 data, err := activities_model.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer) 107 if err != nil { 108 ctx.ServerError("GetUserHeatmapDataByUserTeam", err) 109 return 110 } 111 ctx.Data["HeatmapData"] = data 112 ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) 113 } 114 115 feeds, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ 116 RequestedUser: ctxUser, 117 RequestedTeam: ctx.Org.Team, 118 Actor: ctx.Doer, 119 IncludePrivate: true, 120 OnlyPerformedBy: false, 121 IncludeDeleted: false, 122 Date: ctx.FormString("date"), 123 ListOptions: db.ListOptions{ 124 Page: page, 125 PageSize: setting.UI.FeedPagingNum, 126 }, 127 }) 128 if err != nil { 129 ctx.ServerError("GetFeeds", err) 130 return 131 } 132 133 ctx.Data["Feeds"] = feeds 134 135 pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5) 136 pager.AddParamString("date", date) 137 ctx.Data["Page"] = pager 138 139 ctx.HTML(http.StatusOK, tplDashboard) 140 } 141 142 // Milestones render the user milestones page 143 func Milestones(ctx *context.Context) { 144 if unit.TypeIssues.UnitGlobalDisabled() && unit.TypePullRequests.UnitGlobalDisabled() { 145 log.Debug("Milestones overview page not available as both issues and pull requests are globally disabled") 146 ctx.Status(http.StatusNotFound) 147 return 148 } 149 150 ctx.Data["Title"] = ctx.Tr("milestones") 151 ctx.Data["PageIsMilestonesDashboard"] = true 152 153 ctxUser := getDashboardContextUser(ctx) 154 if ctx.Written() { 155 return 156 } 157 158 repoOpts := repo_model.SearchRepoOptions{ 159 Actor: ctx.Doer, 160 OwnerID: ctxUser.ID, 161 Private: true, 162 AllPublic: false, // Include also all public repositories of users and public organisations 163 AllLimited: false, // Include also all public repositories of limited organisations 164 Archived: optional.Some(false), 165 HasMilestones: optional.Some(true), // Just needs display repos has milestones 166 } 167 168 if ctxUser.IsOrganization() && ctx.Org.Team != nil { 169 repoOpts.TeamID = ctx.Org.Team.ID 170 } 171 172 var ( 173 userRepoCond = repo_model.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit 174 repoCond = userRepoCond 175 repoIDs []int64 176 177 reposQuery = ctx.FormString("repos") 178 isShowClosed = ctx.FormString("state") == "closed" 179 sortType = ctx.FormString("sort") 180 page = ctx.FormInt("page") 181 keyword = ctx.FormTrim("q") 182 ) 183 184 if page <= 1 { 185 page = 1 186 } 187 188 if len(reposQuery) != 0 { 189 if issueReposQueryPattern.MatchString(reposQuery) { 190 // remove "[" and "]" from string 191 reposQuery = reposQuery[1 : len(reposQuery)-1] 192 // for each ID (delimiter ",") add to int to repoIDs 193 194 for _, rID := range strings.Split(reposQuery, ",") { 195 // Ensure nonempty string entries 196 if rID != "" && rID != "0" { 197 rIDint64, err := strconv.ParseInt(rID, 10, 64) 198 // If the repo id specified by query is not parseable or not accessible by user, just ignore it. 199 if err == nil { 200 repoIDs = append(repoIDs, rIDint64) 201 } 202 } 203 } 204 if len(repoIDs) > 0 { 205 // Don't just let repoCond = builder.In("id", repoIDs) because user may has no permission on repoIDs 206 // But the original repoCond has a limitation 207 repoCond = repoCond.And(builder.In("id", repoIDs)) 208 } 209 } else { 210 log.Warn("issueReposQueryPattern not match with query") 211 } 212 } 213 214 counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{ 215 RepoCond: userRepoCond, 216 Name: keyword, 217 IsClosed: optional.Some(isShowClosed), 218 }) 219 if err != nil { 220 ctx.ServerError("CountMilestonesByRepoIDs", err) 221 return 222 } 223 224 milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ 225 ListOptions: db.ListOptions{ 226 Page: page, 227 PageSize: setting.UI.IssuePagingNum, 228 }, 229 RepoCond: repoCond, 230 IsClosed: optional.Some(isShowClosed), 231 SortType: sortType, 232 Name: keyword, 233 }) 234 if err != nil { 235 ctx.ServerError("SearchMilestones", err) 236 return 237 } 238 239 showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, &repoOpts, userRepoCond, false) 240 if err != nil { 241 ctx.ServerError("SearchRepositoryByCondition", err) 242 return 243 } 244 sort.Sort(showRepos) 245 246 for i := 0; i < len(milestones); { 247 for _, repo := range showRepos { 248 if milestones[i].RepoID == repo.ID { 249 milestones[i].Repo = repo 250 break 251 } 252 } 253 if milestones[i].Repo == nil { 254 log.Warn("Cannot find milestone %d 's repository %d", milestones[i].ID, milestones[i].RepoID) 255 milestones = append(milestones[:i], milestones[i+1:]...) 256 continue 257 } 258 259 milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ 260 Links: markup.Links{ 261 Base: milestones[i].Repo.Link(), 262 }, 263 Metas: milestones[i].Repo.ComposeMetas(ctx), 264 Ctx: ctx, 265 }, milestones[i].Content) 266 if err != nil { 267 ctx.ServerError("RenderString", err) 268 return 269 } 270 271 if milestones[i].Repo.IsTimetrackerEnabled(ctx) { 272 err := milestones[i].LoadTotalTrackedTime(ctx) 273 if err != nil { 274 ctx.ServerError("LoadTotalTrackedTime", err) 275 return 276 } 277 } 278 i++ 279 } 280 281 milestoneStats, err := issues_model.GetMilestonesStatsByRepoCondAndKw(ctx, repoCond, keyword) 282 if err != nil { 283 ctx.ServerError("GetMilestoneStats", err) 284 return 285 } 286 287 var totalMilestoneStats *issues_model.MilestonesStats 288 if len(repoIDs) == 0 { 289 totalMilestoneStats = milestoneStats 290 } else { 291 totalMilestoneStats, err = issues_model.GetMilestonesStatsByRepoCondAndKw(ctx, userRepoCond, keyword) 292 if err != nil { 293 ctx.ServerError("GetMilestoneStats", err) 294 return 295 } 296 } 297 298 showRepoIDs := make(container.Set[int64], len(showRepos)) 299 for _, repo := range showRepos { 300 if repo.ID > 0 { 301 showRepoIDs.Add(repo.ID) 302 } 303 } 304 if len(repoIDs) == 0 { 305 repoIDs = showRepoIDs.Values() 306 } 307 repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool { 308 return !showRepoIDs.Contains(v) 309 }) 310 311 var pagerCount int 312 if isShowClosed { 313 ctx.Data["State"] = "closed" 314 ctx.Data["Total"] = totalMilestoneStats.ClosedCount 315 pagerCount = int(milestoneStats.ClosedCount) 316 } else { 317 ctx.Data["State"] = "open" 318 ctx.Data["Total"] = totalMilestoneStats.OpenCount 319 pagerCount = int(milestoneStats.OpenCount) 320 } 321 322 ctx.Data["Milestones"] = milestones 323 ctx.Data["Repos"] = showRepos 324 ctx.Data["Counts"] = counts 325 ctx.Data["MilestoneStats"] = milestoneStats 326 ctx.Data["SortType"] = sortType 327 ctx.Data["Keyword"] = keyword 328 ctx.Data["RepoIDs"] = repoIDs 329 ctx.Data["IsShowClosed"] = isShowClosed 330 331 pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5) 332 pager.AddParamString("q", keyword) 333 pager.AddParamString("repos", reposQuery) 334 pager.AddParamString("sort", sortType) 335 pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) 336 ctx.Data["Page"] = pager 337 338 ctx.HTML(http.StatusOK, tplMilestones) 339 } 340 341 // Pulls renders the user's pull request overview page 342 func Pulls(ctx *context.Context) { 343 if unit.TypePullRequests.UnitGlobalDisabled() { 344 log.Debug("Pull request overview page not available as it is globally disabled.") 345 ctx.Status(http.StatusNotFound) 346 return 347 } 348 349 ctx.Data["Title"] = ctx.Tr("pull_requests") 350 ctx.Data["PageIsPulls"] = true 351 buildIssueOverview(ctx, unit.TypePullRequests) 352 } 353 354 // Issues renders the user's issues overview page 355 func Issues(ctx *context.Context) { 356 if unit.TypeIssues.UnitGlobalDisabled() { 357 log.Debug("Issues overview page not available as it is globally disabled.") 358 ctx.Status(http.StatusNotFound) 359 return 360 } 361 362 ctx.Data["Title"] = ctx.Tr("issues") 363 ctx.Data["PageIsIssues"] = true 364 buildIssueOverview(ctx, unit.TypeIssues) 365 } 366 367 // Regexp for repos query 368 var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`) 369 370 func buildIssueOverview(ctx *context.Context, unitType unit.Type) { 371 // ---------------------------------------------------- 372 // Determine user; can be either user or organization. 373 // Return with NotFound or ServerError if unsuccessful. 374 // ---------------------------------------------------- 375 376 ctxUser := getDashboardContextUser(ctx) 377 if ctx.Written() { 378 return 379 } 380 381 var ( 382 viewType string 383 sortType = ctx.FormString("sort") 384 filterMode int 385 ) 386 387 // Default to recently updated, unlike repository issues list 388 if sortType == "" { 389 sortType = "recentupdate" 390 } 391 392 // -------------------------------------------------------------------------------- 393 // Distinguish User from Organization. 394 // Org: 395 // - Remember pre-determined viewType string for later. Will be posted to ctx.Data. 396 // Organization does not have view type and filter mode. 397 // User: 398 // - Use ctx.FormString("type") to determine filterMode. 399 // The type is set when clicking for example "assigned to me" on the overview page. 400 // - Remember either this or a fallback. Will be posted to ctx.Data. 401 // -------------------------------------------------------------------------------- 402 403 // TODO: distinguish during routing 404 405 viewType = ctx.FormString("type") 406 switch viewType { 407 case "assigned": 408 filterMode = issues_model.FilterModeAssign 409 case "created_by": 410 filterMode = issues_model.FilterModeCreate 411 case "mentioned": 412 filterMode = issues_model.FilterModeMention 413 case "review_requested": 414 filterMode = issues_model.FilterModeReviewRequested 415 case "reviewed_by": 416 filterMode = issues_model.FilterModeReviewed 417 case "your_repositories": 418 fallthrough 419 default: 420 filterMode = issues_model.FilterModeYourRepositories 421 viewType = "your_repositories" 422 } 423 424 // -------------------------------------------------------------------------- 425 // Build opts (IssuesOptions), which contains filter information. 426 // Will eventually be used to retrieve issues relevant for the overview page. 427 // Note: Non-final states of opts are used in-between, namely for: 428 // - Keyword search 429 // - Count Issues by repo 430 // -------------------------------------------------------------------------- 431 432 // Get repository IDs where User/Org/Team has access. 433 var team *organization.Team 434 var org *organization.Organization 435 if ctx.Org != nil { 436 org = ctx.Org.Organization 437 team = ctx.Org.Team 438 } 439 440 isPullList := unitType == unit.TypePullRequests 441 opts := &issues_model.IssuesOptions{ 442 IsPull: optional.Some(isPullList), 443 SortType: sortType, 444 IsArchived: optional.Some(false), 445 Org: org, 446 Team: team, 447 User: ctx.Doer, 448 } 449 450 isFuzzy := ctx.FormBool("fuzzy") 451 452 // Search all repositories which 453 // 454 // As user: 455 // - Owns the repository. 456 // - Have collaborator permissions in repository. 457 // 458 // As org: 459 // - Owns the repository. 460 // 461 // As team: 462 // - Team org's owns the repository. 463 // - Team has read permission to repository. 464 repoOpts := &repo_model.SearchRepoOptions{ 465 Actor: ctx.Doer, 466 OwnerID: ctxUser.ID, 467 Private: true, 468 AllPublic: false, 469 AllLimited: false, 470 Collaborate: optional.None[bool](), 471 UnitType: unitType, 472 Archived: optional.Some(false), 473 } 474 if team != nil { 475 repoOpts.TeamID = team.ID 476 } 477 accessibleRepos := container.Set[int64]{} 478 { 479 ids, _, err := repo_model.SearchRepositoryIDs(ctx, repoOpts) 480 if err != nil { 481 ctx.ServerError("SearchRepositoryIDs", err) 482 return 483 } 484 accessibleRepos.AddMultiple(ids...) 485 opts.RepoIDs = ids 486 if len(opts.RepoIDs) == 0 { 487 // no repos found, don't let the indexer return all repos 488 opts.RepoIDs = []int64{0} 489 } 490 } 491 if ctx.Doer.ID == ctxUser.ID && filterMode != issues_model.FilterModeYourRepositories { 492 // If the doer is the same as the context user, which means the doer is viewing his own dashboard, 493 // it's not enough to show the repos that the doer owns or has been explicitly granted access to, 494 // because the doer may create issues or be mentioned in any public repo. 495 // So we need search issues in all public repos. 496 opts.AllPublic = true 497 } 498 499 switch filterMode { 500 case issues_model.FilterModeAll: 501 case issues_model.FilterModeYourRepositories: 502 case issues_model.FilterModeAssign: 503 opts.AssigneeID = ctx.Doer.ID 504 case issues_model.FilterModeCreate: 505 opts.PosterID = ctx.Doer.ID 506 case issues_model.FilterModeMention: 507 opts.MentionedID = ctx.Doer.ID 508 case issues_model.FilterModeReviewRequested: 509 opts.ReviewRequestedID = ctx.Doer.ID 510 case issues_model.FilterModeReviewed: 511 opts.ReviewedID = ctx.Doer.ID 512 } 513 514 // keyword holds the search term entered into the search field. 515 keyword := strings.Trim(ctx.FormString("q"), " ") 516 ctx.Data["Keyword"] = keyword 517 518 // Educated guess: Do or don't show closed issues. 519 isShowClosed := ctx.FormString("state") == "closed" 520 opts.IsClosed = optional.Some(isShowClosed) 521 522 // Make sure page number is at least 1. Will be posted to ctx.Data. 523 page := ctx.FormInt("page") 524 if page <= 1 { 525 page = 1 526 } 527 opts.Paginator = &db.ListOptions{ 528 Page: page, 529 PageSize: setting.UI.IssuePagingNum, 530 } 531 532 // Get IDs for labels (a filter option for issues/pulls). 533 // Required for IssuesOptions. 534 selectedLabels := ctx.FormString("labels") 535 if len(selectedLabels) > 0 && selectedLabels != "0" { 536 var err error 537 opts.LabelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ",")) 538 if err != nil { 539 ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true) 540 } 541 } 542 543 // ------------------------------ 544 // Get issues as defined by opts. 545 // ------------------------------ 546 547 // Slice of Issues that will be displayed on the overview page 548 // USING FINAL STATE OF opts FOR A QUERY. 549 var issues issues_model.IssueList 550 { 551 issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts).Copy( 552 func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy }, 553 )) 554 if err != nil { 555 ctx.ServerError("issueIDsFromSearch", err) 556 return 557 } 558 issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true) 559 if err != nil { 560 ctx.ServerError("GetIssuesByIDs", err) 561 return 562 } 563 } 564 565 commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues) 566 if err != nil { 567 ctx.ServerError("GetIssuesLastCommitStatus", err) 568 return 569 } 570 571 // ------------------------------- 572 // Fill stats to post to ctx.Data. 573 // ------------------------------- 574 issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( 575 func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy }, 576 )) 577 if err != nil { 578 ctx.ServerError("getUserIssueStats", err) 579 return 580 } 581 582 // Will be posted to ctx.Data. 583 var shownIssues int 584 if !isShowClosed { 585 shownIssues = int(issueStats.OpenCount) 586 } else { 587 shownIssues = int(issueStats.ClosedCount) 588 } 589 590 ctx.Data["IsShowClosed"] = isShowClosed 591 592 ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.FormString("RepoLink")) 593 594 if err := issues.LoadAttributes(ctx); err != nil { 595 ctx.ServerError("issues.LoadAttributes", err) 596 return 597 } 598 ctx.Data["Issues"] = issues 599 600 approvalCounts, err := issues.GetApprovalCounts(ctx) 601 if err != nil { 602 ctx.ServerError("ApprovalCounts", err) 603 return 604 } 605 ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 { 606 counts, ok := approvalCounts[issueID] 607 if !ok || len(counts) == 0 { 608 return 0 609 } 610 reviewTyp := issues_model.ReviewTypeApprove 611 if typ == "reject" { 612 reviewTyp = issues_model.ReviewTypeReject 613 } else if typ == "waiting" { 614 reviewTyp = issues_model.ReviewTypeRequest 615 } 616 for _, count := range counts { 617 if count.Type == reviewTyp { 618 return count.Count 619 } 620 } 621 return 0 622 } 623 ctx.Data["CommitLastStatus"] = lastStatus 624 ctx.Data["CommitStatuses"] = commitStatuses 625 ctx.Data["IssueStats"] = issueStats 626 ctx.Data["ViewType"] = viewType 627 ctx.Data["SortType"] = sortType 628 ctx.Data["IsShowClosed"] = isShowClosed 629 ctx.Data["SelectLabels"] = selectedLabels 630 ctx.Data["IsFuzzy"] = isFuzzy 631 632 if isShowClosed { 633 ctx.Data["State"] = "closed" 634 } else { 635 ctx.Data["State"] = "open" 636 } 637 638 pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5) 639 pager.AddParamString("q", keyword) 640 pager.AddParamString("type", viewType) 641 pager.AddParamString("sort", sortType) 642 pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) 643 pager.AddParamString("labels", selectedLabels) 644 pager.AddParamString("fuzzy", fmt.Sprintf("%v", isFuzzy)) 645 ctx.Data["Page"] = pager 646 647 ctx.HTML(http.StatusOK, tplIssues) 648 } 649 650 // ShowSSHKeys output all the ssh keys of user by uid 651 func ShowSSHKeys(ctx *context.Context) { 652 keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ 653 OwnerID: ctx.ContextUser.ID, 654 }) 655 if err != nil { 656 ctx.ServerError("ListPublicKeys", err) 657 return 658 } 659 660 var buf bytes.Buffer 661 for i := range keys { 662 buf.WriteString(keys[i].OmitEmail()) 663 buf.WriteString("\n") 664 } 665 ctx.PlainTextBytes(http.StatusOK, buf.Bytes()) 666 } 667 668 // ShowGPGKeys output all the public GPG keys of user by uid 669 func ShowGPGKeys(ctx *context.Context) { 670 keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{ 671 ListOptions: db.ListOptionsAll, 672 OwnerID: ctx.ContextUser.ID, 673 }) 674 if err != nil { 675 ctx.ServerError("ListGPGKeys", err) 676 return 677 } 678 679 entities := make([]*openpgp.Entity, 0) 680 failedEntitiesID := make([]string, 0) 681 for _, k := range keys { 682 e, err := asymkey_model.GPGKeyToEntity(ctx, k) 683 if err != nil { 684 if asymkey_model.IsErrGPGKeyImportNotExist(err) { 685 failedEntitiesID = append(failedEntitiesID, k.KeyID) 686 continue // Skip previous import without backup of imported armored key 687 } 688 ctx.ServerError("ShowGPGKeys", err) 689 return 690 } 691 entities = append(entities, e) 692 } 693 var buf bytes.Buffer 694 695 headers := make(map[string]string) 696 if len(failedEntitiesID) > 0 { // If some key need re-import to be exported 697 headers["Note"] = fmt.Sprintf("The keys with the following IDs couldn't be exported and need to be reuploaded %s", strings.Join(failedEntitiesID, ", ")) 698 } else if len(entities) == 0 { 699 headers["Note"] = "This user hasn't uploaded any GPG keys." 700 } 701 writer, _ := armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", headers) 702 for _, e := range entities { 703 err = e.Serialize(writer) // TODO find why key are exported with a different cipherTypeByte as original (should not be blocking but strange) 704 if err != nil { 705 ctx.ServerError("ShowGPGKeys", err) 706 return 707 } 708 } 709 writer.Close() 710 ctx.PlainTextBytes(http.StatusOK, buf.Bytes()) 711 } 712 713 func UsernameSubRoute(ctx *context.Context) { 714 // WORKAROUND to support usernames with "." in it 715 // https://github.com/go-chi/chi/issues/781 716 username := ctx.Params("username") 717 reloadParam := func(suffix string) (success bool) { 718 ctx.SetParams("username", strings.TrimSuffix(username, suffix)) 719 context.UserAssignmentWeb()(ctx) 720 if ctx.Written() { 721 return false 722 } 723 724 // check view permissions 725 if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { 726 ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name)) 727 return false 728 } 729 return true 730 } 731 switch { 732 case strings.HasSuffix(username, ".png"): 733 if reloadParam(".png") { 734 AvatarByUserName(ctx) 735 } 736 case strings.HasSuffix(username, ".keys"): 737 if reloadParam(".keys") { 738 ShowSSHKeys(ctx) 739 } 740 case strings.HasSuffix(username, ".gpg"): 741 if reloadParam(".gpg") { 742 ShowGPGKeys(ctx) 743 } 744 case strings.HasSuffix(username, ".rss"): 745 if !setting.Other.EnableFeed { 746 ctx.Error(http.StatusNotFound) 747 return 748 } 749 if reloadParam(".rss") { 750 feed.ShowUserFeedRSS(ctx) 751 } 752 case strings.HasSuffix(username, ".atom"): 753 if !setting.Other.EnableFeed { 754 ctx.Error(http.StatusNotFound) 755 return 756 } 757 if reloadParam(".atom") { 758 feed.ShowUserFeedAtom(ctx) 759 } 760 default: 761 context.UserAssignmentWeb()(ctx) 762 if !ctx.Written() { 763 ctx.Data["EnableFeed"] = setting.Other.EnableFeed 764 OwnerProfile(ctx) 765 } 766 } 767 } 768 769 func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (*issues_model.IssueStats, error) { 770 doerID := ctx.Doer.ID 771 772 opts = opts.Copy(func(o *issue_indexer.SearchOptions) { 773 // If the doer is the same as the context user, which means the doer is viewing his own dashboard, 774 // it's not enough to show the repos that the doer owns or has been explicitly granted access to, 775 // because the doer may create issues or be mentioned in any public repo. 776 // So we need search issues in all public repos. 777 o.AllPublic = doerID == ctxUser.ID 778 o.AssigneeID = nil 779 o.PosterID = nil 780 o.MentionID = nil 781 o.ReviewRequestedID = nil 782 o.ReviewedID = nil 783 }) 784 785 var ( 786 err error 787 ret = &issues_model.IssueStats{} 788 ) 789 790 { 791 openClosedOpts := opts.Copy() 792 switch filterMode { 793 case issues_model.FilterModeAll: 794 // no-op 795 case issues_model.FilterModeYourRepositories: 796 openClosedOpts.AllPublic = false 797 case issues_model.FilterModeAssign: 798 openClosedOpts.AssigneeID = optional.Some(doerID) 799 case issues_model.FilterModeCreate: 800 openClosedOpts.PosterID = optional.Some(doerID) 801 case issues_model.FilterModeMention: 802 openClosedOpts.MentionID = optional.Some(doerID) 803 case issues_model.FilterModeReviewRequested: 804 openClosedOpts.ReviewRequestedID = optional.Some(doerID) 805 case issues_model.FilterModeReviewed: 806 openClosedOpts.ReviewedID = optional.Some(doerID) 807 } 808 openClosedOpts.IsClosed = optional.Some(false) 809 ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) 810 if err != nil { 811 return nil, err 812 } 813 openClosedOpts.IsClosed = optional.Some(true) 814 ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) 815 if err != nil { 816 return nil, err 817 } 818 } 819 820 ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false })) 821 if err != nil { 822 return nil, err 823 } 824 ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) })) 825 if err != nil { 826 return nil, err 827 } 828 ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) })) 829 if err != nil { 830 return nil, err 831 } 832 ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = optional.Some(doerID) })) 833 if err != nil { 834 return nil, err 835 } 836 ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = optional.Some(doerID) })) 837 if err != nil { 838 return nil, err 839 } 840 ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = optional.Some(doerID) })) 841 if err != nil { 842 return nil, err 843 } 844 return ret, nil 845 }