code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/issue_comment_attachment.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"net/http"
     8  
     9  	issues_model "code.gitea.io/gitea/models/issues"
    10  	repo_model "code.gitea.io/gitea/models/repo"
    11  	"code.gitea.io/gitea/modules/context"
    12  	"code.gitea.io/gitea/modules/log"
    13  	"code.gitea.io/gitea/modules/setting"
    14  	api "code.gitea.io/gitea/modules/structs"
    15  	"code.gitea.io/gitea/modules/web"
    16  	"code.gitea.io/gitea/services/attachment"
    17  	"code.gitea.io/gitea/services/convert"
    18  	issue_service "code.gitea.io/gitea/services/issue"
    19  )
    20  
    21  // GetIssueCommentAttachment gets a single attachment of the comment
    22  func GetIssueCommentAttachment(ctx *context.APIContext) {
    23  	// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueGetIssueCommentAttachment
    24  	// ---
    25  	// summary: Get a comment attachment
    26  	// produces:
    27  	// - application/json
    28  	// parameters:
    29  	// - name: owner
    30  	//   in: path
    31  	//   description: owner of the repo
    32  	//   type: string
    33  	//   required: true
    34  	// - name: repo
    35  	//   in: path
    36  	//   description: name of the repo
    37  	//   type: string
    38  	//   required: true
    39  	// - name: id
    40  	//   in: path
    41  	//   description: id of the comment
    42  	//   type: integer
    43  	//   format: int64
    44  	//   required: true
    45  	// - name: attachment_id
    46  	//   in: path
    47  	//   description: id of the attachment to get
    48  	//   type: integer
    49  	//   format: int64
    50  	//   required: true
    51  	// responses:
    52  	//   "200":
    53  	//     "$ref": "#/responses/Attachment"
    54  	//   "404":
    55  	//     "$ref": "#/responses/error"
    56  
    57  	comment := getIssueCommentSafe(ctx)
    58  	if comment == nil {
    59  		return
    60  	}
    61  	attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
    62  	if attachment == nil {
    63  		return
    64  	}
    65  	if attachment.CommentID != comment.ID {
    66  		log.Debug("User requested attachment[%d] is not in comment[%d].", attachment.ID, comment.ID)
    67  		ctx.NotFound("attachment not in comment")
    68  		return
    69  	}
    70  
    71  	ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
    72  }
    73  
    74  // ListIssueCommentAttachments lists all attachments of the comment
    75  func ListIssueCommentAttachments(ctx *context.APIContext) {
    76  	// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueListIssueCommentAttachments
    77  	// ---
    78  	// summary: List comment's attachments
    79  	// produces:
    80  	// - application/json
    81  	// parameters:
    82  	// - name: owner
    83  	//   in: path
    84  	//   description: owner of the repo
    85  	//   type: string
    86  	//   required: true
    87  	// - name: repo
    88  	//   in: path
    89  	//   description: name of the repo
    90  	//   type: string
    91  	//   required: true
    92  	// - name: id
    93  	//   in: path
    94  	//   description: id of the comment
    95  	//   type: integer
    96  	//   format: int64
    97  	//   required: true
    98  	// responses:
    99  	//   "200":
   100  	//     "$ref": "#/responses/AttachmentList"
   101  	//   "404":
   102  	//     "$ref": "#/responses/error"
   103  	comment := getIssueCommentSafe(ctx)
   104  	if comment == nil {
   105  		return
   106  	}
   107  
   108  	if err := comment.LoadAttachments(ctx); err != nil {
   109  		ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
   110  		return
   111  	}
   112  
   113  	ctx.JSON(http.StatusOK, convert.ToAPIAttachments(ctx.Repo.Repository, comment.Attachments))
   114  }
   115  
   116  // CreateIssueCommentAttachment creates an attachment and saves the given file
   117  func CreateIssueCommentAttachment(ctx *context.APIContext) {
   118  	// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueCreateIssueCommentAttachment
   119  	// ---
   120  	// summary: Create a comment attachment
   121  	// produces:
   122  	// - application/json
   123  	// consumes:
   124  	// - multipart/form-data
   125  	// parameters:
   126  	// - name: owner
   127  	//   in: path
   128  	//   description: owner of the repo
   129  	//   type: string
   130  	//   required: true
   131  	// - name: repo
   132  	//   in: path
   133  	//   description: name of the repo
   134  	//   type: string
   135  	//   required: true
   136  	// - name: id
   137  	//   in: path
   138  	//   description: id of the comment
   139  	//   type: integer
   140  	//   format: int64
   141  	//   required: true
   142  	// - name: name
   143  	//   in: query
   144  	//   description: name of the attachment
   145  	//   type: string
   146  	//   required: false
   147  	// - name: attachment
   148  	//   in: formData
   149  	//   description: attachment to upload
   150  	//   type: file
   151  	//   required: true
   152  	// responses:
   153  	//   "201":
   154  	//     "$ref": "#/responses/Attachment"
   155  	//   "400":
   156  	//     "$ref": "#/responses/error"
   157  	//   "404":
   158  	//     "$ref": "#/responses/error"
   159  
   160  	// Check if comment exists and load comment
   161  	comment := getIssueCommentSafe(ctx)
   162  	if comment == nil {
   163  		return
   164  	}
   165  
   166  	if !canUserWriteIssueCommentAttachment(ctx, comment) {
   167  		return
   168  	}
   169  
   170  	// Get uploaded file from request
   171  	file, header, err := ctx.Req.FormFile("attachment")
   172  	if err != nil {
   173  		ctx.Error(http.StatusInternalServerError, "FormFile", err)
   174  		return
   175  	}
   176  	defer file.Close()
   177  
   178  	filename := header.Filename
   179  	if query := ctx.FormString("name"); query != "" {
   180  		filename = query
   181  	}
   182  
   183  	attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
   184  		Name:       filename,
   185  		UploaderID: ctx.Doer.ID,
   186  		RepoID:     ctx.Repo.Repository.ID,
   187  		IssueID:    comment.IssueID,
   188  		CommentID:  comment.ID,
   189  	})
   190  	if err != nil {
   191  		ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
   192  		return
   193  	}
   194  	if err := comment.LoadAttachments(ctx); err != nil {
   195  		ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
   196  		return
   197  	}
   198  
   199  	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
   200  		ctx.ServerError("UpdateComment", err)
   201  		return
   202  	}
   203  
   204  	ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
   205  }
   206  
   207  // EditIssueCommentAttachment updates the given attachment
   208  func EditIssueCommentAttachment(ctx *context.APIContext) {
   209  	// swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueEditIssueCommentAttachment
   210  	// ---
   211  	// summary: Edit a comment attachment
   212  	// produces:
   213  	// - application/json
   214  	// consumes:
   215  	// - application/json
   216  	// parameters:
   217  	// - name: owner
   218  	//   in: path
   219  	//   description: owner of the repo
   220  	//   type: string
   221  	//   required: true
   222  	// - name: repo
   223  	//   in: path
   224  	//   description: name of the repo
   225  	//   type: string
   226  	//   required: true
   227  	// - name: id
   228  	//   in: path
   229  	//   description: id of the comment
   230  	//   type: integer
   231  	//   format: int64
   232  	//   required: true
   233  	// - name: attachment_id
   234  	//   in: path
   235  	//   description: id of the attachment to edit
   236  	//   type: integer
   237  	//   format: int64
   238  	//   required: true
   239  	// - name: body
   240  	//   in: body
   241  	//   schema:
   242  	//     "$ref": "#/definitions/EditAttachmentOptions"
   243  	// responses:
   244  	//   "201":
   245  	//     "$ref": "#/responses/Attachment"
   246  	//   "404":
   247  	//     "$ref": "#/responses/error"
   248  
   249  	attach := getIssueCommentAttachmentSafeWrite(ctx)
   250  	if attach == nil {
   251  		return
   252  	}
   253  
   254  	form := web.GetForm(ctx).(*api.EditAttachmentOptions)
   255  	if form.Name != "" {
   256  		attach.Name = form.Name
   257  	}
   258  
   259  	if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
   260  		ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
   261  	}
   262  	ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
   263  }
   264  
   265  // DeleteIssueCommentAttachment delete a given attachment
   266  func DeleteIssueCommentAttachment(ctx *context.APIContext) {
   267  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueDeleteIssueCommentAttachment
   268  	// ---
   269  	// summary: Delete a comment attachment
   270  	// produces:
   271  	// - application/json
   272  	// parameters:
   273  	// - name: owner
   274  	//   in: path
   275  	//   description: owner of the repo
   276  	//   type: string
   277  	//   required: true
   278  	// - name: repo
   279  	//   in: path
   280  	//   description: name of the repo
   281  	//   type: string
   282  	//   required: true
   283  	// - name: id
   284  	//   in: path
   285  	//   description: id of the comment
   286  	//   type: integer
   287  	//   format: int64
   288  	//   required: true
   289  	// - name: attachment_id
   290  	//   in: path
   291  	//   description: id of the attachment to delete
   292  	//   type: integer
   293  	//   format: int64
   294  	//   required: true
   295  	// responses:
   296  	//   "204":
   297  	//     "$ref": "#/responses/empty"
   298  	//   "404":
   299  	//     "$ref": "#/responses/error"
   300  
   301  	attach := getIssueCommentAttachmentSafeWrite(ctx)
   302  	if attach == nil {
   303  		return
   304  	}
   305  
   306  	if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
   307  		ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err)
   308  		return
   309  	}
   310  	ctx.Status(http.StatusNoContent)
   311  }
   312  
   313  func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
   314  	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id"))
   315  	if err != nil {
   316  		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
   317  		return nil
   318  	}
   319  	if err := comment.LoadIssue(ctx); err != nil {
   320  		ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
   321  		return nil
   322  	}
   323  	if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
   324  		ctx.Error(http.StatusNotFound, "", "no matching issue comment found")
   325  		return nil
   326  	}
   327  
   328  	if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
   329  		return nil
   330  	}
   331  
   332  	comment.Issue.Repo = ctx.Repo.Repository
   333  
   334  	return comment
   335  }
   336  
   337  func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
   338  	comment := getIssueCommentSafe(ctx)
   339  	if comment == nil {
   340  		return nil
   341  	}
   342  	if !canUserWriteIssueCommentAttachment(ctx, comment) {
   343  		return nil
   344  	}
   345  	return getIssueCommentAttachmentSafeRead(ctx, comment)
   346  }
   347  
   348  func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
   349  	canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
   350  	if !canEditComment {
   351  		ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment")
   352  		return false
   353  	}
   354  
   355  	return true
   356  }
   357  
   358  func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment {
   359  	attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id"))
   360  	if err != nil {
   361  		ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
   362  		return nil
   363  	}
   364  	if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
   365  		return nil
   366  	}
   367  	return attachment
   368  }
   369  
   370  func attachmentBelongsToRepoOrComment(ctx *context.APIContext, attachment *repo_model.Attachment, comment *issues_model.Comment) bool {
   371  	if attachment.RepoID != ctx.Repo.Repository.ID {
   372  		log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
   373  		ctx.NotFound("no such attachment in repo")
   374  		return false
   375  	}
   376  	if attachment.IssueID == 0 || attachment.CommentID == 0 {
   377  		log.Debug("Requested attachment[%d] is not in a comment.", attachment.ID)
   378  		ctx.NotFound("no such attachment in comment")
   379  		return false
   380  	}
   381  	if comment != nil && attachment.CommentID != comment.ID {
   382  		log.Debug("Requested attachment[%d] does not belong to comment[%d].", attachment.ID, comment.ID)
   383  		ctx.NotFound("no such attachment in comment")
   384  		return false
   385  	}
   386  	return true
   387  }