You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

home.go 27KB

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