mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
Several handlers and model helpers skipped checks that their siblings already perform. - Only match principal keys when looking up SSH certificate principals - Return no attachments for an unsaved issue instead of every issue-less one - Reject issue and comment attachments when adding release attachments - Share one permission check for editing, deleting and reacting to issue content and for comment attachments between the web UI and the API, refusing them on locked issues for users who can't write - Reject issue edits, deletion and label changes in archived repositories via the API - Apply cross-origin protection to the repository topics endpoint - Return 404 for the repositories tab of individual users and respect profile visibility for the heatmap Fixes: https://github.com/go-gitea/gitea/issues/39226 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: bircni <bircni@icloud.com>
414 lines
11 KiB
Go
414 lines
11 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
issues_model "gitea.dev/models/issues"
|
|
"gitea.dev/models/perm"
|
|
repo_model "gitea.dev/models/repo"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/setting"
|
|
api "gitea.dev/modules/structs"
|
|
"gitea.dev/modules/util"
|
|
"gitea.dev/modules/web"
|
|
attachment_service "gitea.dev/services/attachment"
|
|
"gitea.dev/services/context"
|
|
"gitea.dev/services/context/upload"
|
|
"gitea.dev/services/convert"
|
|
issue_service "gitea.dev/services/issue"
|
|
)
|
|
|
|
// 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.APIErrorNotFound("attachment not in comment")
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx, ctx.Repo.Repository, 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.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToAPIAttachments(ctx, ctx.Repo.Repository, 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"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/error"
|
|
// "413":
|
|
// "$ref": "#/responses/error"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
// 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.APIErrorInternal(err)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
filename := header.Filename
|
|
if query := ctx.FormString("name"); query != "" {
|
|
filename = query
|
|
}
|
|
|
|
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
|
|
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
|
|
Name: filename,
|
|
UploaderID: ctx.Doer.ID,
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
IssueID: comment.IssueID,
|
|
CommentID: comment.ID,
|
|
})
|
|
if err != nil {
|
|
if upload.IsErrFileTypeForbidden(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
|
} else if errors.Is(err, util.ErrContentTooLarge) {
|
|
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := comment.LoadAttachments(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil {
|
|
if errors.Is(err, user_model.ErrBlockedUser) {
|
|
ctx.APIError(http.StatusForbidden, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx, ctx.Repo.Repository, 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"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
attach := getIssueCommentAttachmentSafeWrite(ctx)
|
|
if attach == nil {
|
|
return
|
|
}
|
|
|
|
form := web.GetForm[*api.EditAttachmentOptions](ctx)
|
|
if form.Name != "" {
|
|
attach.Name = form.Name
|
|
}
|
|
|
|
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil {
|
|
if upload.IsErrFileTypeForbidden(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
|
return
|
|
}
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx, ctx.Repo.Repository, 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"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
attach := getIssueCommentAttachmentSafeWrite(ctx)
|
|
if attach == nil {
|
|
return
|
|
}
|
|
|
|
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
|
|
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
|
if err != nil {
|
|
ctx.APIErrorAuto(err)
|
|
return nil
|
|
}
|
|
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
|
ctx.APIErrorNotFound()
|
|
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 := issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeWrite)
|
|
if !canEditComment {
|
|
ctx.APIError(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.PathParamInt64("attachment_id"))
|
|
if err != nil {
|
|
ctx.APIErrorAuto(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.APIErrorNotFound("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.APIErrorNotFound("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.APIErrorNotFound("no such attachment in comment")
|
|
return false
|
|
}
|
|
return true
|
|
}
|