Files

848 lines
26 KiB
Go
Raw Permalink Normal View History

2014-04-13 01:57:42 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
2014-04-13 01:57:42 -04:00
package user
import (
2014-11-23 02:33:47 -05:00
"bytes"
2014-04-13 01:57:42 -04:00
"fmt"
"net/http"
2019-12-02 04:50:36 +01:00
"regexp"
"slices"
"sort"
2019-12-02 04:50:36 +01:00
"strconv"
"strings"
2014-04-13 01:57:42 -04:00
activities_model "code.gitea.io/gitea/models/activities"
2021-12-10 16:14:24 +08:00
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
2022-04-08 17:11:15 +08:00
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
2024-11-24 16:18:57 +08:00
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo"
2021-11-10 03:57:58 +08:00
"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"
2025-03-13 11:07:48 +08:00
"code.gitea.io/gitea/modules/indexer"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
2019-12-15 08:20:08 -06:00
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
2024-12-08 20:44:17 +08:00
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
"code.gitea.io/gitea/routers/web/shared/issue"
2024-12-08 20:44:17 +08:00
"code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
2024-11-29 09:53:49 -08:00
feed_service "code.gitea.io/gitea/services/feed"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
2017-12-25 18:25:16 -05:00
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"xorm.io/builder"
2014-04-13 01:57:42 -04:00
)
2014-06-22 23:11:12 -04:00
const (
tplDashboard templates.TplName = "user/dashboard/dashboard"
tplIssues templates.TplName = "user/dashboard/issues"
tplMilestones templates.TplName = "user/dashboard/milestones"
tplProfile templates.TplName = "user/profile"
2014-06-22 23:11:12 -04:00
)
2026-02-17 15:03:55 +01:00
// prepareDashboardContextUserOrgTeams finds out which context user dashboard is being viewed as .
func prepareDashboardContextUserOrgTeams(ctx *context.Context) *user_model.User {
2022-03-22 08:03:22 +01:00
ctxUser := ctx.Doer
2024-12-24 21:47:45 +08:00
orgName := ctx.PathParam("org")
if len(orgName) > 0 {
ctxUser = ctx.Org.Organization.AsUser()
ctx.Data["Teams"] = ctx.Org.Teams
2015-08-25 22:58:34 +08:00
}
ctx.Data["ContextUser"] = ctxUser
orgs, err := organization.GetUserOrgsList(ctx, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserOrgsList", err)
2015-08-25 22:58:34 +08:00
return nil
}
ctx.Data["Orgs"] = orgs
2015-08-25 22:58:34 +08:00
return ctxUser
}
2020-08-17 04:07:38 +01:00
// Dashboard render the dashboard page
2016-03-11 11:56:52 -05:00
func Dashboard(ctx *context.Context) {
2026-02-17 15:03:55 +01:00
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
2015-08-25 22:58:34 +08:00
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")
2016-07-24 01:08:22 +08:00
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
}
2023-07-04 20:36:08 +02:00
ctx.PageData["dashboardRepoList"] = map[string]any{
"searchLimit": setting.UI.User.RepoPagingNum,
"uid": uid,
}
2020-12-27 20:58:03 +01:00
2026-02-17 15:03:55 +01:00
prepareHeatmapURL(ctx)
2016-07-24 01:08:22 +08:00
feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctxUser,
2020-12-27 20:58:03 +01:00
RequestedTeam: ctx.Org.Team,
2022-03-22 08:03:22 +01:00
Actor: ctx.Doer,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: false,
Date: ctx.FormString("date"),
ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.FeedPagingNum,
},
})
2022-03-13 17:40:47 +01:00
if err != nil {
ctx.ServerError("GetFeeds", err)
2014-04-13 01:57:42 -04:00
return
}
pager := context.NewPagination(count, setting.UI.FeedPagingNum, page, 5).WithCurRows(len(feeds))
2024-12-30 09:57:38 +08:00
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["Feeds"] = feeds
ctx.HTML(http.StatusOK, tplDashboard)
2014-04-13 01:57:42 -04:00
}
2019-12-15 08:20:08 -06:00
// Milestones render the user milestones page
func Milestones(ctx *context.Context) {
2021-11-10 03:57:58 +08:00
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
}
2019-12-15 08:20:08 -06:00
ctx.Data["Title"] = ctx.Tr("milestones")
ctx.Data["PageIsMilestonesDashboard"] = true
2026-02-17 15:03:55 +01:00
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
2019-12-15 08:20:08 -06:00
if ctx.Written() {
return
}
repoOpts := repo_model.SearchRepoOptions{
Actor: ctx.Doer,
2020-12-27 20:58:03 +01:00
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
2020-12-27 20:58:03 +01:00
}
if ctxUser.IsOrganization() && ctx.Org.Team != nil {
repoOpts.TeamID = ctx.Org.Team.ID
}
2019-12-15 08:20:08 -06:00
2020-12-27 20:58:03 +01:00
var (
userRepoCond = repo_model.SearchRepositoryCondition(repoOpts) // all repo condition user could visit
repoCond = userRepoCond
repoIDs []int64
2019-12-15 08:20:08 -06:00
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
2019-12-15 08:20:08 -06:00
}
if len(reposQuery) != 0 {
if issueReposQueryPattern.MatchString(reposQuery) {
// remove "[" and "]" from string
reposQuery = reposQuery[1 : len(reposQuery)-1]
2022-01-20 18:46:10 +01:00
// for each ID (delimiter ",") add to int to repoIDs
2025-06-18 03:48:09 +02:00
for rID := range strings.SplitSeq(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)
}
2019-12-15 08:20:08 -06:00
}
}
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")
2019-12-15 08:20:08 -06:00
}
}
2023-12-11 16:56:48 +08:00
counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{
RepoCond: userRepoCond,
Name: keyword,
IsClosed: optional.Some(isShowClosed),
2023-12-11 16:56:48 +08:00
})
2019-12-15 08:20:08 -06:00
if err != nil {
ctx.ServerError("CountMilestonesByRepoIDs", err)
return
}
2023-12-11 16:56:48 +08:00
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),
2023-12-11 16:56:48 +08:00
SortType: sortType,
Name: keyword,
})
2019-12-15 08:20:08 -06:00
if err != nil {
ctx.ServerError("SearchMilestones", err)
2019-12-15 08:20:08 -06:00
return
}
showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, repoOpts, userRepoCond, false)
if err != nil {
ctx.ServerError("SearchRepositoryByCondition", err)
2019-12-15 08:20:08 -06:00
return
}
sort.Sort(showRepos)
2019-12-15 08:20:08 -06:00
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
}
2024-11-24 16:18:57 +08:00
rctx := renderhelper.NewRenderContextRepoComment(ctx, milestones[i].Repo)
milestones[i].RenderedContent, err = markdown.RenderString(rctx, milestones[i].Content)
2021-04-20 06:25:08 +08:00
if err != nil {
ctx.ServerError("RenderString", err)
return
}
2022-12-10 10:46:31 +08:00
if milestones[i].Repo.IsTimetrackerEnabled(ctx) {
err := milestones[i].LoadTotalTrackedTime(ctx)
2019-12-15 08:20:08 -06:00
if err != nil {
ctx.ServerError("LoadTotalTrackedTime", err)
return
}
}
i++
2019-12-15 08:20:08 -06:00
}
milestoneStats, err := issues_model.GetMilestonesStatsByRepoCondAndKw(ctx, repoCond, keyword)
2019-12-15 08:20:08 -06:00
if err != nil {
ctx.ServerError("GetMilestoneStats", err)
return
}
2022-04-08 17:11:15 +08:00
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
}
2019-12-15 08:20:08 -06:00
}
2024-02-14 13:19:57 -05:00
showRepoIDs := make(container.Set[int64], len(showRepos))
for _, repo := range showRepos {
if repo.ID > 0 {
2024-02-14 13:19:57 -05:00
showRepoIDs.Add(repo.ID)
}
}
if len(repoIDs) == 0 {
2024-02-14 13:19:57 -05:00
repoIDs = showRepoIDs.Values()
}
repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool {
2024-02-14 13:19:57 -05:00
return !showRepoIDs.Contains(v)
})
2026-03-08 15:35:50 +01:00
var pagerCount int64
2019-12-15 08:20:08 -06:00
if isShowClosed {
ctx.Data["State"] = "closed"
ctx.Data["Total"] = totalMilestoneStats.ClosedCount
2026-03-08 15:35:50 +01:00
pagerCount = milestoneStats.ClosedCount
2019-12-15 08:20:08 -06:00
} else {
ctx.Data["State"] = "open"
ctx.Data["Total"] = totalMilestoneStats.OpenCount
2026-03-08 15:35:50 +01:00
pagerCount = milestoneStats.OpenCount
2019-12-15 08:20:08 -06:00
}
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
2019-12-15 08:20:08 -06:00
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
2024-12-30 09:57:38 +08:00
pager.AddParamFromRequest(ctx.Req)
2019-12-15 08:20:08 -06:00
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestones)
2019-12-15 08:20:08 -06:00
}
// Pulls renders the user's pull request overview page
func Pulls(ctx *context.Context) {
2021-11-10 03:57:58 +08:00
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
2021-11-10 03:57:58 +08:00
buildIssueOverview(ctx, unit.TypePullRequests)
}
2019-12-02 04:50:36 +01:00
// Issues renders the user's issues overview page
2016-03-11 11:56:52 -05:00
func Issues(ctx *context.Context) {
2021-11-10 03:57:58 +08:00
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
2021-11-10 03:57:58 +08:00
buildIssueOverview(ctx, unit.TypeIssues)
}
// Regexp for repos query
var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
2021-11-10 03:57:58 +08:00
func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// ----------------------------------------------------
// Determine user; can be either user or organization.
// Return with NotFound or ServerError if unsuccessful.
// ----------------------------------------------------
2015-08-25 22:58:34 +08:00
2026-02-17 15:03:55 +01:00
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
2015-08-25 22:58:34 +08:00
if ctx.Written() {
return
}
// Default to recently updated, unlike repository issues list
2024-12-08 20:44:17 +08:00
sortType := util.IfZero(ctx.FormString("sort"), "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
2024-12-08 20:44:17 +08:00
viewType := ctx.FormString("type")
var filterMode int
switch viewType {
case "assigned":
filterMode = issues_model.FilterModeAssign
case "created_by":
filterMode = issues_model.FilterModeCreate
case "mentioned":
filterMode = issues_model.FilterModeMention
case "review_requested":
filterMode = issues_model.FilterModeReviewRequested
case "reviewed_by":
filterMode = issues_model.FilterModeReviewed
case "your_repositories":
fallthrough
default:
filterMode = issues_model.FilterModeYourRepositories
2019-12-03 00:01:29 -06:00
viewType = "your_repositories"
2015-08-25 22:58:34 +08:00
}
isPullList := unitType == unit.TypePullRequests
opts := &issues_model.IssuesOptions{
IsPull: optional.Some(isPullList),
SortType: sortType,
IsArchived: optional.Some(false),
Doer: ctx.Doer,
}
// --------------------------------------------------------------------------
// 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
// --------------------------------------------------------------------------
2017-02-14 22:15:18 +08:00
// Get repository IDs where User/Org/Team has access.
if ctx.Org != nil && ctx.Org.Organization != nil {
opts.Owner = ctx.Org.Organization.AsUser()
opts.Team = ctx.Org.Team
issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser())
if ctx.Written() {
return
}
}
2024-12-08 20:44:17 +08:00
// Get filter by author id & assignee id
// the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly.
// In the future, we need something like github: "author:user1" to accept usernames directly.
posterUsername := ctx.FormString("poster")
ctx.Data["FilterPosterUsername"] = posterUsername
2024-12-08 20:44:17 +08:00
opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername)
assigneeUsername := ctx.FormString("assignee")
ctx.Data["FilterAssigneeUsername"] = assigneeUsername
opts.AssigneeID = user.GetFilterUserIDByName(ctx, assigneeUsername)
2025-03-13 11:07:48 +08:00
searchMode := ctx.FormString("search_mode")
// 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 opts.Team != nil {
repoOpts.TeamID = opts.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
}
2017-02-14 22:15:18 +08:00
switch filterMode {
case issues_model.FilterModeAll:
case issues_model.FilterModeYourRepositories:
case issues_model.FilterModeAssign:
opts.AssigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
case issues_model.FilterModeCreate:
opts.PosterID = strconv.FormatInt(ctx.Doer.ID, 10)
case issues_model.FilterModeMention:
2022-03-22 08:03:22 +01:00
opts.MentionedID = ctx.Doer.ID
case issues_model.FilterModeReviewRequested:
2022-03-22 08:03:22 +01:00
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.
2025-06-18 03:48:09 +02:00
page := max(ctx.FormInt("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
2024-03-21 23:07:35 +08:00
opts.LabelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
if err != nil {
2024-03-21 23:07:35 +08:00
ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
}
}
// ------------------------------
// 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(
2025-03-13 11:07:48 +08:00
func(o *issue_indexer.SearchOptions) {
o.SearchMode = indexer.SearchModeType(searchMode)
},
))
if err != nil {
ctx.ServerError("issueIDsFromSearch", err)
return
}
issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true)
if err != nil {
ctx.ServerError("GetIssuesByIDs", err)
return
}
}
2015-09-02 16:18:09 -04:00
commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
if err != nil {
ctx.ServerError("GetIssuesLastCommitStatus", err)
return
2017-08-02 22:09:16 -07:00
}
if !ctx.Repo.CanRead(unit.TypeActions) {
for key := range commitStatuses {
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
}
}
2017-08-02 22:09:16 -07:00
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
2025-01-21 18:53:44 +08:00
issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
2024-12-08 20:44:17 +08:00
func(o *issue_indexer.SearchOptions) {
2025-03-13 11:07:48 +08:00
o.SearchMode = indexer.SearchModeType(searchMode)
2024-12-08 20:44:17 +08:00
},
))
if err != nil {
ctx.ServerError("getUserIssueStats", err)
return
2019-12-02 04:50:36 +01:00
}
// Will be posted to ctx.Data.
2026-03-08 15:35:50 +01:00
var shownIssues int64
2015-08-25 23:22:05 +08:00
if !isShowClosed {
2026-03-08 15:35:50 +01:00
shownIssues = issueStats.OpenCount
2015-08-25 23:22:05 +08:00
} else {
2026-03-08 15:35:50 +01:00
shownIssues = issueStats.ClosedCount
2015-08-25 23:22:05 +08:00
}
ctx.Data["IsShowClosed"] = isShowClosed
2022-01-20 18:46:10 +01:00
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
}
2015-08-25 22:58:34 +08:00
ctx.Data["Issues"] = issues
approvalCounts, err := issues.GetApprovalCounts(ctx)
if err != nil {
ctx.ServerError("ApprovalCounts", err)
return
}
2020-03-06 03:44:06 +00:00
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
counts, ok := approvalCounts[issueID]
if !ok || len(counts) == 0 {
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
2025-03-29 22:32:28 +01:00
switch typ {
case "reject":
reviewTyp = issues_model.ReviewTypeReject
2025-03-29 22:32:28 +01:00
case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
2020-03-06 03:44:06 +00:00
}
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
2015-08-25 22:58:34 +08:00
ctx.Data["ViewType"] = viewType
2015-11-04 12:50:02 -05:00
ctx.Data["SortType"] = sortType
2015-08-25 22:58:34 +08:00
ctx.Data["IsShowClosed"] = isShowClosed
2025-03-13 11:07:48 +08:00
ctx.Data["SearchModes"] = issue_indexer.SupportedSearchModes()
ctx.Data["SelectedSearchMode"] = ctx.FormTrim("search_mode")
2017-02-14 22:15:18 +08:00
2015-08-25 22:58:34 +08:00
if isShowClosed {
ctx.Data["State"] = "closed"
} else {
ctx.Data["State"] = "open"
}
2019-12-02 04:50:36 +01:00
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplIssues)
2015-08-25 22:58:34 +08:00
}
2016-11-27 12:59:12 +01:00
// 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,
})
2014-11-23 02:33:47 -05:00
if err != nil {
2018-01-10 22:34:17 +01:00
ctx.ServerError("ListPublicKeys", err)
2014-11-23 02:33:47 -05:00
return
}
var buf bytes.Buffer
2026-01-10 02:58:21 +08:00
// "authorized_keys" file format: "#" followed by comment line per key
buf.WriteString("# Gitea isn't a key server. The keys are exported as the user uploaded and might not have been fully verified.\n")
2014-11-23 02:33:47 -05:00
for i := range keys {
if keys[i].Type == asymkey_model.KeyTypePrincipal {
continue // SSH principal keys are not for signing or authentication
}
2014-11-23 02:33:47 -05:00
buf.WriteString(keys[i].OmitEmail())
2015-06-08 00:40:38 -07:00
buf.WriteString("\n")
2014-11-23 02:33:47 -05:00
}
2021-12-15 14:59:57 +08:00
ctx.PlainTextBytes(http.StatusOK, buf.Bytes())
2014-11-23 02:33:47 -05:00
}
// ShowGPGKeys output all the public GPG keys of user by uid
func ShowGPGKeys(ctx *context.Context) {
2024-01-15 10:19:25 +08:00
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 {
2021-12-10 16:14:24 +08:00
if asymkey_model.IsErrGPGKeyImportNotExist(err) {
failedEntitiesID = append(failedEntitiesID, k.KeyID)
2022-01-20 18:46:10 +01:00
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)
2026-01-10 02:58:21 +08:00
// https://www.rfc-editor.org/rfc/rfc4880
headers["Comment"] = "Gitea isn't a key server. The keys are exported as the user uploaded and might not have been fully verified."
2022-01-20 18:46:10 +01:00
if len(failedEntitiesID) > 0 { // If some key need re-import to be exported
2025-04-01 12:14:01 +02:00
headers["Note"] = "The keys with the following IDs couldn't be exported and need to be reuploaded " + 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 {
2022-01-20 18:46:10 +01:00
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()
2021-12-15 14:59:57 +08:00
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
2024-06-19 06:32:45 +08:00
username := ctx.PathParam("username")
reloadParam := func(suffix string) (success bool) {
2024-06-19 06:32:45 +08:00
ctx.SetPathParam("username", strings.TrimSuffix(username, suffix))
context.UserAssignmentWeb()(ctx)
2024-03-13 14:57:30 +08:00
if ctx.Written() {
return false
}
// check view permissions
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
2025-02-17 14:13:17 +08:00
ctx.NotFound(fmt.Errorf("%s", ctx.ContextUser.Name))
return false
}
2024-03-13 14:57:30 +08:00
return true
}
switch {
case strings.HasSuffix(username, ".png"):
if reloadParam(".png") {
2025-01-30 07:33:50 +08:00
AvatarByUsernameSize(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 {
2025-02-17 14:13:17 +08:00
ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".rss") {
feed.ShowUserFeedRSS(ctx)
}
case strings.HasSuffix(username, ".atom"):
if !setting.Other.EnableFeed {
2025-02-17 14:13:17 +08:00
ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".atom") {
feed.ShowUserFeedAtom(ctx)
}
default:
context.UserAssignmentWeb()(ctx)
if !ctx.Written() {
ctx.Data["EnableFeed"] = setting.Other.EnableFeed
OwnerProfile(ctx)
}
}
}
2025-01-21 18:53:44 +08:00
func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
2024-12-08 20:44:17 +08:00
ret = &issues_model.IssueStats{}
doerID := ctx.Doer.ID
2025-01-21 18:53:44 +08:00
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
})
// Open/Closed are for the tabs of the issue list
{
openClosedOpts := opts.Copy()
switch filterMode {
case issues_model.FilterModeAll:
// no-op
case issues_model.FilterModeYourRepositories:
openClosedOpts.AllPublic = false
case issues_model.FilterModeAssign:
openClosedOpts.AssigneeID = strconv.FormatInt(doerID, 10)
case issues_model.FilterModeCreate:
openClosedOpts.PosterID = strconv.FormatInt(doerID, 10)
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
}
}
2025-01-21 18:53:44 +08:00
// Below stats are for the left sidebar
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
o.AssigneeID = ""
o.PosterID = ""
2025-01-21 18:53:44 +08:00
o.MentionID = nil
o.ReviewRequestedID = nil
o.ReviewedID = nil
})
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 = strconv.FormatInt(doerID, 10) }))
if err != nil {
return nil, err
}
ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = strconv.FormatInt(doerID, 10) }))
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
}