code.gitea.io/gitea@v1.22.3/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/optional" 18 api "code.gitea.io/gitea/modules/structs" 19 "code.gitea.io/gitea/modules/web" 20 "code.gitea.io/gitea/routers/api/v1/utils" 21 "code.gitea.io/gitea/services/context" 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 optional.Option[bool] 282 canReadIssue := ctx.Repo.CanRead(unit.TypeIssues) 283 canReadPull := ctx.Repo.CanRead(unit.TypePullRequests) 284 if canReadIssue && canReadPull { 285 isPull = optional.None[bool]() 286 } else if canReadIssue { 287 isPull = optional.Some(false) 288 } else if canReadPull { 289 isPull = optional.Some(true) 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.LoadAttachments(ctx); err != nil { 327 ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) 328 return 329 } 330 if _, err := comments.Issues().LoadRepositories(ctx); err != nil { 331 ctx.Error(http.StatusInternalServerError, "LoadRepositories", err) 332 return 333 } 334 for i := range comments { 335 apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, comments[i]) 336 } 337 338 ctx.SetTotalCountHeader(totalCount) 339 ctx.JSON(http.StatusOK, &apiComments) 340 } 341 342 // CreateIssueComment create a comment for an issue 343 func CreateIssueComment(ctx *context.APIContext) { 344 // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/comments issue issueCreateComment 345 // --- 346 // summary: Add a comment to an issue 347 // consumes: 348 // - application/json 349 // produces: 350 // - application/json 351 // parameters: 352 // - name: owner 353 // in: path 354 // description: owner of the repo 355 // type: string 356 // required: true 357 // - name: repo 358 // in: path 359 // description: name of the repo 360 // type: string 361 // required: true 362 // - name: index 363 // in: path 364 // description: index of the issue 365 // type: integer 366 // format: int64 367 // required: true 368 // - name: body 369 // in: body 370 // schema: 371 // "$ref": "#/definitions/CreateIssueCommentOption" 372 // responses: 373 // "201": 374 // "$ref": "#/responses/Comment" 375 // "403": 376 // "$ref": "#/responses/forbidden" 377 // "404": 378 // "$ref": "#/responses/notFound" 379 // "423": 380 // "$ref": "#/responses/repoArchivedError" 381 382 form := web.GetForm(ctx).(*api.CreateIssueCommentOption) 383 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 384 if err != nil { 385 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 386 return 387 } 388 389 if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { 390 ctx.NotFound() 391 return 392 } 393 394 if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { 395 ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked"))) 396 return 397 } 398 399 comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) 400 if err != nil { 401 if errors.Is(err, user_model.ErrBlockedUser) { 402 ctx.Error(http.StatusForbidden, "CreateIssueComment", err) 403 } else { 404 ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) 405 } 406 return 407 } 408 409 ctx.JSON(http.StatusCreated, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) 410 } 411 412 // GetIssueComment Get a comment by ID 413 func GetIssueComment(ctx *context.APIContext) { 414 // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id} issue issueGetComment 415 // --- 416 // summary: Get a comment 417 // consumes: 418 // - application/json 419 // produces: 420 // - application/json 421 // parameters: 422 // - name: owner 423 // in: path 424 // description: owner of the repo 425 // type: string 426 // required: true 427 // - name: repo 428 // in: path 429 // description: name of the repo 430 // type: string 431 // required: true 432 // - name: id 433 // in: path 434 // description: id of the comment 435 // type: integer 436 // format: int64 437 // required: true 438 // responses: 439 // "200": 440 // "$ref": "#/responses/Comment" 441 // "204": 442 // "$ref": "#/responses/empty" 443 // "403": 444 // "$ref": "#/responses/forbidden" 445 // "404": 446 // "$ref": "#/responses/notFound" 447 448 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 449 if err != nil { 450 if issues_model.IsErrCommentNotExist(err) { 451 ctx.NotFound(err) 452 } else { 453 ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) 454 } 455 return 456 } 457 458 if err = comment.LoadIssue(ctx); err != nil { 459 ctx.InternalServerError(err) 460 return 461 } 462 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 463 ctx.Status(http.StatusNotFound) 464 return 465 } 466 467 if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { 468 ctx.NotFound() 469 return 470 } 471 472 if comment.Type != issues_model.CommentTypeComment { 473 ctx.Status(http.StatusNoContent) 474 return 475 } 476 477 if err := comment.LoadPoster(ctx); err != nil { 478 ctx.Error(http.StatusInternalServerError, "comment.LoadPoster", err) 479 return 480 } 481 482 ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) 483 } 484 485 // EditIssueComment modify a comment of an issue 486 func EditIssueComment(ctx *context.APIContext) { 487 // swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id} issue issueEditComment 488 // --- 489 // summary: Edit a comment 490 // consumes: 491 // - application/json 492 // produces: 493 // - application/json 494 // parameters: 495 // - name: owner 496 // in: path 497 // description: owner of the repo 498 // type: string 499 // required: true 500 // - name: repo 501 // in: path 502 // description: name of the repo 503 // type: string 504 // required: true 505 // - name: id 506 // in: path 507 // description: id of the comment to edit 508 // type: integer 509 // format: int64 510 // required: true 511 // - name: body 512 // in: body 513 // schema: 514 // "$ref": "#/definitions/EditIssueCommentOption" 515 // responses: 516 // "200": 517 // "$ref": "#/responses/Comment" 518 // "204": 519 // "$ref": "#/responses/empty" 520 // "403": 521 // "$ref": "#/responses/forbidden" 522 // "404": 523 // "$ref": "#/responses/notFound" 524 // "423": 525 // "$ref": "#/responses/repoArchivedError" 526 527 form := web.GetForm(ctx).(*api.EditIssueCommentOption) 528 editIssueComment(ctx, *form) 529 } 530 531 // EditIssueCommentDeprecated modify a comment of an issue 532 func EditIssueCommentDeprecated(ctx *context.APIContext) { 533 // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueEditCommentDeprecated 534 // --- 535 // summary: Edit a comment 536 // deprecated: true 537 // consumes: 538 // - application/json 539 // produces: 540 // - application/json 541 // parameters: 542 // - name: owner 543 // in: path 544 // description: owner of the repo 545 // type: string 546 // required: true 547 // - name: repo 548 // in: path 549 // description: name of the repo 550 // type: string 551 // required: true 552 // - name: index 553 // in: path 554 // description: this parameter is ignored 555 // type: integer 556 // required: true 557 // - name: id 558 // in: path 559 // description: id of the comment to edit 560 // type: integer 561 // format: int64 562 // required: true 563 // - name: body 564 // in: body 565 // schema: 566 // "$ref": "#/definitions/EditIssueCommentOption" 567 // responses: 568 // "200": 569 // "$ref": "#/responses/Comment" 570 // "204": 571 // "$ref": "#/responses/empty" 572 // "403": 573 // "$ref": "#/responses/forbidden" 574 // "404": 575 // "$ref": "#/responses/notFound" 576 577 form := web.GetForm(ctx).(*api.EditIssueCommentOption) 578 editIssueComment(ctx, *form) 579 } 580 581 func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { 582 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 583 if err != nil { 584 if issues_model.IsErrCommentNotExist(err) { 585 ctx.NotFound(err) 586 } else { 587 ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) 588 } 589 return 590 } 591 592 if err := comment.LoadIssue(ctx); err != nil { 593 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 594 return 595 } 596 597 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 598 ctx.Status(http.StatusNotFound) 599 return 600 } 601 602 if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { 603 ctx.Status(http.StatusForbidden) 604 return 605 } 606 607 if !comment.Type.HasContentSupport() { 608 ctx.Status(http.StatusNoContent) 609 return 610 } 611 612 oldContent := comment.Content 613 comment.Content = form.Body 614 if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { 615 if errors.Is(err, user_model.ErrBlockedUser) { 616 ctx.Error(http.StatusForbidden, "UpdateComment", err) 617 } else { 618 ctx.Error(http.StatusInternalServerError, "UpdateComment", err) 619 } 620 return 621 } 622 623 ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) 624 } 625 626 // DeleteIssueComment delete a comment from an issue 627 func DeleteIssueComment(ctx *context.APIContext) { 628 // swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id} issue issueDeleteComment 629 // --- 630 // summary: Delete a comment 631 // parameters: 632 // - name: owner 633 // in: path 634 // description: owner of the repo 635 // type: string 636 // required: true 637 // - name: repo 638 // in: path 639 // description: name of the repo 640 // type: string 641 // required: true 642 // - name: id 643 // in: path 644 // description: id of comment to delete 645 // type: integer 646 // format: int64 647 // required: true 648 // responses: 649 // "204": 650 // "$ref": "#/responses/empty" 651 // "403": 652 // "$ref": "#/responses/forbidden" 653 // "404": 654 // "$ref": "#/responses/notFound" 655 656 deleteIssueComment(ctx) 657 } 658 659 // DeleteIssueCommentDeprecated delete a comment from an issue 660 func DeleteIssueCommentDeprecated(ctx *context.APIContext) { 661 // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueDeleteCommentDeprecated 662 // --- 663 // summary: Delete a comment 664 // deprecated: true 665 // parameters: 666 // - name: owner 667 // in: path 668 // description: owner of the repo 669 // type: string 670 // required: true 671 // - name: repo 672 // in: path 673 // description: name of the repo 674 // type: string 675 // required: true 676 // - name: index 677 // in: path 678 // description: this parameter is ignored 679 // type: integer 680 // required: true 681 // - name: id 682 // in: path 683 // description: id of comment to delete 684 // type: integer 685 // format: int64 686 // required: true 687 // responses: 688 // "204": 689 // "$ref": "#/responses/empty" 690 // "403": 691 // "$ref": "#/responses/forbidden" 692 // "404": 693 // "$ref": "#/responses/notFound" 694 695 deleteIssueComment(ctx) 696 } 697 698 func deleteIssueComment(ctx *context.APIContext) { 699 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 700 if err != nil { 701 if issues_model.IsErrCommentNotExist(err) { 702 ctx.NotFound(err) 703 } else { 704 ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) 705 } 706 return 707 } 708 709 if err := comment.LoadIssue(ctx); err != nil { 710 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 711 return 712 } 713 714 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 715 ctx.Status(http.StatusNotFound) 716 return 717 } 718 719 if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { 720 ctx.Status(http.StatusForbidden) 721 return 722 } else if comment.Type != issues_model.CommentTypeComment { 723 ctx.Status(http.StatusNoContent) 724 return 725 } 726 727 if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { 728 ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) 729 return 730 } 731 732 ctx.Status(http.StatusNoContent) 733 }