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