gitea源码

issue.go 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "errors"
  7. "fmt"
  8. "html/template"
  9. "net/http"
  10. "strconv"
  11. "strings"
  12. "code.gitea.io/gitea/models/db"
  13. issues_model "code.gitea.io/gitea/models/issues"
  14. "code.gitea.io/gitea/models/organization"
  15. access_model "code.gitea.io/gitea/models/perm/access"
  16. project_model "code.gitea.io/gitea/models/project"
  17. "code.gitea.io/gitea/models/renderhelper"
  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/log"
  22. "code.gitea.io/gitea/modules/markup/markdown"
  23. "code.gitea.io/gitea/modules/optional"
  24. api "code.gitea.io/gitea/modules/structs"
  25. "code.gitea.io/gitea/modules/templates"
  26. "code.gitea.io/gitea/modules/util"
  27. "code.gitea.io/gitea/modules/web"
  28. "code.gitea.io/gitea/routers/common"
  29. "code.gitea.io/gitea/services/context"
  30. "code.gitea.io/gitea/services/convert"
  31. "code.gitea.io/gitea/services/forms"
  32. issue_service "code.gitea.io/gitea/services/issue"
  33. )
  34. const (
  35. tplAttachment templates.TplName = "repo/issue/view_content/attachments"
  36. tplIssues templates.TplName = "repo/issue/list"
  37. tplIssueNew templates.TplName = "repo/issue/new"
  38. tplIssueChoose templates.TplName = "repo/issue/choose"
  39. tplIssueView templates.TplName = "repo/issue/view"
  40. tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
  41. tplReactions templates.TplName = "repo/issue/view_content/reactions"
  42. issueTemplateKey = "IssueTemplate"
  43. issueTemplateTitleKey = "IssueTemplateTitle"
  44. )
  45. // IssueTemplateCandidates issue templates
  46. var IssueTemplateCandidates = []string{
  47. "ISSUE_TEMPLATE.md",
  48. "ISSUE_TEMPLATE.yaml",
  49. "ISSUE_TEMPLATE.yml",
  50. "issue_template.md",
  51. "issue_template.yaml",
  52. "issue_template.yml",
  53. ".gitea/ISSUE_TEMPLATE.md",
  54. ".gitea/ISSUE_TEMPLATE.yaml",
  55. ".gitea/ISSUE_TEMPLATE.yml",
  56. ".gitea/issue_template.md",
  57. ".gitea/issue_template.yaml",
  58. ".gitea/issue_template.yml",
  59. ".github/ISSUE_TEMPLATE.md",
  60. ".github/ISSUE_TEMPLATE.yaml",
  61. ".github/ISSUE_TEMPLATE.yml",
  62. ".github/issue_template.md",
  63. ".github/issue_template.yaml",
  64. ".github/issue_template.yml",
  65. }
  66. // MustAllowUserComment checks to make sure if an issue is locked.
  67. // If locked and user has permissions to write to the repository,
  68. // then the comment is allowed, else it is blocked
  69. func MustAllowUserComment(ctx *context.Context) {
  70. issue := GetActionIssue(ctx)
  71. if ctx.Written() {
  72. return
  73. }
  74. if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
  75. ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
  76. ctx.Redirect(issue.Link())
  77. return
  78. }
  79. }
  80. // MustEnableIssues check if repository enable internal issues
  81. func MustEnableIssues(ctx *context.Context) {
  82. if !ctx.Repo.CanRead(unit.TypeIssues) &&
  83. !ctx.Repo.CanRead(unit.TypeExternalTracker) {
  84. ctx.NotFound(nil)
  85. return
  86. }
  87. unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
  88. if err == nil {
  89. ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
  90. return
  91. }
  92. }
  93. // MustAllowPulls check if repository enable pull requests and user have right to do that
  94. func MustAllowPulls(ctx *context.Context) {
  95. if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
  96. ctx.NotFound(nil)
  97. return
  98. }
  99. // User can send pull request if owns a forked repository.
  100. if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
  101. ctx.Repo.PullRequest.Allowed = true
  102. }
  103. }
  104. func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) (open, closed []*project_model.Project) {
  105. // Distinguish whether the owner of the repository
  106. // is an individual or an organization
  107. repoOwnerType := project_model.TypeIndividual
  108. if repo.Owner.IsOrganization() {
  109. repoOwnerType = project_model.TypeOrganization
  110. }
  111. projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
  112. var openProjects []*project_model.Project
  113. var closedProjects []*project_model.Project
  114. var err error
  115. if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
  116. openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
  117. ListOptions: db.ListOptionsAll,
  118. RepoID: repo.ID,
  119. IsClosed: optional.Some(false),
  120. Type: project_model.TypeRepository,
  121. })
  122. if err != nil {
  123. ctx.ServerError("GetProjects", err)
  124. return nil, nil
  125. }
  126. closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
  127. ListOptions: db.ListOptionsAll,
  128. RepoID: repo.ID,
  129. IsClosed: optional.Some(true),
  130. Type: project_model.TypeRepository,
  131. })
  132. if err != nil {
  133. ctx.ServerError("GetProjects", err)
  134. return nil, nil
  135. }
  136. }
  137. if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
  138. openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
  139. ListOptions: db.ListOptionsAll,
  140. OwnerID: repo.OwnerID,
  141. IsClosed: optional.Some(false),
  142. Type: repoOwnerType,
  143. })
  144. if err != nil {
  145. ctx.ServerError("GetProjects", err)
  146. return nil, nil
  147. }
  148. openProjects = append(openProjects, openProjects2...)
  149. closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
  150. ListOptions: db.ListOptionsAll,
  151. OwnerID: repo.OwnerID,
  152. IsClosed: optional.Some(true),
  153. Type: repoOwnerType,
  154. })
  155. if err != nil {
  156. ctx.ServerError("GetProjects", err)
  157. return nil, nil
  158. }
  159. closedProjects = append(closedProjects, closedProjects2...)
  160. }
  161. return openProjects, closedProjects
  162. }
  163. // GetActionIssue will return the issue which is used in the context.
  164. func GetActionIssue(ctx *context.Context) *issues_model.Issue {
  165. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
  166. if err != nil {
  167. ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
  168. return nil
  169. }
  170. issue.Repo = ctx.Repo.Repository
  171. checkIssueRights(ctx, issue)
  172. if ctx.Written() {
  173. return nil
  174. }
  175. if err = issue.LoadAttributes(ctx); err != nil {
  176. ctx.ServerError("LoadAttributes", err)
  177. return nil
  178. }
  179. return issue
  180. }
  181. func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
  182. if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
  183. !issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
  184. ctx.NotFound(nil)
  185. }
  186. }
  187. func getActionIssues(ctx *context.Context) issues_model.IssueList {
  188. commaSeparatedIssueIDs := ctx.FormString("issue_ids")
  189. if len(commaSeparatedIssueIDs) == 0 {
  190. return nil
  191. }
  192. issueIDs := make([]int64, 0, 10)
  193. for stringIssueID := range strings.SplitSeq(commaSeparatedIssueIDs, ",") {
  194. issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
  195. if err != nil {
  196. ctx.ServerError("ParseInt", err)
  197. return nil
  198. }
  199. issueIDs = append(issueIDs, issueID)
  200. }
  201. issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
  202. if err != nil {
  203. ctx.ServerError("GetIssuesByIDs", err)
  204. return nil
  205. }
  206. // Check access rights for all issues
  207. issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
  208. prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
  209. for _, issue := range issues {
  210. if issue.RepoID != ctx.Repo.Repository.ID {
  211. ctx.NotFound(errors.New("some issue's RepoID is incorrect"))
  212. return nil
  213. }
  214. if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
  215. ctx.NotFound(nil)
  216. return nil
  217. }
  218. if err = issue.LoadAttributes(ctx); err != nil {
  219. ctx.ServerError("LoadAttributes", err)
  220. return nil
  221. }
  222. }
  223. return issues
  224. }
  225. // GetIssueInfo get an issue of a repository
  226. func GetIssueInfo(ctx *context.Context) {
  227. issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
  228. if err != nil {
  229. if issues_model.IsErrIssueNotExist(err) {
  230. ctx.HTTPError(http.StatusNotFound)
  231. } else {
  232. ctx.HTTPError(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
  233. }
  234. return
  235. }
  236. if issue.IsPull {
  237. // Need to check if Pulls are enabled and we can read Pulls
  238. if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
  239. ctx.HTTPError(http.StatusNotFound)
  240. return
  241. }
  242. } else {
  243. // Need to check if Issues are enabled and we can read Issues
  244. if !ctx.Repo.CanRead(unit.TypeIssues) {
  245. ctx.HTTPError(http.StatusNotFound)
  246. return
  247. }
  248. }
  249. ctx.JSON(http.StatusOK, map[string]any{
  250. "convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue),
  251. "renderedLabels": templates.NewRenderUtils(ctx).RenderLabels(issue.Labels, ctx.Repo.RepoLink, issue),
  252. })
  253. }
  254. // UpdateIssueTitle change issue's title
  255. func UpdateIssueTitle(ctx *context.Context) {
  256. issue := GetActionIssue(ctx)
  257. if ctx.Written() {
  258. return
  259. }
  260. if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  261. ctx.HTTPError(http.StatusForbidden)
  262. return
  263. }
  264. title := ctx.FormTrim("title")
  265. if len(title) == 0 {
  266. ctx.HTTPError(http.StatusNoContent)
  267. return
  268. }
  269. if err := issue_service.ChangeTitle(ctx, issue, ctx.Doer, title); err != nil {
  270. ctx.ServerError("ChangeTitle", err)
  271. return
  272. }
  273. ctx.JSON(http.StatusOK, map[string]any{
  274. "title": issue.Title,
  275. })
  276. }
  277. // UpdateIssueRef change issue's ref (branch)
  278. func UpdateIssueRef(ctx *context.Context) {
  279. issue := GetActionIssue(ctx)
  280. if ctx.Written() {
  281. return
  282. }
  283. if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
  284. ctx.HTTPError(http.StatusForbidden)
  285. return
  286. }
  287. ref := ctx.FormTrim("ref")
  288. if err := issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, ref); err != nil {
  289. ctx.ServerError("ChangeRef", err)
  290. return
  291. }
  292. ctx.JSON(http.StatusOK, map[string]any{
  293. "ref": ref,
  294. })
  295. }
  296. // UpdateIssueContent change issue's content
  297. func UpdateIssueContent(ctx *context.Context) {
  298. issue := GetActionIssue(ctx)
  299. if ctx.Written() {
  300. return
  301. }
  302. if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  303. ctx.HTTPError(http.StatusForbidden)
  304. return
  305. }
  306. if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content"), ctx.FormInt("content_version")); err != nil {
  307. if errors.Is(err, user_model.ErrBlockedUser) {
  308. ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
  309. } else if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
  310. if issue.IsPull {
  311. ctx.JSONError(ctx.Tr("repo.pulls.edit.already_changed"))
  312. } else {
  313. ctx.JSONError(ctx.Tr("repo.issues.edit.already_changed"))
  314. }
  315. } else {
  316. ctx.ServerError("ChangeContent", err)
  317. }
  318. return
  319. }
  320. // when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
  321. if !ctx.FormBool("ignore_attachments") {
  322. if err := updateAttachments(ctx, issue, ctx.FormStrings("files[]")); err != nil {
  323. ctx.ServerError("UpdateAttachments", err)
  324. return
  325. }
  326. }
  327. rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
  328. FootnoteContextID: "0",
  329. })
  330. content, err := markdown.RenderString(rctx, issue.Content)
  331. if err != nil {
  332. ctx.ServerError("RenderString", err)
  333. return
  334. }
  335. ctx.JSON(http.StatusOK, map[string]any{
  336. "content": content,
  337. "contentVersion": issue.ContentVersion,
  338. "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
  339. })
  340. }
  341. // UpdateIssueDeadline updates an issue deadline
  342. func UpdateIssueDeadline(ctx *context.Context) {
  343. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
  344. if err != nil {
  345. if issues_model.IsErrIssueNotExist(err) {
  346. ctx.NotFound(err)
  347. } else {
  348. ctx.HTTPError(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
  349. }
  350. return
  351. }
  352. if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  353. ctx.HTTPError(http.StatusForbidden, "", "Not repo writer")
  354. return
  355. }
  356. deadlineUnix, _ := common.ParseDeadlineDateToEndOfDay(ctx.FormString("deadline"))
  357. if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
  358. ctx.HTTPError(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
  359. return
  360. }
  361. ctx.JSONRedirect("")
  362. }
  363. // UpdateIssueMilestone change issue's milestone
  364. func UpdateIssueMilestone(ctx *context.Context) {
  365. issues := getActionIssues(ctx)
  366. if ctx.Written() {
  367. return
  368. }
  369. milestoneID := ctx.FormInt64("id")
  370. for _, issue := range issues {
  371. oldMilestoneID := issue.MilestoneID
  372. if oldMilestoneID == milestoneID {
  373. continue
  374. }
  375. issue.MilestoneID = milestoneID
  376. if milestoneID > 0 {
  377. var err error
  378. issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
  379. if err != nil {
  380. ctx.ServerError("GetMilestoneByRepoID", err)
  381. return
  382. }
  383. } else {
  384. issue.Milestone = nil
  385. }
  386. if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
  387. ctx.ServerError("ChangeMilestoneAssign", err)
  388. return
  389. }
  390. }
  391. ctx.JSONOK()
  392. }
  393. // UpdateIssueAssignee change issue's or pull's assignee
  394. func UpdateIssueAssignee(ctx *context.Context) {
  395. issues := getActionIssues(ctx)
  396. if ctx.Written() {
  397. return
  398. }
  399. assigneeID := ctx.FormInt64("id")
  400. action := ctx.FormString("action")
  401. for _, issue := range issues {
  402. switch action {
  403. case "clear":
  404. if err := issue_service.DeleteNotPassedAssignee(ctx, issue, ctx.Doer, []*user_model.User{}); err != nil {
  405. ctx.ServerError("ClearAssignees", err)
  406. return
  407. }
  408. default:
  409. assignee, err := user_model.GetUserByID(ctx, assigneeID)
  410. if err != nil {
  411. ctx.ServerError("GetUserByID", err)
  412. return
  413. }
  414. valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
  415. if err != nil {
  416. ctx.ServerError("canBeAssigned", err)
  417. return
  418. }
  419. if !valid {
  420. ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
  421. return
  422. }
  423. _, _, err = issue_service.ToggleAssigneeWithNotify(ctx, issue, ctx.Doer, assigneeID)
  424. if err != nil {
  425. ctx.ServerError("ToggleAssignee", err)
  426. return
  427. }
  428. }
  429. }
  430. ctx.JSONOK()
  431. }
  432. // ChangeIssueReaction create a reaction for issue
  433. func ChangeIssueReaction(ctx *context.Context) {
  434. form := web.GetForm(ctx).(*forms.ReactionForm)
  435. issue := GetActionIssue(ctx)
  436. if ctx.Written() {
  437. return
  438. }
  439. if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
  440. if log.IsTrace() {
  441. if ctx.IsSigned {
  442. issueType := "issues"
  443. if issue.IsPull {
  444. issueType = "pulls"
  445. }
  446. log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
  447. "User in Repo has Permissions: %-+v",
  448. ctx.Doer,
  449. issue.PosterID,
  450. issueType,
  451. ctx.Repo.Repository,
  452. ctx.Repo.Permission)
  453. } else {
  454. log.Trace("Permission Denied: Not logged in")
  455. }
  456. }
  457. ctx.HTTPError(http.StatusForbidden)
  458. return
  459. }
  460. if ctx.HasError() {
  461. ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
  462. return
  463. }
  464. switch ctx.PathParam("action") {
  465. case "react":
  466. reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
  467. if err != nil {
  468. if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
  469. ctx.ServerError("ChangeIssueReaction", err)
  470. return
  471. }
  472. log.Info("CreateIssueReaction: %s", err)
  473. break
  474. }
  475. // Reload new reactions
  476. issue.Reactions = nil
  477. if err = issue.LoadAttributes(ctx); err != nil {
  478. log.Info("issue.LoadAttributes: %s", err)
  479. break
  480. }
  481. log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
  482. case "unreact":
  483. if err := issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content); err != nil {
  484. ctx.ServerError("DeleteIssueReaction", err)
  485. return
  486. }
  487. // Reload new reactions
  488. issue.Reactions = nil
  489. if err := issue.LoadAttributes(ctx); err != nil {
  490. log.Info("issue.LoadAttributes: %s", err)
  491. break
  492. }
  493. log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
  494. default:
  495. ctx.NotFound(nil)
  496. return
  497. }
  498. if len(issue.Reactions) == 0 {
  499. ctx.JSON(http.StatusOK, map[string]any{
  500. "empty": true,
  501. "html": "",
  502. })
  503. return
  504. }
  505. html, err := ctx.RenderToHTML(tplReactions, map[string]any{
  506. "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
  507. "Reactions": issue.Reactions.GroupByType(),
  508. })
  509. if err != nil {
  510. ctx.ServerError("ChangeIssueReaction.HTMLString", err)
  511. return
  512. }
  513. ctx.JSON(http.StatusOK, map[string]any{
  514. "html": html,
  515. })
  516. }
  517. // GetIssueAttachments returns attachments for the issue
  518. func GetIssueAttachments(ctx *context.Context) {
  519. issue := GetActionIssue(ctx)
  520. if ctx.Written() {
  521. return
  522. }
  523. attachments := make([]*api.Attachment, len(issue.Attachments))
  524. for i := 0; i < len(issue.Attachments); i++ {
  525. attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i])
  526. }
  527. ctx.JSON(http.StatusOK, attachments)
  528. }
  529. func updateAttachments(ctx *context.Context, item any, files []string) error {
  530. var attachments []*repo_model.Attachment
  531. switch content := item.(type) {
  532. case *issues_model.Issue:
  533. attachments = content.Attachments
  534. case *issues_model.Comment:
  535. attachments = content.Attachments
  536. default:
  537. return fmt.Errorf("unknown Type: %T", content)
  538. }
  539. for i := 0; i < len(attachments); i++ {
  540. if util.SliceContainsString(files, attachments[i].UUID) {
  541. continue
  542. }
  543. if err := repo_model.DeleteAttachment(ctx, attachments[i], true); err != nil {
  544. return err
  545. }
  546. }
  547. var err error
  548. if len(files) > 0 {
  549. switch content := item.(type) {
  550. case *issues_model.Issue:
  551. err = issues_model.UpdateIssueAttachments(ctx, content.ID, files)
  552. case *issues_model.Comment:
  553. err = issues_model.UpdateCommentAttachments(ctx, content, files)
  554. default:
  555. return fmt.Errorf("unknown Type: %T", content)
  556. }
  557. if err != nil {
  558. return err
  559. }
  560. }
  561. switch content := item.(type) {
  562. case *issues_model.Issue:
  563. content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID)
  564. case *issues_model.Comment:
  565. content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID)
  566. default:
  567. return fmt.Errorf("unknown Type: %T", content)
  568. }
  569. return err
  570. }
  571. func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
  572. attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
  573. "ctxData": ctx.Data,
  574. "Attachments": attachments,
  575. "Content": content,
  576. })
  577. if err != nil {
  578. ctx.ServerError("attachmentsHTML.HTMLString", err)
  579. return ""
  580. }
  581. return attachHTML
  582. }
  583. // handleMentionableAssigneesAndTeams gets all teams that current user can mention, and fills the assignee users to the context data
  584. func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_model.User) {
  585. // TODO: need to figure out how many places this is really used, and rename it to "MentionableAssignees"
  586. // at the moment it is used on the issue list page, for the markdown editor mention
  587. ctx.Data["Assignees"] = assignees
  588. if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
  589. return
  590. }
  591. var isAdmin bool
  592. var err error
  593. var teams []*organization.Team
  594. org := organization.OrgFromUser(ctx.Repo.Owner)
  595. // Admin has super access.
  596. if ctx.Doer.IsAdmin {
  597. isAdmin = true
  598. } else {
  599. isAdmin, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
  600. if err != nil {
  601. ctx.ServerError("IsOwnedBy", err)
  602. return
  603. }
  604. }
  605. if isAdmin {
  606. teams, err = org.LoadTeams(ctx)
  607. if err != nil {
  608. ctx.ServerError("LoadTeams", err)
  609. return
  610. }
  611. } else {
  612. teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
  613. if err != nil {
  614. ctx.ServerError("GetUserTeams", err)
  615. return
  616. }
  617. }
  618. ctx.Data["MentionableTeams"] = teams
  619. ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
  620. ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx)
  621. }