summaryrefslogtreecommitdiffstats
path: root/routers/web/user/home.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--routers/web/user/home.go883
1 files changed, 883 insertions, 0 deletions
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
new file mode 100644
index 0000000..4b249e9
--- /dev/null
+++ b/routers/web/user/home.go
@@ -0,0 +1,883 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "regexp"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/container"
+ issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+ "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/routers/web/feed"
+ "code.gitea.io/gitea/services/context"
+ issue_service "code.gitea.io/gitea/services/issue"
+ pull_service "code.gitea.io/gitea/services/pull"
+
+ "github.com/ProtonMail/go-crypto/openpgp"
+ "github.com/ProtonMail/go-crypto/openpgp/armor"
+ "xorm.io/builder"
+)
+
+const (
+ tplDashboard base.TplName = "user/dashboard/dashboard"
+ tplIssues base.TplName = "user/dashboard/issues"
+ tplMilestones base.TplName = "user/dashboard/milestones"
+ tplProfile base.TplName = "user/profile"
+)
+
+// getDashboardContextUser finds out which context user dashboard is being viewed as .
+func getDashboardContextUser(ctx *context.Context) *user_model.User {
+ ctxUser := ctx.Doer
+ orgName := ctx.Params(":org")
+ if len(orgName) > 0 {
+ ctxUser = ctx.Org.Organization.AsUser()
+ ctx.Data["Teams"] = ctx.Org.Teams
+ }
+ ctx.Data["ContextUser"] = ctxUser
+
+ orgs, err := organization.GetUserOrgsList(ctx, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserOrgsList", err)
+ return nil
+ }
+ ctx.Data["Orgs"] = orgs
+
+ return ctxUser
+}
+
+// Dashboard render the dashboard page
+func Dashboard(ctx *context.Context) {
+ ctxUser := getDashboardContextUser(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ var (
+ date = ctx.FormString("date")
+ page = ctx.FormInt("page")
+ )
+
+ // Make sure page number is at least 1. Will be posted to ctx.Data.
+ if page <= 1 {
+ page = 1
+ }
+
+ ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Locale.TrString("dashboard")
+ ctx.Data["PageIsDashboard"] = true
+ ctx.Data["PageIsNews"] = true
+ cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)
+ ctx.Data["UserOrgsCount"] = cnt
+ ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
+ ctx.Data["Date"] = date
+
+ var uid int64
+ if ctxUser != nil {
+ uid = ctxUser.ID
+ }
+
+ ctx.PageData["dashboardRepoList"] = map[string]any{
+ "searchLimit": setting.UI.User.RepoPagingNum,
+ "uid": uid,
+ }
+
+ if setting.Service.EnableUserHeatmap {
+ data, err := activities_model.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
+ return
+ }
+ ctx.Data["HeatmapData"] = data
+ ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
+ }
+
+ feeds, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
+ RequestedUser: ctxUser,
+ RequestedTeam: ctx.Org.Team,
+ Actor: ctx.Doer,
+ IncludePrivate: true,
+ OnlyPerformedBy: false,
+ IncludeDeleted: false,
+ Date: ctx.FormString("date"),
+ ListOptions: db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.FeedPagingNum,
+ },
+ })
+ if err != nil {
+ ctx.ServerError("GetFeeds", err)
+ return
+ }
+
+ ctx.Data["Feeds"] = feeds
+
+ pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
+ pager.AddParam(ctx, "date", "Date")
+ ctx.Data["Page"] = pager
+
+ ctx.HTML(http.StatusOK, tplDashboard)
+}
+
+// Milestones render the user milestones page
+func Milestones(ctx *context.Context) {
+ if unit.TypeIssues.UnitGlobalDisabled() && unit.TypePullRequests.UnitGlobalDisabled() {
+ log.Debug("Milestones overview page not available as both issues and pull requests are globally disabled")
+ ctx.Status(http.StatusNotFound)
+ return
+ }
+
+ ctx.Data["Title"] = ctx.Tr("milestones")
+ ctx.Data["PageIsMilestonesDashboard"] = true
+
+ ctxUser := getDashboardContextUser(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ repoOpts := repo_model.SearchRepoOptions{
+ Actor: ctx.Doer,
+ OwnerID: ctxUser.ID,
+ Private: true,
+ AllPublic: false, // Include also all public repositories of users and public organisations
+ AllLimited: false, // Include also all public repositories of limited organisations
+ Archived: optional.Some(false),
+ HasMilestones: optional.Some(true), // Just needs display repos has milestones
+ }
+
+ if ctxUser.IsOrganization() && ctx.Org.Team != nil {
+ repoOpts.TeamID = ctx.Org.Team.ID
+ }
+
+ var (
+ userRepoCond = repo_model.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
+ repoCond = userRepoCond
+ repoIDs []int64
+
+ reposQuery = ctx.FormString("repos")
+ isShowClosed = ctx.FormString("state") == "closed"
+ sortType = ctx.FormString("sort")
+ page = ctx.FormInt("page")
+ keyword = ctx.FormTrim("q")
+ )
+
+ if page <= 1 {
+ page = 1
+ }
+
+ if len(reposQuery) != 0 {
+ if issueReposQueryPattern.MatchString(reposQuery) {
+ // remove "[" and "]" from string
+ reposQuery = reposQuery[1 : len(reposQuery)-1]
+ // for each ID (delimiter ",") add to int to repoIDs
+
+ for _, rID := range strings.Split(reposQuery, ",") {
+ // Ensure nonempty string entries
+ if rID != "" && rID != "0" {
+ rIDint64, err := strconv.ParseInt(rID, 10, 64)
+ // If the repo id specified by query is not parseable or not accessible by user, just ignore it.
+ if err == nil {
+ repoIDs = append(repoIDs, rIDint64)
+ }
+ }
+ }
+ if len(repoIDs) > 0 {
+ // Don't just let repoCond = builder.In("id", repoIDs) because user may has no permission on repoIDs
+ // But the original repoCond has a limitation
+ repoCond = repoCond.And(builder.In("id", repoIDs))
+ }
+ } else {
+ log.Warn("issueReposQueryPattern not match with query")
+ }
+ }
+
+ counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{
+ RepoCond: userRepoCond,
+ Name: keyword,
+ IsClosed: optional.Some(isShowClosed),
+ })
+ if err != nil {
+ ctx.ServerError("CountMilestonesByRepoIDs", err)
+ return
+ }
+
+ milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+ ListOptions: db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.IssuePagingNum,
+ },
+ RepoCond: repoCond,
+ IsClosed: optional.Some(isShowClosed),
+ SortType: sortType,
+ Name: keyword,
+ })
+ if err != nil {
+ ctx.ServerError("SearchMilestones", err)
+ return
+ }
+
+ showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, &repoOpts, userRepoCond, false)
+ if err != nil {
+ ctx.ServerError("SearchRepositoryByCondition", err)
+ return
+ }
+ sort.Sort(showRepos)
+
+ for i := 0; i < len(milestones); {
+ for _, repo := range showRepos {
+ if milestones[i].RepoID == repo.ID {
+ milestones[i].Repo = repo
+ break
+ }
+ }
+ if milestones[i].Repo == nil {
+ log.Warn("Cannot find milestone %d 's repository %d", milestones[i].ID, milestones[i].RepoID)
+ milestones = append(milestones[:i], milestones[i+1:]...)
+ continue
+ }
+
+ milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+ Links: markup.Links{
+ Base: milestones[i].Repo.Link(),
+ },
+ Metas: milestones[i].Repo.ComposeMetas(ctx),
+ Ctx: ctx,
+ }, milestones[i].Content)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+
+ if milestones[i].Repo.IsTimetrackerEnabled(ctx) {
+ err := milestones[i].LoadTotalTrackedTime(ctx)
+ if err != nil {
+ ctx.ServerError("LoadTotalTrackedTime", err)
+ return
+ }
+ }
+ i++
+ }
+
+ milestoneStats, err := issues_model.GetMilestonesStatsByRepoCondAndKw(ctx, repoCond, keyword)
+ if err != nil {
+ ctx.ServerError("GetMilestoneStats", err)
+ return
+ }
+
+ var totalMilestoneStats *issues_model.MilestonesStats
+ if len(repoIDs) == 0 {
+ totalMilestoneStats = milestoneStats
+ } else {
+ totalMilestoneStats, err = issues_model.GetMilestonesStatsByRepoCondAndKw(ctx, userRepoCond, keyword)
+ if err != nil {
+ ctx.ServerError("GetMilestoneStats", err)
+ return
+ }
+ }
+
+ showRepoIDs := make(container.Set[int64], len(showRepos))
+ for _, repo := range showRepos {
+ if repo.ID > 0 {
+ showRepoIDs.Add(repo.ID)
+ }
+ }
+ if len(repoIDs) == 0 {
+ repoIDs = showRepoIDs.Values()
+ }
+ repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool {
+ return !showRepoIDs.Contains(v)
+ })
+
+ var pagerCount int
+ if isShowClosed {
+ ctx.Data["State"] = "closed"
+ ctx.Data["Total"] = totalMilestoneStats.ClosedCount
+ pagerCount = int(milestoneStats.ClosedCount)
+ } else {
+ ctx.Data["State"] = "open"
+ ctx.Data["Total"] = totalMilestoneStats.OpenCount
+ pagerCount = int(milestoneStats.OpenCount)
+ }
+
+ ctx.Data["Milestones"] = milestones
+ ctx.Data["Repos"] = showRepos
+ ctx.Data["Counts"] = counts
+ ctx.Data["MilestoneStats"] = milestoneStats
+ ctx.Data["SortType"] = sortType
+ ctx.Data["Keyword"] = keyword
+ ctx.Data["RepoIDs"] = repoIDs
+ ctx.Data["IsShowClosed"] = isShowClosed
+
+ pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
+ pager.AddParam(ctx, "q", "Keyword")
+ pager.AddParam(ctx, "repos", "RepoIDs")
+ pager.AddParam(ctx, "sort", "SortType")
+ pager.AddParam(ctx, "state", "State")
+ ctx.Data["Page"] = pager
+
+ ctx.HTML(http.StatusOK, tplMilestones)
+}
+
+// Pulls renders the user's pull request overview page
+func Pulls(ctx *context.Context) {
+ if unit.TypePullRequests.UnitGlobalDisabled() {
+ log.Debug("Pull request overview page not available as it is globally disabled.")
+ ctx.Status(http.StatusNotFound)
+ return
+ }
+
+ ctx.Data["Title"] = ctx.Tr("pull_requests")
+ ctx.Data["PageIsPulls"] = true
+ buildIssueOverview(ctx, unit.TypePullRequests)
+}
+
+// Issues renders the user's issues overview page
+func Issues(ctx *context.Context) {
+ if unit.TypeIssues.UnitGlobalDisabled() {
+ log.Debug("Issues overview page not available as it is globally disabled.")
+ ctx.Status(http.StatusNotFound)
+ return
+ }
+
+ ctx.Data["Title"] = ctx.Tr("issues")
+ ctx.Data["PageIsIssues"] = true
+ buildIssueOverview(ctx, unit.TypeIssues)
+}
+
+// Regexp for repos query
+var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
+
+func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
+ // ----------------------------------------------------
+ // Determine user; can be either user or organization.
+ // Return with NotFound or ServerError if unsuccessful.
+ // ----------------------------------------------------
+
+ ctxUser := getDashboardContextUser(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ var (
+ viewType string
+ sortType = ctx.FormString("sort")
+ filterMode int
+ )
+
+ // Default to recently updated, unlike repository issues list
+ if sortType == "" {
+ sortType = "recentupdate"
+ }
+
+ // --------------------------------------------------------------------------------
+ // Distinguish User from Organization.
+ // Org:
+ // - Remember pre-determined viewType string for later. Will be posted to ctx.Data.
+ // Organization does not have view type and filter mode.
+ // User:
+ // - Use ctx.FormString("type") to determine filterMode.
+ // The type is set when clicking for example "assigned to me" on the overview page.
+ // - Remember either this or a fallback. Will be posted to ctx.Data.
+ // --------------------------------------------------------------------------------
+
+ // TODO: distinguish during routing
+
+ viewType = ctx.FormString("type")
+ switch viewType {
+ case "assigned":
+ filterMode = issues_model.FilterModeAssign
+ case "mentioned":
+ filterMode = issues_model.FilterModeMention
+ case "review_requested":
+ filterMode = issues_model.FilterModeReviewRequested
+ case "reviewed_by":
+ filterMode = issues_model.FilterModeReviewed
+ case "your_repositories":
+ filterMode = issues_model.FilterModeYourRepositories
+ case "created_by":
+ fallthrough
+ default:
+ filterMode = issues_model.FilterModeCreate
+ viewType = "created_by"
+ }
+
+ // --------------------------------------------------------------------------
+ // Build opts (IssuesOptions), which contains filter information.
+ // Will eventually be used to retrieve issues relevant for the overview page.
+ // Note: Non-final states of opts are used in-between, namely for:
+ // - Keyword search
+ // - Count Issues by repo
+ // --------------------------------------------------------------------------
+
+ // Get repository IDs where User/Org/Team has access.
+ var team *organization.Team
+ var org *organization.Organization
+ if ctx.Org != nil {
+ org = ctx.Org.Organization
+ team = ctx.Org.Team
+ }
+
+ isPullList := unitType == unit.TypePullRequests
+ opts := &issues_model.IssuesOptions{
+ IsPull: optional.Some(isPullList),
+ SortType: sortType,
+ IsArchived: optional.Some(false),
+ Org: org,
+ Team: team,
+ User: ctx.Doer,
+ }
+
+ isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
+
+ // Search all repositories which
+ //
+ // As user:
+ // - Owns the repository.
+ // - Have collaborator permissions in repository.
+ //
+ // As org:
+ // - Owns the repository.
+ //
+ // As team:
+ // - Team org's owns the repository.
+ // - Team has read permission to repository.
+ repoOpts := &repo_model.SearchRepoOptions{
+ Actor: ctx.Doer,
+ OwnerID: ctxUser.ID,
+ Private: true,
+ AllPublic: false,
+ AllLimited: false,
+ Collaborate: optional.None[bool](),
+ UnitType: unitType,
+ Archived: optional.Some(false),
+ }
+ if team != nil {
+ repoOpts.TeamID = team.ID
+ }
+ accessibleRepos := container.Set[int64]{}
+ {
+ ids, _, err := repo_model.SearchRepositoryIDs(ctx, repoOpts)
+ if err != nil {
+ ctx.ServerError("SearchRepositoryIDs", err)
+ return
+ }
+ accessibleRepos.AddMultiple(ids...)
+ opts.RepoIDs = ids
+ if len(opts.RepoIDs) == 0 {
+ // no repos found, don't let the indexer return all repos
+ opts.RepoIDs = []int64{0}
+ }
+ }
+ if ctx.Doer.ID == ctxUser.ID && filterMode != issues_model.FilterModeYourRepositories {
+ // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
+ // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
+ // because the doer may create issues or be mentioned in any public repo.
+ // So we need search issues in all public repos.
+ opts.AllPublic = true
+ }
+
+ switch filterMode {
+ case issues_model.FilterModeAll:
+ case issues_model.FilterModeYourRepositories:
+ case issues_model.FilterModeAssign:
+ opts.AssigneeID = ctx.Doer.ID
+ case issues_model.FilterModeCreate:
+ opts.PosterID = ctx.Doer.ID
+ case issues_model.FilterModeMention:
+ opts.MentionedID = ctx.Doer.ID
+ case issues_model.FilterModeReviewRequested:
+ opts.ReviewRequestedID = ctx.Doer.ID
+ case issues_model.FilterModeReviewed:
+ opts.ReviewedID = ctx.Doer.ID
+ }
+
+ // keyword holds the search term entered into the search field.
+ keyword := strings.Trim(ctx.FormString("q"), " ")
+ ctx.Data["Keyword"] = keyword
+
+ // Educated guess: Do or don't show closed issues.
+ isShowClosed := ctx.FormString("state") == "closed"
+ opts.IsClosed = optional.Some(isShowClosed)
+
+ // Make sure page number is at least 1. Will be posted to ctx.Data.
+ page := ctx.FormInt("page")
+ if page <= 1 {
+ page = 1
+ }
+ opts.Paginator = &db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.IssuePagingNum,
+ }
+
+ // Get IDs for labels (a filter option for issues/pulls).
+ // Required for IssuesOptions.
+ selectedLabels := ctx.FormString("labels")
+ if len(selectedLabels) > 0 && selectedLabels != "0" {
+ var err error
+ opts.LabelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
+ if err != nil {
+ ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
+ }
+ }
+
+ if org != nil {
+ // Get Org Labels
+ labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByOrgID", err)
+ return
+ }
+
+ // Get the exclusive scope for every label ID
+ labelExclusiveScopes := make([]string, 0, len(opts.LabelIDs))
+ for _, labelID := range opts.LabelIDs {
+ foundExclusiveScope := false
+ for _, label := range labels {
+ if label.ID == labelID || label.ID == -labelID {
+ labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
+ foundExclusiveScope = true
+ break
+ }
+ }
+ if !foundExclusiveScope {
+ labelExclusiveScopes = append(labelExclusiveScopes, "")
+ }
+ }
+
+ for _, l := range labels {
+ l.LoadSelectedLabelsAfterClick(opts.LabelIDs, labelExclusiveScopes)
+ }
+ ctx.Data["Labels"] = labels
+ }
+
+ // ------------------------------
+ // Get issues as defined by opts.
+ // ------------------------------
+
+ // Slice of Issues that will be displayed on the overview page
+ // USING FINAL STATE OF opts FOR A QUERY.
+ var issues issues_model.IssueList
+ {
+ issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts).Copy(
+ func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy },
+ ))
+ if err != nil {
+ ctx.ServerError("issueIDsFromSearch", err)
+ return
+ }
+ issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true)
+ if err != nil {
+ ctx.ServerError("GetIssuesByIDs", err)
+ return
+ }
+ }
+
+ commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
+ if err != nil {
+ ctx.ServerError("GetIssuesLastCommitStatus", err)
+ return
+ }
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ for key := range commitStatuses {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
+ }
+ }
+
+ // -------------------------------
+ // Fill stats to post to ctx.Data.
+ // -------------------------------
+ issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
+ func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy },
+ ))
+ if err != nil {
+ ctx.ServerError("getUserIssueStats", err)
+ return
+ }
+
+ // Will be posted to ctx.Data.
+ var shownIssues int
+ if !isShowClosed {
+ shownIssues = int(issueStats.OpenCount)
+ } else {
+ shownIssues = int(issueStats.ClosedCount)
+ }
+
+ ctx.Data["IsShowClosed"] = isShowClosed
+
+ ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.FormString("RepoLink"))
+
+ if err := issues.LoadAttributes(ctx); err != nil {
+ ctx.ServerError("issues.LoadAttributes", err)
+ return
+ }
+ ctx.Data["Issues"] = issues
+
+ approvalCounts, err := issues.GetApprovalCounts(ctx)
+ if err != nil {
+ ctx.ServerError("ApprovalCounts", err)
+ return
+ }
+ ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
+ counts, ok := approvalCounts[issueID]
+ if !ok || len(counts) == 0 {
+ return 0
+ }
+ reviewTyp := issues_model.ReviewTypeApprove
+ if typ == "reject" {
+ reviewTyp = issues_model.ReviewTypeReject
+ } else if typ == "waiting" {
+ reviewTyp = issues_model.ReviewTypeRequest
+ }
+ for _, count := range counts {
+ if count.Type == reviewTyp {
+ return count.Count
+ }
+ }
+ return 0
+ }
+ ctx.Data["CommitLastStatus"] = lastStatus
+ ctx.Data["CommitStatuses"] = commitStatuses
+ ctx.Data["IssueStats"] = issueStats
+ ctx.Data["ViewType"] = viewType
+ ctx.Data["SortType"] = sortType
+ ctx.Data["IsShowClosed"] = isShowClosed
+ ctx.Data["SelectLabels"] = selectedLabels
+ ctx.Data["PageIsOrgIssues"] = org != nil
+ ctx.Data["IsFuzzy"] = isFuzzy
+
+ if isShowClosed {
+ ctx.Data["State"] = "closed"
+ } else {
+ ctx.Data["State"] = "open"
+ }
+
+ pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
+ pager.AddParam(ctx, "q", "Keyword")
+ pager.AddParam(ctx, "type", "ViewType")
+ pager.AddParam(ctx, "sort", "SortType")
+ pager.AddParam(ctx, "state", "State")
+ pager.AddParam(ctx, "labels", "SelectLabels")
+ pager.AddParam(ctx, "milestone", "MilestoneID")
+ pager.AddParam(ctx, "assignee", "AssigneeID")
+ pager.AddParam(ctx, "fuzzy", "IsFuzzy")
+ ctx.Data["Page"] = pager
+
+ ctx.HTML(http.StatusOK, tplIssues)
+}
+
+// ShowSSHKeys output all the ssh keys of user by uid
+func ShowSSHKeys(ctx *context.Context) {
+ keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
+ OwnerID: ctx.ContextUser.ID,
+ })
+ if err != nil {
+ ctx.ServerError("ListPublicKeys", err)
+ return
+ }
+
+ var buf bytes.Buffer
+ for i := range keys {
+ buf.WriteString(keys[i].OmitEmail())
+ buf.WriteString("\n")
+ }
+ ctx.PlainTextBytes(http.StatusOK, buf.Bytes())
+}
+
+// ShowGPGKeys output all the public GPG keys of user by uid
+func ShowGPGKeys(ctx *context.Context) {
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ ListOptions: db.ListOptionsAll,
+ OwnerID: ctx.ContextUser.ID,
+ })
+ if err != nil {
+ ctx.ServerError("ListGPGKeys", err)
+ return
+ }
+
+ entities := make([]*openpgp.Entity, 0)
+ failedEntitiesID := make([]string, 0)
+ for _, k := range keys {
+ e, err := asymkey_model.GPGKeyToEntity(ctx, k)
+ if err != nil {
+ if asymkey_model.IsErrGPGKeyImportNotExist(err) {
+ failedEntitiesID = append(failedEntitiesID, k.KeyID)
+ continue // Skip previous import without backup of imported armored key
+ }
+ ctx.ServerError("ShowGPGKeys", err)
+ return
+ }
+ entities = append(entities, e)
+ }
+ var buf bytes.Buffer
+
+ headers := make(map[string]string)
+ if len(failedEntitiesID) > 0 { // If some key need re-import to be exported
+ headers["Note"] = fmt.Sprintf("The keys with the following IDs couldn't be exported and need to be reuploaded %s", strings.Join(failedEntitiesID, ", "))
+ } else if len(entities) == 0 {
+ headers["Note"] = "This user hasn't uploaded any GPG keys."
+ }
+ writer, _ := armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", headers)
+ for _, e := range entities {
+ err = e.Serialize(writer) // TODO find why key are exported with a different cipherTypeByte as original (should not be blocking but strange)
+ if err != nil {
+ ctx.ServerError("ShowGPGKeys", err)
+ return
+ }
+ }
+ writer.Close()
+ 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.Params("username")
+ reloadParam := func(suffix string) (success bool) {
+ ctx.SetParams("username", strings.TrimSuffix(username, suffix))
+ context.UserAssignmentWeb()(ctx)
+ if ctx.Written() {
+ return false
+ }
+ // check view permissions
+ if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
+ ctx.NotFound("User not visible", nil)
+ return false
+ }
+ return true
+ }
+ switch {
+ case strings.HasSuffix(username, ".png"):
+ if reloadParam(".png") {
+ AvatarByUserName(ctx)
+ }
+ 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 !setting.Other.EnableFeed {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ if reloadParam(".rss") {
+ feed.ShowUserFeedRSS(ctx)
+ }
+ case strings.HasSuffix(username, ".atom"):
+ if !setting.Other.EnableFeed {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ if reloadParam(".atom") {
+ feed.ShowUserFeedAtom(ctx)
+ }
+ default:
+ context.UserAssignmentWeb()(ctx)
+ if !ctx.Written() {
+ ctx.Data["EnableFeed"] = setting.Other.EnableFeed
+ OwnerProfile(ctx)
+ }
+ }
+}
+
+func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (*issues_model.IssueStats, error) {
+ doerID := ctx.Doer.ID
+
+ opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
+ // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
+ // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
+ // because the doer may create issues or be mentioned in any public repo.
+ // So we need search issues in all public repos.
+ o.AllPublic = doerID == ctxUser.ID
+ o.AssigneeID = nil
+ o.PosterID = nil
+ o.MentionID = nil
+ o.ReviewRequestedID = nil
+ o.ReviewedID = nil
+ })
+
+ var (
+ err error
+ ret = &issues_model.IssueStats{}
+ )
+
+ {
+ openClosedOpts := opts.Copy()
+ switch filterMode {
+ case issues_model.FilterModeAll:
+ // no-op
+ case issues_model.FilterModeYourRepositories:
+ openClosedOpts.AllPublic = false
+ case issues_model.FilterModeAssign:
+ openClosedOpts.AssigneeID = optional.Some(doerID)
+ case issues_model.FilterModeCreate:
+ openClosedOpts.PosterID = optional.Some(doerID)
+ case issues_model.FilterModeMention:
+ openClosedOpts.MentionID = optional.Some(doerID)
+ case issues_model.FilterModeReviewRequested:
+ openClosedOpts.ReviewRequestedID = optional.Some(doerID)
+ case issues_model.FilterModeReviewed:
+ openClosedOpts.ReviewedID = optional.Some(doerID)
+ }
+ openClosedOpts.IsClosed = optional.Some(false)
+ ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
+ if err != nil {
+ return nil, err
+ }
+ openClosedOpts.IsClosed = optional.Some(true)
+ ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false }))
+ if err != nil {
+ return nil, err
+ }
+ ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) }))
+ if err != nil {
+ return nil, err
+ }
+ ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) }))
+ if err != nil {
+ return nil, err
+ }
+ ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = optional.Some(doerID) }))
+ if err != nil {
+ return nil, err
+ }
+ ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = optional.Some(doerID) }))
+ if err != nil {
+ return nil, err
+ }
+ ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = optional.Some(doerID) }))
+ if err != nil {
+ return nil, err
+ }
+ return ret, nil
+}