mirror of https://github.com/go-gitea/gitea
Add API management for issue/pull and comment attachments (#21783)
Close #14601 Fix #3690 Revive of #14601. Updated to current code, cleanup and added more read/write checks. Signed-off-by: Andrew Thornton <art27@cantab.net> Signed-off-by: Andre Bruch <ab@andrebruch.com> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Norwin <git@nroo.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>pull/22080/head
parent
8fb1e53ca2
commit
3c59d31bc6
@ -0,0 +1,30 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package convert |
||||
|
||||
import ( |
||||
repo_model "code.gitea.io/gitea/models/repo" |
||||
api "code.gitea.io/gitea/modules/structs" |
||||
) |
||||
|
||||
// ToAttachment converts models.Attachment to api.Attachment
|
||||
func ToAttachment(a *repo_model.Attachment) *api.Attachment { |
||||
return &api.Attachment{ |
||||
ID: a.ID, |
||||
Name: a.Name, |
||||
Created: a.CreatedUnix.AsTime(), |
||||
DownloadCount: a.DownloadCount, |
||||
Size: a.Size, |
||||
UUID: a.UUID, |
||||
DownloadURL: a.DownloadURL(), |
||||
} |
||||
} |
||||
|
||||
func ToAttachments(attachments []*repo_model.Attachment) []*api.Attachment { |
||||
converted := make([]*api.Attachment, 0, len(attachments)) |
||||
for _, attachment := range attachments { |
||||
converted = append(converted, ToAttachment(attachment)) |
||||
} |
||||
return converted |
||||
} |
@ -0,0 +1,372 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues" |
||||
repo_model "code.gitea.io/gitea/models/repo" |
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/convert" |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
api "code.gitea.io/gitea/modules/structs" |
||||
"code.gitea.io/gitea/modules/web" |
||||
"code.gitea.io/gitea/services/attachment" |
||||
issue_service "code.gitea.io/gitea/services/issue" |
||||
) |
||||
|
||||
// GetIssueAttachment gets a single attachment of the issue
|
||||
func GetIssueAttachment(ctx *context.APIContext) { |
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueGetIssueAttachment
|
||||
// ---
|
||||
// summary: Get an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
issue := getIssueFromContext(ctx) |
||||
if issue == nil { |
||||
return |
||||
} |
||||
|
||||
attach := getIssueAttachmentSafeRead(ctx, issue) |
||||
if attach == nil { |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAttachment(attach)) |
||||
} |
||||
|
||||
// ListIssueAttachments lists all attachments of the issue
|
||||
func ListIssueAttachments(ctx *context.APIContext) { |
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets issue issueListIssueAttachments
|
||||
// ---
|
||||
// summary: List issue's attachments
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/AttachmentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
issue := getIssueFromContext(ctx) |
||||
if issue == nil { |
||||
return |
||||
} |
||||
|
||||
if err := issue.LoadAttributes(ctx); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue).Attachments) |
||||
} |
||||
|
||||
// CreateIssueAttachment creates an attachment and saves the given file
|
||||
func CreateIssueAttachment(ctx *context.APIContext) { |
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assets issue issueCreateIssueAttachment
|
||||
// ---
|
||||
// summary: Create an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
// type: file
|
||||
// required: true
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
issue := getIssueFromContext(ctx) |
||||
if issue == nil { |
||||
return |
||||
} |
||||
|
||||
if !canUserWriteIssueAttachment(ctx, issue) { |
||||
return |
||||
} |
||||
|
||||
// Get uploaded file from request
|
||||
file, header, err := ctx.Req.FormFile("attachment") |
||||
if err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "FormFile", err) |
||||
return |
||||
} |
||||
defer file.Close() |
||||
|
||||
filename := header.Filename |
||||
if query := ctx.FormString("name"); query != "" { |
||||
filename = query |
||||
} |
||||
|
||||
attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{ |
||||
Name: filename, |
||||
UploaderID: ctx.Doer.ID, |
||||
RepoID: ctx.Repo.Repository.ID, |
||||
IssueID: issue.ID, |
||||
}) |
||||
if err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err) |
||||
return |
||||
} |
||||
|
||||
issue.Attachments = append(issue.Attachments, attachment) |
||||
|
||||
if err := issue_service.ChangeContent(issue, ctx.Doer, issue.Content); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "ChangeContent", err) |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment)) |
||||
} |
||||
|
||||
// EditIssueAttachment updates the given attachment
|
||||
func EditIssueAttachment(ctx *context.APIContext) { |
||||
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueEditIssueAttachment
|
||||
// ---
|
||||
// summary: Edit an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditAttachmentOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
attachment := getIssueAttachmentSafeWrite(ctx) |
||||
if attachment == nil { |
||||
return |
||||
} |
||||
|
||||
// do changes to attachment. only meaningful change is name.
|
||||
form := web.GetForm(ctx).(*api.EditAttachmentOptions) |
||||
if form.Name != "" { |
||||
attachment.Name = form.Name |
||||
} |
||||
|
||||
if err := repo_model.UpdateAttachment(ctx, attachment); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err) |
||||
} |
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment)) |
||||
} |
||||
|
||||
// DeleteIssueAttachment delete a given attachment
|
||||
func DeleteIssueAttachment(ctx *context.APIContext) { |
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueDeleteIssueAttachment
|
||||
// ---
|
||||
// summary: Delete an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
attachment := getIssueAttachmentSafeWrite(ctx) |
||||
if attachment == nil { |
||||
return |
||||
} |
||||
|
||||
if err := repo_model.DeleteAttachment(attachment, true); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err) |
||||
return |
||||
} |
||||
|
||||
ctx.Status(http.StatusNoContent) |
||||
} |
||||
|
||||
func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue { |
||||
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64("index")) |
||||
if err != nil { |
||||
ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err) |
||||
return nil |
||||
} |
||||
|
||||
issue.Repo = ctx.Repo.Repository |
||||
|
||||
return issue |
||||
} |
||||
|
||||
func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment { |
||||
issue := getIssueFromContext(ctx) |
||||
if issue == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !canUserWriteIssueAttachment(ctx, issue) { |
||||
return nil |
||||
} |
||||
|
||||
return getIssueAttachmentSafeRead(ctx, issue) |
||||
} |
||||
|
||||
func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment { |
||||
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("asset")) |
||||
if err != nil { |
||||
ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) |
||||
return nil |
||||
} |
||||
if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) { |
||||
return nil |
||||
} |
||||
return attachment |
||||
} |
||||
|
||||
func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool { |
||||
canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) |
||||
if !canEditIssue { |
||||
ctx.Error(http.StatusForbidden, "", "user should have permission to write issue") |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_model.Attachment, issue *issues_model.Issue) bool { |
||||
if attachment.RepoID != ctx.Repo.Repository.ID { |
||||
log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository) |
||||
ctx.NotFound("no such attachment in repo") |
||||
return false |
||||
} |
||||
if attachment.IssueID == 0 { |
||||
log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID) |
||||
ctx.NotFound("no such attachment in issue") |
||||
return false |
||||
} else if issue != nil && attachment.IssueID != issue.ID { |
||||
log.Debug("Requested attachment[%d] does not belong to issue[%d, #%d].", attachment.ID, issue.ID, issue.Index) |
||||
ctx.NotFound("no such attachment in issue") |
||||
return false |
||||
} |
||||
return true |
||||
} |
@ -0,0 +1,383 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues" |
||||
repo_model "code.gitea.io/gitea/models/repo" |
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/convert" |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
api "code.gitea.io/gitea/modules/structs" |
||||
"code.gitea.io/gitea/modules/web" |
||||
"code.gitea.io/gitea/services/attachment" |
||||
comment_service "code.gitea.io/gitea/services/comments" |
||||
) |
||||
|
||||
// GetIssueCommentAttachment gets a single attachment of the comment
|
||||
func GetIssueCommentAttachment(ctx *context.APIContext) { |
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueGetIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Get a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
comment := getIssueCommentSafe(ctx) |
||||
if comment == nil { |
||||
return |
||||
} |
||||
attachment := getIssueCommentAttachmentSafeRead(ctx, comment) |
||||
if attachment == nil { |
||||
return |
||||
} |
||||
if attachment.CommentID != comment.ID { |
||||
log.Debug("User requested attachment[%d] is not in comment[%d].", attachment.ID, comment.ID) |
||||
ctx.NotFound("attachment not in comment") |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAttachment(attachment)) |
||||
} |
||||
|
||||
// ListIssueCommentAttachments lists all attachments of the comment
|
||||
func ListIssueCommentAttachments(ctx *context.APIContext) { |
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueListIssueCommentAttachments
|
||||
// ---
|
||||
// summary: List comment's attachments
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/AttachmentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
comment := getIssueCommentSafe(ctx) |
||||
if comment == nil { |
||||
return |
||||
} |
||||
|
||||
if err := comment.LoadAttachments(ctx); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAttachments(comment.Attachments)) |
||||
} |
||||
|
||||
// CreateIssueCommentAttachment creates an attachment and saves the given file
|
||||
func CreateIssueCommentAttachment(ctx *context.APIContext) { |
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueCreateIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Create a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
// type: file
|
||||
// required: true
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
// Check if comment exists and load comment
|
||||
comment := getIssueCommentSafe(ctx) |
||||
if comment == nil { |
||||
return |
||||
} |
||||
|
||||
if !canUserWriteIssueCommentAttachment(ctx, comment) { |
||||
return |
||||
} |
||||
|
||||
// Get uploaded file from request
|
||||
file, header, err := ctx.Req.FormFile("attachment") |
||||
if err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "FormFile", err) |
||||
return |
||||
} |
||||
defer file.Close() |
||||
|
||||
filename := header.Filename |
||||
if query := ctx.FormString("name"); query != "" { |
||||
filename = query |
||||
} |
||||
|
||||
attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{ |
||||
Name: filename, |
||||
UploaderID: ctx.Doer.ID, |
||||
RepoID: ctx.Repo.Repository.ID, |
||||
IssueID: comment.IssueID, |
||||
CommentID: comment.ID, |
||||
}) |
||||
if err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err) |
||||
return |
||||
} |
||||
if err := comment.LoadAttachments(ctx); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) |
||||
return |
||||
} |
||||
|
||||
if err = comment_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil { |
||||
ctx.ServerError("UpdateComment", err) |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment)) |
||||
} |
||||
|
||||
// EditIssueCommentAttachment updates the given attachment
|
||||
func EditIssueCommentAttachment(ctx *context.APIContext) { |
||||
// swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueEditIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Edit a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditAttachmentOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
attach := getIssueCommentAttachmentSafeWrite(ctx) |
||||
if attach == nil { |
||||
return |
||||
} |
||||
|
||||
form := web.GetForm(ctx).(*api.EditAttachmentOptions) |
||||
if form.Name != "" { |
||||
attach.Name = form.Name |
||||
} |
||||
|
||||
if err := repo_model.UpdateAttachment(ctx, attach); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach) |
||||
} |
||||
ctx.JSON(http.StatusCreated, convert.ToAttachment(attach)) |
||||
} |
||||
|
||||
// DeleteIssueCommentAttachment delete a given attachment
|
||||
func DeleteIssueCommentAttachment(ctx *context.APIContext) { |
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueDeleteIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Delete a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
attach := getIssueCommentAttachmentSafeWrite(ctx) |
||||
if attach == nil { |
||||
return |
||||
} |
||||
|
||||
if err := repo_model.DeleteAttachment(attach, true); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err) |
||||
return |
||||
} |
||||
ctx.Status(http.StatusNoContent) |
||||
} |
||||
|
||||
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { |
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id")) |
||||
if err != nil { |
||||
ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) |
||||
return nil |
||||
} |
||||
if err := comment.LoadIssue(ctx); err != nil { |
||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) |
||||
return nil |
||||
} |
||||
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID { |
||||
ctx.Error(http.StatusNotFound, "", "no matching issue comment found") |
||||
return nil |
||||
} |
||||
|
||||
comment.Issue.Repo = ctx.Repo.Repository |
||||
|
||||
return comment |
||||
} |
||||
|
||||
func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment { |
||||
comment := getIssueCommentSafe(ctx) |
||||
if comment == nil { |
||||
return nil |
||||
} |
||||
if !canUserWriteIssueCommentAttachment(ctx, comment) { |
||||
return nil |
||||
} |
||||
return getIssueCommentAttachmentSafeRead(ctx, comment) |
||||
} |
||||
|
||||
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool { |
||||
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) |
||||
if !canEditComment { |
||||
ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment") |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment { |
||||
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("asset")) |
||||
if err != nil { |
||||
ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) |
||||
return nil |
||||
} |
||||
if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) { |
||||
return nil |
||||
} |
||||
return attachment |
||||
} |
||||
|
||||
func attachmentBelongsToRepoOrComment(ctx *context.APIContext, attachment *repo_model.Attachment, comment *issues_model.Comment) bool { |
||||
if attachment.RepoID != ctx.Repo.Repository.ID { |
||||
log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository) |
||||
ctx.NotFound("no such attachment in repo") |
||||
return false |
||||
} |
||||
if attachment.IssueID == 0 || attachment.CommentID == 0 { |
||||
log.Debug("Requested attachment[%d] is not in a comment.", attachment.ID) |
||||
ctx.NotFound("no such attachment in comment") |
||||
return false |
||||
} |
||||
if comment != nil && attachment.CommentID != comment.ID { |
||||
log.Debug("Requested attachment[%d] does not belong to comment[%d].", attachment.ID, comment.ID) |
||||
ctx.NotFound("no such attachment in comment") |
||||
return false |
||||
} |
||||
return true |
||||
} |
@ -0,0 +1,154 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package integration |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"mime/multipart" |
||||
"net/http" |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
issues_model "code.gitea.io/gitea/models/issues" |
||||
repo_model "code.gitea.io/gitea/models/repo" |
||||
"code.gitea.io/gitea/models/unittest" |
||||
user_model "code.gitea.io/gitea/models/user" |
||||
"code.gitea.io/gitea/modules/convert" |
||||
api "code.gitea.io/gitea/modules/structs" |
||||
"code.gitea.io/gitea/tests" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestAPIGetCommentAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}) |
||||
assert.NoError(t, comment.LoadIssue(db.DefaultContext)) |
||||
assert.NoError(t, comment.LoadAttachments(db.DefaultContext)) |
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: comment.Attachments[0].ID}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID) |
||||
session.MakeRequest(t, req, http.StatusOK) |
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token) |
||||
resp := session.MakeRequest(t, req, http.StatusOK) |
||||
|
||||
var apiAttachment api.Attachment |
||||
DecodeJSON(t, resp, &apiAttachment) |
||||
|
||||
expect := convert.ToAttachment(attachment) |
||||
assert.Equal(t, expect.ID, apiAttachment.ID) |
||||
assert.Equal(t, expect.Name, apiAttachment.Name) |
||||
assert.Equal(t, expect.UUID, apiAttachment.UUID) |
||||
assert.Equal(t, expect.Created.Unix(), apiAttachment.Created.Unix()) |
||||
} |
||||
|
||||
func TestAPIListCommentAttachments(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets", |
||||
repoOwner.Name, repo.Name, comment.ID) |
||||
resp := session.MakeRequest(t, req, http.StatusOK) |
||||
|
||||
var apiAttachments []*api.Attachment |
||||
DecodeJSON(t, resp, &apiAttachments) |
||||
expectedCount := unittest.GetCount(t, &repo_model.Attachment{CommentID: comment.ID}) |
||||
assert.EqualValues(t, expectedCount, len(apiAttachments)) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachments[0].ID, CommentID: comment.ID}) |
||||
} |
||||
|
||||
func TestAPICreateCommentAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s", |
||||
repoOwner.Name, repo.Name, comment.ID, token) |
||||
|
||||
filename := "image.png" |
||||
buff := generateImg() |
||||
body := &bytes.Buffer{} |
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body) |
||||
part, err := writer.CreateFormFile("attachment", filename) |
||||
assert.NoError(t, err) |
||||
_, err = io.Copy(part, &buff) |
||||
assert.NoError(t, err) |
||||
err = writer.Close() |
||||
assert.NoError(t, err) |
||||
|
||||
req := NewRequestWithBody(t, "POST", urlStr, body) |
||||
req.Header.Add("Content-Type", writer.FormDataContentType()) |
||||
resp := session.MakeRequest(t, req, http.StatusCreated) |
||||
|
||||
apiAttachment := new(api.Attachment) |
||||
DecodeJSON(t, resp, &apiAttachment) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID}) |
||||
} |
||||
|
||||
func TestAPIEditCommentAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
const newAttachmentName = "newAttachmentName" |
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6}) |
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", |
||||
repoOwner.Name, repo.Name, comment.ID, attachment.ID, token) |
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ |
||||
"name": newAttachmentName, |
||||
}) |
||||
resp := session.MakeRequest(t, req, http.StatusCreated) |
||||
apiAttachment := new(api.Attachment) |
||||
DecodeJSON(t, resp, &apiAttachment) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID, Name: apiAttachment.Name}) |
||||
} |
||||
|
||||
func TestAPIDeleteCommentAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6}) |
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", |
||||
repoOwner.Name, repo.Name, comment.ID, attachment.ID, token) |
||||
|
||||
req := NewRequestf(t, "DELETE", urlStr) |
||||
session.MakeRequest(t, req, http.StatusNoContent) |
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Attachment{ID: attachment.ID, CommentID: comment.ID}) |
||||
} |
@ -0,0 +1,143 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package integration |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"mime/multipart" |
||||
"net/http" |
||||
"testing" |
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues" |
||||
repo_model "code.gitea.io/gitea/models/repo" |
||||
"code.gitea.io/gitea/models/unittest" |
||||
user_model "code.gitea.io/gitea/models/user" |
||||
api "code.gitea.io/gitea/modules/structs" |
||||
"code.gitea.io/gitea/tests" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestAPIGetIssueAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s", |
||||
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token) |
||||
|
||||
req := NewRequest(t, "GET", urlStr) |
||||
resp := session.MakeRequest(t, req, http.StatusOK) |
||||
apiAttachment := new(api.Attachment) |
||||
DecodeJSON(t, resp, &apiAttachment) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID}) |
||||
} |
||||
|
||||
func TestAPIListIssueAttachments(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s", |
||||
repoOwner.Name, repo.Name, issue.Index, token) |
||||
|
||||
req := NewRequest(t, "GET", urlStr) |
||||
resp := session.MakeRequest(t, req, http.StatusOK) |
||||
apiAttachment := new([]api.Attachment) |
||||
DecodeJSON(t, resp, &apiAttachment) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: (*apiAttachment)[0].ID, IssueID: issue.ID}) |
||||
} |
||||
|
||||
func TestAPICreateIssueAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s", |
||||
repoOwner.Name, repo.Name, issue.Index, token) |
||||
|
||||
filename := "image.png" |
||||
buff := generateImg() |
||||
body := &bytes.Buffer{} |
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body) |
||||
part, err := writer.CreateFormFile("attachment", filename) |
||||
assert.NoError(t, err) |
||||
_, err = io.Copy(part, &buff) |
||||
assert.NoError(t, err) |
||||
err = writer.Close() |
||||
assert.NoError(t, err) |
||||
|
||||
req := NewRequestWithBody(t, "POST", urlStr, body) |
||||
req.Header.Add("Content-Type", writer.FormDataContentType()) |
||||
resp := session.MakeRequest(t, req, http.StatusCreated) |
||||
|
||||
apiAttachment := new(api.Attachment) |
||||
DecodeJSON(t, resp, &apiAttachment) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID}) |
||||
} |
||||
|
||||
func TestAPIEditIssueAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
const newAttachmentName = "newAttachmentName" |
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s", |
||||
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token) |
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ |
||||
"name": newAttachmentName, |
||||
}) |
||||
resp := session.MakeRequest(t, req, http.StatusCreated) |
||||
apiAttachment := new(api.Attachment) |
||||
DecodeJSON(t, resp, &apiAttachment) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID, Name: apiAttachment.Name}) |
||||
} |
||||
|
||||
func TestAPIDeleteIssueAttachment(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1}) |
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) |
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID}) |
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) |
||||
|
||||
session := loginUser(t, repoOwner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s", |
||||
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token) |
||||
|
||||
req := NewRequest(t, "DELETE", urlStr) |
||||
session.MakeRequest(t, req, http.StatusNoContent) |
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Attachment{ID: attachment.ID, IssueID: issue.ID}) |
||||
} |
Loading…
Reference in new issue