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