code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/issue_comment.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2020 The Gitea Authors. 3 // SPDX-License-Identifier: MIT 4 5 package repo 6 7 import ( 8 stdCtx "context" 9 "errors" 10 "net/http" 11 12 issues_model "code.gitea.io/gitea/models/issues" 13 access_model "code.gitea.io/gitea/models/perm/access" 14 repo_model "code.gitea.io/gitea/models/repo" 15 "code.gitea.io/gitea/models/unit" 16 user_model "code.gitea.io/gitea/models/user" 17 "code.gitea.io/gitea/modules/context" 18 api "code.gitea.io/gitea/modules/structs" 19 "code.gitea.io/gitea/modules/util" 20 "code.gitea.io/gitea/modules/web" 21 "code.gitea.io/gitea/routers/api/v1/utils" 22 "code.gitea.io/gitea/services/convert" 23 issue_service "code.gitea.io/gitea/services/issue" 24 ) 25 26 // ListIssueComments list all the comments of an issue 27 func ListIssueComments(ctx *context.APIContext) { 28 // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/comments issue issueGetComments 29 // --- 30 // summary: List all comments on an issue 31 // produces: 32 // - application/json 33 // parameters: 34 // - name: owner 35 // in: path 36 // description: owner of the repo 37 // type: string 38 // required: true 39 // - name: repo 40 // in: path 41 // description: name of the repo 42 // type: string 43 // required: true 44 // - name: index 45 // in: path 46 // description: index of the issue 47 // type: integer 48 // format: int64 49 // required: true 50 // - name: since 51 // in: query 52 // description: if provided, only comments updated since the specified time are returned. 53 // type: string 54 // format: date-time 55 // - name: before 56 // in: query 57 // description: if provided, only comments updated before the provided time are returned. 58 // type: string 59 // format: date-time 60 // responses: 61 // "200": 62 // "$ref": "#/responses/CommentList" 63 // "404": 64 // "$ref": "#/responses/notFound" 65 66 before, since, err := context.GetQueryBeforeSince(ctx.Base) 67 if err != nil { 68 ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) 69 return 70 } 71 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 72 if err != nil { 73 ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) 74 return 75 } 76 if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { 77 ctx.NotFound() 78 return 79 } 80 81 issue.Repo = ctx.Repo.Repository 82 83 opts := &issues_model.FindCommentsOptions{ 84 IssueID: issue.ID, 85 Since: since, 86 Before: before, 87 Type: issues_model.CommentTypeComment, 88 } 89 90 comments, err := issues_model.FindComments(ctx, opts) 91 if err != nil { 92 ctx.Error(http.StatusInternalServerError, "FindComments", err) 93 return 94 } 95 96 totalCount, err := issues_model.CountComments(ctx, opts) 97 if err != nil { 98 ctx.InternalServerError(err) 99 return 100 } 101 102 if err := comments.LoadPosters(ctx); err != nil { 103 ctx.Error(http.StatusInternalServerError, "LoadPosters", err) 104 return 105 } 106 107 if err := comments.LoadAttachments(ctx); err != nil { 108 ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) 109 return 110 } 111 112 apiComments := make([]*api.Comment, len(comments)) 113 for i, comment := range comments { 114 comment.Issue = issue 115 apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, comments[i]) 116 } 117 118 ctx.SetTotalCountHeader(totalCount) 119 ctx.JSON(http.StatusOK, &apiComments) 120 } 121 122 // ListIssueCommentsAndTimeline list all the comments and events of an issue 123 func ListIssueCommentsAndTimeline(ctx *context.APIContext) { 124 // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/timeline issue issueGetCommentsAndTimeline 125 // --- 126 // summary: List all comments and events on an issue 127 // produces: 128 // - application/json 129 // parameters: 130 // - name: owner 131 // in: path 132 // description: owner of the repo 133 // type: string 134 // required: true 135 // - name: repo 136 // in: path 137 // description: name of the repo 138 // type: string 139 // required: true 140 // - name: index 141 // in: path 142 // description: index of the issue 143 // type: integer 144 // format: int64 145 // required: true 146 // - name: since 147 // in: query 148 // description: if provided, only comments updated since the specified time are returned. 149 // type: string 150 // format: date-time 151 // - name: page 152 // in: query 153 // description: page number of results to return (1-based) 154 // type: integer 155 // - name: limit 156 // in: query 157 // description: page size of results 158 // type: integer 159 // - name: before 160 // in: query 161 // description: if provided, only comments updated before the provided time are returned. 162 // type: string 163 // format: date-time 164 // responses: 165 // "200": 166 // "$ref": "#/responses/TimelineList" 167 // "404": 168 // "$ref": "#/responses/notFound" 169 170 before, since, err := context.GetQueryBeforeSince(ctx.Base) 171 if err != nil { 172 ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) 173 return 174 } 175 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 176 if err != nil { 177 ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) 178 return 179 } 180 issue.Repo = ctx.Repo.Repository 181 182 opts := &issues_model.FindCommentsOptions{ 183 ListOptions: utils.GetListOptions(ctx), 184 IssueID: issue.ID, 185 Since: since, 186 Before: before, 187 Type: issues_model.CommentTypeUndefined, 188 } 189 190 comments, err := issues_model.FindComments(ctx, opts) 191 if err != nil { 192 ctx.Error(http.StatusInternalServerError, "FindComments", err) 193 return 194 } 195 196 if err := comments.LoadPosters(ctx); err != nil { 197 ctx.Error(http.StatusInternalServerError, "LoadPosters", err) 198 return 199 } 200 201 var apiComments []*api.TimelineComment 202 for _, comment := range comments { 203 if comment.Type != issues_model.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) { 204 comment.Issue = issue 205 apiComments = append(apiComments, convert.ToTimelineComment(ctx, issue.Repo, comment, ctx.Doer)) 206 } 207 } 208 209 ctx.SetTotalCountHeader(int64(len(apiComments))) 210 ctx.JSON(http.StatusOK, &apiComments) 211 } 212 213 func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issues_model.Comment, issueRepoID int64) bool { 214 // Remove comments that the user has no permissions to see 215 if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 { 216 var err error 217 // Set RefRepo for description in template 218 c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID) 219 if err != nil { 220 return false 221 } 222 perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, user) 223 if err != nil { 224 return false 225 } 226 if !perm.CanReadIssuesOrPulls(c.RefIsPull) { 227 return false 228 } 229 } 230 return true 231 } 232 233 // ListRepoIssueComments returns all issue-comments for a repo 234 func ListRepoIssueComments(ctx *context.APIContext) { 235 // swagger:operation GET /repos/{owner}/{repo}/issues/comments issue issueGetRepoComments 236 // --- 237 // summary: List all comments in a repository 238 // produces: 239 // - application/json 240 // parameters: 241 // - name: owner 242 // in: path 243 // description: owner of the repo 244 // type: string 245 // required: true 246 // - name: repo 247 // in: path 248 // description: name of the repo 249 // type: string 250 // required: true 251 // - name: since 252 // in: query 253 // description: if provided, only comments updated since the provided time are returned. 254 // type: string 255 // format: date-time 256 // - name: before 257 // in: query 258 // description: if provided, only comments updated before the provided time are returned. 259 // type: string 260 // format: date-time 261 // - name: page 262 // in: query 263 // description: page number of results to return (1-based) 264 // type: integer 265 // - name: limit 266 // in: query 267 // description: page size of results 268 // type: integer 269 // responses: 270 // "200": 271 // "$ref": "#/responses/CommentList" 272 // "404": 273 // "$ref": "#/responses/notFound" 274 275 before, since, err := context.GetQueryBeforeSince(ctx.Base) 276 if err != nil { 277 ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) 278 return 279 } 280 281 var isPull util.OptionalBool 282 canReadIssue := ctx.Repo.CanRead(unit.TypeIssues) 283 canReadPull := ctx.Repo.CanRead(unit.TypePullRequests) 284 if canReadIssue && canReadPull { 285 isPull = util.OptionalBoolNone 286 } else if canReadIssue { 287 isPull = util.OptionalBoolFalse 288 } else if canReadPull { 289 isPull = util.OptionalBoolTrue 290 } else { 291 ctx.NotFound() 292 return 293 } 294 295 opts := &issues_model.FindCommentsOptions{ 296 ListOptions: utils.GetListOptions(ctx), 297 RepoID: ctx.Repo.Repository.ID, 298 Type: issues_model.CommentTypeComment, 299 Since: since, 300 Before: before, 301 IsPull: isPull, 302 } 303 304 comments, err := issues_model.FindComments(ctx, opts) 305 if err != nil { 306 ctx.Error(http.StatusInternalServerError, "FindComments", err) 307 return 308 } 309 310 totalCount, err := issues_model.CountComments(ctx, opts) 311 if err != nil { 312 ctx.InternalServerError(err) 313 return 314 } 315 316 if err = comments.LoadPosters(ctx); err != nil { 317 ctx.Error(http.StatusInternalServerError, "LoadPosters", err) 318 return 319 } 320 321 apiComments := make([]*api.Comment, len(comments)) 322 if err := comments.LoadIssues(ctx); err != nil { 323 ctx.Error(http.StatusInternalServerError, "LoadIssues", err) 324 return 325 } 326 if err := comments.LoadPosters(ctx); err != nil { 327 ctx.Error(http.StatusInternalServerError, "LoadPosters", err) 328 return 329 } 330 if err := comments.LoadAttachments(ctx); err != nil { 331 ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) 332 return 333 } 334 if _, err := comments.Issues().LoadRepositories(ctx); err != nil { 335 ctx.Error(http.StatusInternalServerError, "LoadRepositories", err) 336 return 337 } 338 for i := range comments { 339 apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, comments[i]) 340 } 341 342 ctx.SetTotalCountHeader(totalCount) 343 ctx.JSON(http.StatusOK, &apiComments) 344 } 345 346 // CreateIssueComment create a comment for an issue 347 func CreateIssueComment(ctx *context.APIContext) { 348 // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/comments issue issueCreateComment 349 // --- 350 // summary: Add a comment to an issue 351 // consumes: 352 // - application/json 353 // produces: 354 // - application/json 355 // parameters: 356 // - name: owner 357 // in: path 358 // description: owner of the repo 359 // type: string 360 // required: true 361 // - name: repo 362 // in: path 363 // description: name of the repo 364 // type: string 365 // required: true 366 // - name: index 367 // in: path 368 // description: index of the issue 369 // type: integer 370 // format: int64 371 // required: true 372 // - name: body 373 // in: body 374 // schema: 375 // "$ref": "#/definitions/CreateIssueCommentOption" 376 // responses: 377 // "201": 378 // "$ref": "#/responses/Comment" 379 // "403": 380 // "$ref": "#/responses/forbidden" 381 // "404": 382 // "$ref": "#/responses/notFound" 383 form := web.GetForm(ctx).(*api.CreateIssueCommentOption) 384 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 385 if err != nil { 386 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 387 return 388 } 389 390 if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { 391 ctx.NotFound() 392 return 393 } 394 395 if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { 396 ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked"))) 397 return 398 } 399 400 comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) 401 if err != nil { 402 ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) 403 return 404 } 405 406 ctx.JSON(http.StatusCreated, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) 407 } 408 409 // GetIssueComment Get a comment by ID 410 func GetIssueComment(ctx *context.APIContext) { 411 // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id} issue issueGetComment 412 // --- 413 // summary: Get a comment 414 // consumes: 415 // - application/json 416 // produces: 417 // - application/json 418 // parameters: 419 // - name: owner 420 // in: path 421 // description: owner of the repo 422 // type: string 423 // required: true 424 // - name: repo 425 // in: path 426 // description: name of the repo 427 // type: string 428 // required: true 429 // - name: id 430 // in: path 431 // description: id of the comment 432 // type: integer 433 // format: int64 434 // required: true 435 // responses: 436 // "200": 437 // "$ref": "#/responses/Comment" 438 // "204": 439 // "$ref": "#/responses/empty" 440 // "403": 441 // "$ref": "#/responses/forbidden" 442 // "404": 443 // "$ref": "#/responses/notFound" 444 445 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 446 if err != nil { 447 if issues_model.IsErrCommentNotExist(err) { 448 ctx.NotFound(err) 449 } else { 450 ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) 451 } 452 return 453 } 454 455 if err = comment.LoadIssue(ctx); err != nil { 456 ctx.InternalServerError(err) 457 return 458 } 459 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 460 ctx.Status(http.StatusNotFound) 461 return 462 } 463 464 if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { 465 ctx.NotFound() 466 return 467 } 468 469 if comment.Type != issues_model.CommentTypeComment { 470 ctx.Status(http.StatusNoContent) 471 return 472 } 473 474 if err := comment.LoadPoster(ctx); err != nil { 475 ctx.Error(http.StatusInternalServerError, "comment.LoadPoster", err) 476 return 477 } 478 479 ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) 480 } 481 482 // EditIssueComment modify a comment of an issue 483 func EditIssueComment(ctx *context.APIContext) { 484 // swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id} issue issueEditComment 485 // --- 486 // summary: Edit a comment 487 // consumes: 488 // - application/json 489 // produces: 490 // - application/json 491 // parameters: 492 // - name: owner 493 // in: path 494 // description: owner of the repo 495 // type: string 496 // required: true 497 // - name: repo 498 // in: path 499 // description: name of the repo 500 // type: string 501 // required: true 502 // - name: id 503 // in: path 504 // description: id of the comment to edit 505 // type: integer 506 // format: int64 507 // required: true 508 // - name: body 509 // in: body 510 // schema: 511 // "$ref": "#/definitions/EditIssueCommentOption" 512 // responses: 513 // "200": 514 // "$ref": "#/responses/Comment" 515 // "204": 516 // "$ref": "#/responses/empty" 517 // "403": 518 // "$ref": "#/responses/forbidden" 519 // "404": 520 // "$ref": "#/responses/notFound" 521 522 form := web.GetForm(ctx).(*api.EditIssueCommentOption) 523 editIssueComment(ctx, *form) 524 } 525 526 // EditIssueCommentDeprecated modify a comment of an issue 527 func EditIssueCommentDeprecated(ctx *context.APIContext) { 528 // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueEditCommentDeprecated 529 // --- 530 // summary: Edit a comment 531 // deprecated: true 532 // consumes: 533 // - application/json 534 // produces: 535 // - application/json 536 // parameters: 537 // - name: owner 538 // in: path 539 // description: owner of the repo 540 // type: string 541 // required: true 542 // - name: repo 543 // in: path 544 // description: name of the repo 545 // type: string 546 // required: true 547 // - name: index 548 // in: path 549 // description: this parameter is ignored 550 // type: integer 551 // required: true 552 // - name: id 553 // in: path 554 // description: id of the comment to edit 555 // type: integer 556 // format: int64 557 // required: true 558 // - name: body 559 // in: body 560 // schema: 561 // "$ref": "#/definitions/EditIssueCommentOption" 562 // responses: 563 // "200": 564 // "$ref": "#/responses/Comment" 565 // "204": 566 // "$ref": "#/responses/empty" 567 // "403": 568 // "$ref": "#/responses/forbidden" 569 // "404": 570 // "$ref": "#/responses/notFound" 571 572 form := web.GetForm(ctx).(*api.EditIssueCommentOption) 573 editIssueComment(ctx, *form) 574 } 575 576 func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { 577 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 578 if err != nil { 579 if issues_model.IsErrCommentNotExist(err) { 580 ctx.NotFound(err) 581 } else { 582 ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) 583 } 584 return 585 } 586 587 if err := comment.LoadIssue(ctx); err != nil { 588 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 589 return 590 } 591 592 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 593 ctx.Status(http.StatusNotFound) 594 return 595 } 596 597 if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { 598 ctx.Status(http.StatusForbidden) 599 return 600 } 601 602 if !comment.Type.HasContentSupport() { 603 ctx.Status(http.StatusNoContent) 604 return 605 } 606 607 oldContent := comment.Content 608 comment.Content = form.Body 609 if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { 610 ctx.Error(http.StatusInternalServerError, "UpdateComment", err) 611 return 612 } 613 614 ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) 615 } 616 617 // DeleteIssueComment delete a comment from an issue 618 func DeleteIssueComment(ctx *context.APIContext) { 619 // swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id} issue issueDeleteComment 620 // --- 621 // summary: Delete a comment 622 // parameters: 623 // - name: owner 624 // in: path 625 // description: owner of the repo 626 // type: string 627 // required: true 628 // - name: repo 629 // in: path 630 // description: name of the repo 631 // type: string 632 // required: true 633 // - name: id 634 // in: path 635 // description: id of comment to delete 636 // type: integer 637 // format: int64 638 // required: true 639 // responses: 640 // "204": 641 // "$ref": "#/responses/empty" 642 // "403": 643 // "$ref": "#/responses/forbidden" 644 // "404": 645 // "$ref": "#/responses/notFound" 646 647 deleteIssueComment(ctx) 648 } 649 650 // DeleteIssueCommentDeprecated delete a comment from an issue 651 func DeleteIssueCommentDeprecated(ctx *context.APIContext) { 652 // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueDeleteCommentDeprecated 653 // --- 654 // summary: Delete a comment 655 // deprecated: true 656 // parameters: 657 // - name: owner 658 // in: path 659 // description: owner of the repo 660 // type: string 661 // required: true 662 // - name: repo 663 // in: path 664 // description: name of the repo 665 // type: string 666 // required: true 667 // - name: index 668 // in: path 669 // description: this parameter is ignored 670 // type: integer 671 // required: true 672 // - name: id 673 // in: path 674 // description: id of comment to delete 675 // type: integer 676 // format: int64 677 // required: true 678 // responses: 679 // "204": 680 // "$ref": "#/responses/empty" 681 // "403": 682 // "$ref": "#/responses/forbidden" 683 // "404": 684 // "$ref": "#/responses/notFound" 685 686 deleteIssueComment(ctx) 687 } 688 689 func deleteIssueComment(ctx *context.APIContext) { 690 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 691 if err != nil { 692 if issues_model.IsErrCommentNotExist(err) { 693 ctx.NotFound(err) 694 } else { 695 ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) 696 } 697 return 698 } 699 700 if err := comment.LoadIssue(ctx); err != nil { 701 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 702 return 703 } 704 705 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 706 ctx.Status(http.StatusNotFound) 707 return 708 } 709 710 if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { 711 ctx.Status(http.StatusForbidden) 712 return 713 } else if comment.Type != issues_model.CommentTypeComment { 714 ctx.Status(http.StatusNoContent) 715 return 716 } 717 718 if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { 719 ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) 720 return 721 } 722 723 ctx.Status(http.StatusNoContent) 724 }