code.gitea.io/gitea@v1.21.7/routers/api/v1/utils/hook.go (about) 1 // Copyright 2016 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package utils 5 6 import ( 7 "fmt" 8 "net/http" 9 "strconv" 10 "strings" 11 12 user_model "code.gitea.io/gitea/models/user" 13 "code.gitea.io/gitea/models/webhook" 14 "code.gitea.io/gitea/modules/context" 15 "code.gitea.io/gitea/modules/json" 16 "code.gitea.io/gitea/modules/setting" 17 api "code.gitea.io/gitea/modules/structs" 18 "code.gitea.io/gitea/modules/util" 19 webhook_module "code.gitea.io/gitea/modules/webhook" 20 webhook_service "code.gitea.io/gitea/services/webhook" 21 ) 22 23 // ListOwnerHooks lists the webhooks of the provided owner 24 func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { 25 opts := &webhook.ListWebhookOptions{ 26 ListOptions: GetListOptions(ctx), 27 OwnerID: owner.ID, 28 } 29 30 count, err := webhook.CountWebhooksByOpts(opts) 31 if err != nil { 32 ctx.InternalServerError(err) 33 return 34 } 35 36 hooks, err := webhook.ListWebhooksByOpts(ctx, opts) 37 if err != nil { 38 ctx.InternalServerError(err) 39 return 40 } 41 42 apiHooks := make([]*api.Hook, len(hooks)) 43 for i, hook := range hooks { 44 apiHooks[i], err = webhook_service.ToHook(owner.HomeLink(), hook) 45 if err != nil { 46 ctx.InternalServerError(err) 47 return 48 } 49 } 50 51 ctx.SetTotalCountHeader(count) 52 ctx.JSON(http.StatusOK, apiHooks) 53 } 54 55 // GetOwnerHook gets an user or organization webhook. Errors are written to ctx. 56 func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webhook, error) { 57 w, err := webhook.GetWebhookByOwnerID(ctx, ownerID, hookID) 58 if err != nil { 59 if webhook.IsErrWebhookNotExist(err) { 60 ctx.NotFound() 61 } else { 62 ctx.Error(http.StatusInternalServerError, "GetWebhookByOwnerID", err) 63 } 64 return nil, err 65 } 66 return w, nil 67 } 68 69 // GetRepoHook get a repo's webhook. If there is an error, write to `ctx` 70 // accordingly and return the error 71 func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhook, error) { 72 w, err := webhook.GetWebhookByRepoID(ctx, repoID, hookID) 73 if err != nil { 74 if webhook.IsErrWebhookNotExist(err) { 75 ctx.NotFound() 76 } else { 77 ctx.Error(http.StatusInternalServerError, "GetWebhookByID", err) 78 } 79 return nil, err 80 } 81 return w, nil 82 } 83 84 // checkCreateHookOption check if a CreateHookOption form is valid. If invalid, 85 // write the appropriate error to `ctx`. Return whether the form is valid 86 func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { 87 if !webhook_service.IsValidHookTaskType(form.Type) { 88 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Invalid hook type: %s", form.Type)) 89 return false 90 } 91 for _, name := range []string{"url", "content_type"} { 92 if _, ok := form.Config[name]; !ok { 93 ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: "+name) 94 return false 95 } 96 } 97 if !webhook.IsValidHookContentType(form.Config["content_type"]) { 98 ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type") 99 return false 100 } 101 return true 102 } 103 104 // AddSystemHook add a system hook 105 func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) { 106 hook, ok := addHook(ctx, form, 0, 0) 107 if ok { 108 h, err := webhook_service.ToHook(setting.AppSubURL+"/admin", hook) 109 if err != nil { 110 ctx.Error(http.StatusInternalServerError, "convert.ToHook", err) 111 return 112 } 113 ctx.JSON(http.StatusCreated, h) 114 } 115 } 116 117 // AddOwnerHook adds a hook to an user or organization 118 func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) { 119 hook, ok := addHook(ctx, form, owner.ID, 0) 120 if !ok { 121 return 122 } 123 apiHook, ok := toAPIHook(ctx, owner.HomeLink(), hook) 124 if !ok { 125 return 126 } 127 ctx.JSON(http.StatusCreated, apiHook) 128 } 129 130 // AddRepoHook add a hook to a repo. Writes to `ctx` accordingly 131 func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) { 132 repo := ctx.Repo 133 hook, ok := addHook(ctx, form, 0, repo.Repository.ID) 134 if !ok { 135 return 136 } 137 apiHook, ok := toAPIHook(ctx, repo.RepoLink, hook) 138 if !ok { 139 return 140 } 141 ctx.JSON(http.StatusCreated, apiHook) 142 } 143 144 // toAPIHook converts the hook to its API representation. 145 // If there is an error, write to `ctx` accordingly. Return (hook, ok) 146 func toAPIHook(ctx *context.APIContext, repoLink string, hook *webhook.Webhook) (*api.Hook, bool) { 147 apiHook, err := webhook_service.ToHook(repoLink, hook) 148 if err != nil { 149 ctx.Error(http.StatusInternalServerError, "ToHook", err) 150 return nil, false 151 } 152 return apiHook, true 153 } 154 155 func issuesHook(events []string, event string) bool { 156 return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventIssues), true) 157 } 158 159 func pullHook(events []string, event string) bool { 160 return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true) 161 } 162 163 // addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is 164 // an error, write to `ctx` accordingly. Return (webhook, ok) 165 func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) { 166 var isSystemWebhook bool 167 if !checkCreateHookOption(ctx, form) { 168 return nil, false 169 } 170 171 if len(form.Events) == 0 { 172 form.Events = []string{"push"} 173 } 174 if form.Config["is_system_webhook"] != "" { 175 sw, err := strconv.ParseBool(form.Config["is_system_webhook"]) 176 if err != nil { 177 ctx.Error(http.StatusUnprocessableEntity, "", "Invalid is_system_webhook value") 178 return nil, false 179 } 180 isSystemWebhook = sw 181 } 182 w := &webhook.Webhook{ 183 OwnerID: ownerID, 184 RepoID: repoID, 185 URL: form.Config["url"], 186 ContentType: webhook.ToHookContentType(form.Config["content_type"]), 187 Secret: form.Config["secret"], 188 HTTPMethod: "POST", 189 IsSystemWebhook: isSystemWebhook, 190 HookEvent: &webhook_module.HookEvent{ 191 ChooseEvents: true, 192 HookEvents: webhook_module.HookEvents{ 193 Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true), 194 Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true), 195 Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true), 196 Issues: issuesHook(form.Events, "issues_only"), 197 IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)), 198 IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)), 199 IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)), 200 IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)), 201 Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true), 202 PullRequest: pullHook(form.Events, "pull_request_only"), 203 PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)), 204 PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)), 205 PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)), 206 PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)), 207 PullRequestReview: pullHook(form.Events, "pull_request_review"), 208 PullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)), 209 PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)), 210 Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true), 211 Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true), 212 Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true), 213 }, 214 BranchFilter: form.BranchFilter, 215 }, 216 IsActive: form.Active, 217 Type: form.Type, 218 } 219 err := w.SetHeaderAuthorization(form.AuthorizationHeader) 220 if err != nil { 221 ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err) 222 return nil, false 223 } 224 if w.Type == webhook_module.SLACK { 225 channel, ok := form.Config["channel"] 226 if !ok { 227 ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: channel") 228 return nil, false 229 } 230 channel = strings.TrimSpace(channel) 231 232 if !webhook_service.IsValidSlackChannel(channel) { 233 ctx.Error(http.StatusBadRequest, "", "Invalid slack channel name") 234 return nil, false 235 } 236 237 meta, err := json.Marshal(&webhook_service.SlackMeta{ 238 Channel: channel, 239 Username: form.Config["username"], 240 IconURL: form.Config["icon_url"], 241 Color: form.Config["color"], 242 }) 243 if err != nil { 244 ctx.Error(http.StatusInternalServerError, "slack: JSON marshal failed", err) 245 return nil, false 246 } 247 w.Meta = string(meta) 248 } 249 250 if err := w.UpdateEvent(); err != nil { 251 ctx.Error(http.StatusInternalServerError, "UpdateEvent", err) 252 return nil, false 253 } else if err := webhook.CreateWebhook(ctx, w); err != nil { 254 ctx.Error(http.StatusInternalServerError, "CreateWebhook", err) 255 return nil, false 256 } 257 return w, true 258 } 259 260 // EditSystemHook edit system webhook `w` according to `form`. Writes to `ctx` accordingly 261 func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) { 262 hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID) 263 if err != nil { 264 ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err) 265 return 266 } 267 if !editHook(ctx, form, hook) { 268 ctx.Error(http.StatusInternalServerError, "editHook", err) 269 return 270 } 271 updated, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID) 272 if err != nil { 273 ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err) 274 return 275 } 276 h, err := webhook_service.ToHook(setting.AppURL+"/admin", updated) 277 if err != nil { 278 ctx.Error(http.StatusInternalServerError, "convert.ToHook", err) 279 return 280 } 281 ctx.JSON(http.StatusOK, h) 282 } 283 284 // EditOwnerHook updates a webhook of an user or organization 285 func EditOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.EditHookOption, hookID int64) { 286 hook, err := GetOwnerHook(ctx, owner.ID, hookID) 287 if err != nil { 288 return 289 } 290 if !editHook(ctx, form, hook) { 291 return 292 } 293 updated, err := GetOwnerHook(ctx, owner.ID, hookID) 294 if err != nil { 295 return 296 } 297 apiHook, ok := toAPIHook(ctx, owner.HomeLink(), updated) 298 if !ok { 299 return 300 } 301 ctx.JSON(http.StatusOK, apiHook) 302 } 303 304 // EditRepoHook edit webhook `w` according to `form`. Writes to `ctx` accordingly 305 func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) { 306 repo := ctx.Repo 307 hook, err := GetRepoHook(ctx, repo.Repository.ID, hookID) 308 if err != nil { 309 return 310 } 311 if !editHook(ctx, form, hook) { 312 return 313 } 314 updated, err := GetRepoHook(ctx, repo.Repository.ID, hookID) 315 if err != nil { 316 return 317 } 318 apiHook, ok := toAPIHook(ctx, repo.RepoLink, updated) 319 if !ok { 320 return 321 } 322 ctx.JSON(http.StatusOK, apiHook) 323 } 324 325 // editHook edit the webhook `w` according to `form`. If an error occurs, write 326 // to `ctx` accordingly and return the error. Return whether successful 327 func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool { 328 if form.Config != nil { 329 if url, ok := form.Config["url"]; ok { 330 w.URL = url 331 } 332 if ct, ok := form.Config["content_type"]; ok { 333 if !webhook.IsValidHookContentType(ct) { 334 ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type") 335 return false 336 } 337 w.ContentType = webhook.ToHookContentType(ct) 338 } 339 340 if w.Type == webhook_module.SLACK { 341 if channel, ok := form.Config["channel"]; ok { 342 meta, err := json.Marshal(&webhook_service.SlackMeta{ 343 Channel: channel, 344 Username: form.Config["username"], 345 IconURL: form.Config["icon_url"], 346 Color: form.Config["color"], 347 }) 348 if err != nil { 349 ctx.Error(http.StatusInternalServerError, "slack: JSON marshal failed", err) 350 return false 351 } 352 w.Meta = string(meta) 353 } 354 } 355 } 356 357 // Update events 358 if len(form.Events) == 0 { 359 form.Events = []string{"push"} 360 } 361 w.PushOnly = false 362 w.SendEverything = false 363 w.ChooseEvents = true 364 w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) 365 w.Push = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true) 366 w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) 367 w.Delete = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true) 368 w.Fork = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true) 369 w.Repository = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true) 370 w.Wiki = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true) 371 w.Release = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true) 372 w.BranchFilter = form.BranchFilter 373 374 err := w.SetHeaderAuthorization(form.AuthorizationHeader) 375 if err != nil { 376 ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err) 377 return false 378 } 379 380 // Issues 381 w.Issues = issuesHook(form.Events, "issues_only") 382 w.IssueAssign = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)) 383 w.IssueLabel = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)) 384 w.IssueMilestone = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)) 385 w.IssueComment = issuesHook(form.Events, string(webhook_module.HookEventIssueComment)) 386 387 // Pull requests 388 w.PullRequest = pullHook(form.Events, "pull_request_only") 389 w.PullRequestAssign = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)) 390 w.PullRequestLabel = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)) 391 w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)) 392 w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)) 393 w.PullRequestReview = pullHook(form.Events, "pull_request_review") 394 w.PullRequestReviewRequest = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)) 395 w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)) 396 397 if err := w.UpdateEvent(); err != nil { 398 ctx.Error(http.StatusInternalServerError, "UpdateEvent", err) 399 return false 400 } 401 402 if form.Active != nil { 403 w.IsActive = *form.Active 404 } 405 406 if err := webhook.UpdateWebhook(w); err != nil { 407 ctx.Error(http.StatusInternalServerError, "UpdateWebhook", err) 408 return false 409 } 410 return true 411 } 412 413 // DeleteOwnerHook deletes the hook owned by the owner. 414 func DeleteOwnerHook(ctx *context.APIContext, owner *user_model.User, hookID int64) { 415 if err := webhook.DeleteWebhookByOwnerID(ctx, owner.ID, hookID); err != nil { 416 if webhook.IsErrWebhookNotExist(err) { 417 ctx.NotFound() 418 } else { 419 ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOwnerID", err) 420 } 421 return 422 } 423 ctx.Status(http.StatusNoContent) 424 }