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