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