Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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