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