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  }