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