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