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