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.

issue_comment.go 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2020 The Gitea Authors.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "errors"
  8. "net/http"
  9. "code.gitea.io/gitea/models"
  10. "code.gitea.io/gitea/modules/context"
  11. "code.gitea.io/gitea/modules/convert"
  12. api "code.gitea.io/gitea/modules/structs"
  13. "code.gitea.io/gitea/modules/web"
  14. "code.gitea.io/gitea/routers/api/v1/utils"
  15. comment_service "code.gitea.io/gitea/services/comments"
  16. )
  17. // ListIssueComments list all the comments of an issue
  18. func ListIssueComments(ctx *context.APIContext) {
  19. // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/comments issue issueGetComments
  20. // ---
  21. // summary: List all comments on an issue
  22. // produces:
  23. // - application/json
  24. // parameters:
  25. // - name: owner
  26. // in: path
  27. // description: owner of the repo
  28. // type: string
  29. // required: true
  30. // - name: repo
  31. // in: path
  32. // description: name of the repo
  33. // type: string
  34. // required: true
  35. // - name: index
  36. // in: path
  37. // description: index of the issue
  38. // type: integer
  39. // format: int64
  40. // required: true
  41. // - name: since
  42. // in: query
  43. // description: if provided, only comments updated since the specified time are returned.
  44. // type: string
  45. // format: date-time
  46. // - name: before
  47. // in: query
  48. // description: if provided, only comments updated before the provided time are returned.
  49. // type: string
  50. // format: date-time
  51. // responses:
  52. // "200":
  53. // "$ref": "#/responses/CommentList"
  54. before, since, err := utils.GetQueryBeforeSince(ctx)
  55. if err != nil {
  56. ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
  57. return
  58. }
  59. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  60. if err != nil {
  61. ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
  62. return
  63. }
  64. issue.Repo = ctx.Repo.Repository
  65. opts := &models.FindCommentsOptions{
  66. IssueID: issue.ID,
  67. Since: since,
  68. Before: before,
  69. Type: models.CommentTypeComment,
  70. }
  71. comments, err := models.FindComments(opts)
  72. if err != nil {
  73. ctx.Error(http.StatusInternalServerError, "FindComments", err)
  74. return
  75. }
  76. totalCount, err := models.CountComments(opts)
  77. if err != nil {
  78. ctx.InternalServerError(err)
  79. return
  80. }
  81. if err := models.CommentList(comments).LoadPosters(); err != nil {
  82. ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
  83. return
  84. }
  85. apiComments := make([]*api.Comment, len(comments))
  86. for i, comment := range comments {
  87. comment.Issue = issue
  88. apiComments[i] = convert.ToComment(comments[i])
  89. }
  90. ctx.SetTotalCountHeader(totalCount)
  91. ctx.JSON(http.StatusOK, &apiComments)
  92. }
  93. // ListRepoIssueComments returns all issue-comments for a repo
  94. func ListRepoIssueComments(ctx *context.APIContext) {
  95. // swagger:operation GET /repos/{owner}/{repo}/issues/comments issue issueGetRepoComments
  96. // ---
  97. // summary: List all comments in a repository
  98. // produces:
  99. // - application/json
  100. // parameters:
  101. // - name: owner
  102. // in: path
  103. // description: owner of the repo
  104. // type: string
  105. // required: true
  106. // - name: repo
  107. // in: path
  108. // description: name of the repo
  109. // type: string
  110. // required: true
  111. // - name: since
  112. // in: query
  113. // description: if provided, only comments updated since the provided time are returned.
  114. // type: string
  115. // format: date-time
  116. // - name: before
  117. // in: query
  118. // description: if provided, only comments updated before the provided time are returned.
  119. // type: string
  120. // format: date-time
  121. // - name: page
  122. // in: query
  123. // description: page number of results to return (1-based)
  124. // type: integer
  125. // - name: limit
  126. // in: query
  127. // description: page size of results
  128. // type: integer
  129. // responses:
  130. // "200":
  131. // "$ref": "#/responses/CommentList"
  132. before, since, err := utils.GetQueryBeforeSince(ctx)
  133. if err != nil {
  134. ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
  135. return
  136. }
  137. opts := &models.FindCommentsOptions{
  138. ListOptions: utils.GetListOptions(ctx),
  139. RepoID: ctx.Repo.Repository.ID,
  140. Type: models.CommentTypeComment,
  141. Since: since,
  142. Before: before,
  143. }
  144. comments, err := models.FindComments(opts)
  145. if err != nil {
  146. ctx.Error(http.StatusInternalServerError, "FindComments", err)
  147. return
  148. }
  149. totalCount, err := models.CountComments(opts)
  150. if err != nil {
  151. ctx.InternalServerError(err)
  152. return
  153. }
  154. if err = models.CommentList(comments).LoadPosters(); err != nil {
  155. ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
  156. return
  157. }
  158. apiComments := make([]*api.Comment, len(comments))
  159. if err := models.CommentList(comments).LoadIssues(); err != nil {
  160. ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
  161. return
  162. }
  163. if err := models.CommentList(comments).LoadPosters(); err != nil {
  164. ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
  165. return
  166. }
  167. if _, err := models.CommentList(comments).Issues().LoadRepositories(); err != nil {
  168. ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
  169. return
  170. }
  171. for i := range comments {
  172. apiComments[i] = convert.ToComment(comments[i])
  173. }
  174. ctx.SetTotalCountHeader(totalCount)
  175. ctx.JSON(http.StatusOK, &apiComments)
  176. }
  177. // CreateIssueComment create a comment for an issue
  178. func CreateIssueComment(ctx *context.APIContext) {
  179. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/comments issue issueCreateComment
  180. // ---
  181. // summary: Add a comment to an issue
  182. // consumes:
  183. // - application/json
  184. // produces:
  185. // - application/json
  186. // parameters:
  187. // - name: owner
  188. // in: path
  189. // description: owner of the repo
  190. // type: string
  191. // required: true
  192. // - name: repo
  193. // in: path
  194. // description: name of the repo
  195. // type: string
  196. // required: true
  197. // - name: index
  198. // in: path
  199. // description: index of the issue
  200. // type: integer
  201. // format: int64
  202. // required: true
  203. // - name: body
  204. // in: body
  205. // schema:
  206. // "$ref": "#/definitions/CreateIssueCommentOption"
  207. // responses:
  208. // "201":
  209. // "$ref": "#/responses/Comment"
  210. // "403":
  211. // "$ref": "#/responses/forbidden"
  212. form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
  213. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  214. if err != nil {
  215. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  216. return
  217. }
  218. if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
  219. ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
  220. return
  221. }
  222. comment, err := comment_service.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Body, nil)
  223. if err != nil {
  224. ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
  225. return
  226. }
  227. ctx.JSON(http.StatusCreated, convert.ToComment(comment))
  228. }
  229. // GetIssueComment Get a comment by ID
  230. func GetIssueComment(ctx *context.APIContext) {
  231. // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id} issue issueGetComment
  232. // ---
  233. // summary: Get a comment
  234. // consumes:
  235. // - application/json
  236. // produces:
  237. // - application/json
  238. // parameters:
  239. // - name: owner
  240. // in: path
  241. // description: owner of the repo
  242. // type: string
  243. // required: true
  244. // - name: repo
  245. // in: path
  246. // description: name of the repo
  247. // type: string
  248. // required: true
  249. // - name: id
  250. // in: path
  251. // description: id of the comment
  252. // type: integer
  253. // format: int64
  254. // required: true
  255. // responses:
  256. // "200":
  257. // "$ref": "#/responses/Comment"
  258. // "204":
  259. // "$ref": "#/responses/empty"
  260. // "403":
  261. // "$ref": "#/responses/forbidden"
  262. // "404":
  263. // "$ref": "#/responses/notFound"
  264. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  265. if err != nil {
  266. if models.IsErrCommentNotExist(err) {
  267. ctx.NotFound(err)
  268. } else {
  269. ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
  270. }
  271. return
  272. }
  273. if err = comment.LoadIssue(); err != nil {
  274. ctx.InternalServerError(err)
  275. return
  276. }
  277. if comment.Issue.RepoID != ctx.Repo.Repository.ID {
  278. ctx.Status(http.StatusNotFound)
  279. return
  280. }
  281. if comment.Type != models.CommentTypeComment {
  282. ctx.Status(http.StatusNoContent)
  283. return
  284. }
  285. if err := comment.LoadPoster(); err != nil {
  286. ctx.Error(http.StatusInternalServerError, "comment.LoadPoster", err)
  287. return
  288. }
  289. ctx.JSON(http.StatusOK, convert.ToComment(comment))
  290. }
  291. // EditIssueComment modify a comment of an issue
  292. func EditIssueComment(ctx *context.APIContext) {
  293. // swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id} issue issueEditComment
  294. // ---
  295. // summary: Edit a comment
  296. // consumes:
  297. // - application/json
  298. // produces:
  299. // - application/json
  300. // parameters:
  301. // - name: owner
  302. // in: path
  303. // description: owner of the repo
  304. // type: string
  305. // required: true
  306. // - name: repo
  307. // in: path
  308. // description: name of the repo
  309. // type: string
  310. // required: true
  311. // - name: id
  312. // in: path
  313. // description: id of the comment to edit
  314. // type: integer
  315. // format: int64
  316. // required: true
  317. // - name: body
  318. // in: body
  319. // schema:
  320. // "$ref": "#/definitions/EditIssueCommentOption"
  321. // responses:
  322. // "200":
  323. // "$ref": "#/responses/Comment"
  324. // "204":
  325. // "$ref": "#/responses/empty"
  326. // "403":
  327. // "$ref": "#/responses/forbidden"
  328. // "404":
  329. // "$ref": "#/responses/notFound"
  330. form := web.GetForm(ctx).(*api.EditIssueCommentOption)
  331. editIssueComment(ctx, *form)
  332. }
  333. // EditIssueCommentDeprecated modify a comment of an issue
  334. func EditIssueCommentDeprecated(ctx *context.APIContext) {
  335. // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueEditCommentDeprecated
  336. // ---
  337. // summary: Edit a comment
  338. // deprecated: true
  339. // consumes:
  340. // - application/json
  341. // produces:
  342. // - application/json
  343. // parameters:
  344. // - name: owner
  345. // in: path
  346. // description: owner of the repo
  347. // type: string
  348. // required: true
  349. // - name: repo
  350. // in: path
  351. // description: name of the repo
  352. // type: string
  353. // required: true
  354. // - name: index
  355. // in: path
  356. // description: this parameter is ignored
  357. // type: integer
  358. // required: true
  359. // - name: id
  360. // in: path
  361. // description: id of the comment to edit
  362. // type: integer
  363. // format: int64
  364. // required: true
  365. // - name: body
  366. // in: body
  367. // schema:
  368. // "$ref": "#/definitions/EditIssueCommentOption"
  369. // responses:
  370. // "200":
  371. // "$ref": "#/responses/Comment"
  372. // "204":
  373. // "$ref": "#/responses/empty"
  374. // "403":
  375. // "$ref": "#/responses/forbidden"
  376. // "404":
  377. // "$ref": "#/responses/notFound"
  378. form := web.GetForm(ctx).(*api.EditIssueCommentOption)
  379. editIssueComment(ctx, *form)
  380. }
  381. func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
  382. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  383. if err != nil {
  384. if models.IsErrCommentNotExist(err) {
  385. ctx.NotFound(err)
  386. } else {
  387. ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
  388. }
  389. return
  390. }
  391. if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
  392. ctx.Status(http.StatusForbidden)
  393. return
  394. } else if comment.Type != models.CommentTypeComment {
  395. ctx.Status(http.StatusNoContent)
  396. return
  397. }
  398. oldContent := comment.Content
  399. comment.Content = form.Body
  400. if err := comment_service.UpdateComment(comment, ctx.User, oldContent); err != nil {
  401. ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
  402. return
  403. }
  404. ctx.JSON(http.StatusOK, convert.ToComment(comment))
  405. }
  406. // DeleteIssueComment delete a comment from an issue
  407. func DeleteIssueComment(ctx *context.APIContext) {
  408. // swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id} issue issueDeleteComment
  409. // ---
  410. // summary: Delete a comment
  411. // parameters:
  412. // - name: owner
  413. // in: path
  414. // description: owner of the repo
  415. // type: string
  416. // required: true
  417. // - name: repo
  418. // in: path
  419. // description: name of the repo
  420. // type: string
  421. // required: true
  422. // - name: id
  423. // in: path
  424. // description: id of comment to delete
  425. // type: integer
  426. // format: int64
  427. // required: true
  428. // responses:
  429. // "204":
  430. // "$ref": "#/responses/empty"
  431. // "403":
  432. // "$ref": "#/responses/forbidden"
  433. // "404":
  434. // "$ref": "#/responses/notFound"
  435. deleteIssueComment(ctx)
  436. }
  437. // DeleteIssueCommentDeprecated delete a comment from an issue
  438. func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
  439. // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueDeleteCommentDeprecated
  440. // ---
  441. // summary: Delete a comment
  442. // deprecated: true
  443. // parameters:
  444. // - name: owner
  445. // in: path
  446. // description: owner of the repo
  447. // type: string
  448. // required: true
  449. // - name: repo
  450. // in: path
  451. // description: name of the repo
  452. // type: string
  453. // required: true
  454. // - name: index
  455. // in: path
  456. // description: this parameter is ignored
  457. // type: integer
  458. // required: true
  459. // - name: id
  460. // in: path
  461. // description: id of comment to delete
  462. // type: integer
  463. // format: int64
  464. // required: true
  465. // responses:
  466. // "204":
  467. // "$ref": "#/responses/empty"
  468. // "403":
  469. // "$ref": "#/responses/forbidden"
  470. // "404":
  471. // "$ref": "#/responses/notFound"
  472. deleteIssueComment(ctx)
  473. }
  474. func deleteIssueComment(ctx *context.APIContext) {
  475. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  476. if err != nil {
  477. if models.IsErrCommentNotExist(err) {
  478. ctx.NotFound(err)
  479. } else {
  480. ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
  481. }
  482. return
  483. }
  484. if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
  485. ctx.Status(http.StatusForbidden)
  486. return
  487. } else if comment.Type != models.CommentTypeComment {
  488. ctx.Status(http.StatusNoContent)
  489. return
  490. }
  491. if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
  492. ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err)
  493. return
  494. }
  495. ctx.Status(http.StatusNoContent)
  496. }