1// Copyright 2019 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package webhook
5
6import (
7 "context"
8 "errors"
9 "fmt"
10 "html/template"
11 "net/http"
12 "strings"
13
14 "forgejo.org/models/db"
15 repo_model "forgejo.org/models/repo"
16 user_model "forgejo.org/models/user"
17 webhook_model "forgejo.org/models/webhook"
18 "forgejo.org/modules/git"
19 "forgejo.org/modules/graceful"
20 "forgejo.org/modules/log"
21 "forgejo.org/modules/optional"
22 "forgejo.org/modules/queue"
23 "forgejo.org/modules/setting"
24 api "forgejo.org/modules/structs"
25 "forgejo.org/modules/util"
26 webhook_module "forgejo.org/modules/webhook"
27 "forgejo.org/services/forms"
28 "forgejo.org/services/webhook/sourcehut"
29
30 "github.com/gobwas/glob"
31)
32
33type Handler interface {
34 Type() webhook_module.HookType
35 Metadata(*webhook_model.Webhook) any
36 // UnmarshalForm provides a function to bind the request to the form.
37 // If form implements the [binding.Validator] interface, the Validate method will be called
38 UnmarshalForm(bind func(form any)) forms.WebhookForm
39 NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
40 Icon(size int) template.HTML
41}
42
43var webhookHandlers = []Handler{
44 defaultHandler{true},
45 defaultHandler{false},
46 gogsHandler{},
47
48 slackHandler{},
49 discordHandler{},
50 dingtalkHandler{},
51 telegramHandler{},
52 msteamsHandler{},
53 feishuHandler{},
54 matrixHandler{},
55 wechatworkHandler{},
56 packagistHandler{},
57 sourcehut.BuildsHandler{},
58}
59
60// GetWebhookHandler return the handler for a given webhook type (nil if not found)
61func GetWebhookHandler(name webhook_module.HookType) Handler {
62 for _, h := range webhookHandlers {
63 if h.Type() == name {
64 return h
65 }
66 }
67 return nil
68}
69
70// List provides a list of the supported webhooks
71func List() []Handler {
72 return webhookHandlers
73}
74
75// IsValidHookTaskType returns true if a webhook registered
76func IsValidHookTaskType(name string) bool {
77 return GetWebhookHandler(name) != nil
78}
79
80// hookQueue is a global queue of web hooks
81var hookQueue *queue.WorkerPoolQueue[int64]
82
83// getPayloadBranch returns branch for hook event, if applicable.
84func getPayloadBranch(p api.Payloader) string {
85 var ref string
86 switch pp := p.(type) {
87 case *api.CreatePayload:
88 ref = pp.Ref
89 case *api.DeletePayload:
90 ref = pp.Ref
91 case *api.PushPayload:
92 ref = pp.Ref
93 }
94 if strings.HasPrefix(ref, git.BranchPrefix) {
95 return ref[len(git.BranchPrefix):]
96 }
97 return ""
98}
99
100// EventSource represents the source of a webhook action. Repository and/or Owner must be set.
101type EventSource struct {
102 Repository *repo_model.Repository
103 Owner *user_model.User
104}
105
106// handle delivers hook tasks
107func handler(items ...int64) []int64 {
108 ctx := graceful.GetManager().HammerContext()
109
110 for _, taskID := range items {
111 task, err := webhook_model.GetHookTaskByID(ctx, taskID)
112 if err != nil {
113 if errors.Is(err, util.ErrNotExist) {
114 log.Warn("GetHookTaskByID[%d] warn: %v", taskID, err)
115 } else {
116 log.Error("GetHookTaskByID[%d] failed: %v", taskID, err)
117 }
118 continue
119 }
120
121 if task.IsDelivered {
122 // Already delivered in the meantime
123 log.Trace("Task[%d] has already been delivered", task.ID)
124 continue
125 }
126
127 if err := Deliver(ctx, task); err != nil {
128 log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err)
129 }
130 }
131
132 return nil
133}
134
135func enqueueHookTask(taskID int64) error {
136 err := hookQueue.Push(taskID)
137 if err != nil && err != queue.ErrAlreadyInQueue {
138 return err
139 }
140 return nil
141}
142
143func checkBranch(w *webhook_model.Webhook, branch string) bool {
144 if w.BranchFilter == "" || w.BranchFilter == "*" {
145 return true
146 }
147
148 g, err := glob.Compile(w.BranchFilter)
149 if err != nil {
150 // should not really happen as BranchFilter is validated
151 log.Error("CheckBranch failed: %s", err)
152 return false
153 }
154
155 return g.Match(branch)
156}
157
158// PrepareWebhook creates a hook task and enqueues it for processing.
159// The payload is saved as-is. The adjustments depending on the webhook type happen
160// right before delivery, in the [Deliver] method.
161func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
162 // Skip sending if webhooks are disabled.
163 if setting.DisableWebhooks {
164 return nil
165 }
166
167 for _, e := range w.EventCheckers() {
168 if event == e.Type {
169 if !e.Has() {
170 return nil
171 }
172
173 break
174 }
175 }
176
177 // Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
178 // Integration webhooks (e.g. drone) still receive the required data.
179 if pushEvent, ok := p.(*api.PushPayload); ok &&
180 w.Type != webhook_module.FORGEJO && w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS &&
181 len(pushEvent.Commits) == 0 {
182 return nil
183 }
184
185 // If payload has no associated branch (e.g. it's a new tag, issue, etc.),
186 // branch filter has no effect.
187 if branch := getPayloadBranch(p); branch != "" {
188 if !checkBranch(w, branch) {
189 log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter)
190 return nil
191 }
192 }
193
194 payload, err := p.JSONPayload()
195 if err != nil {
196 return fmt.Errorf("JSONPayload for %s: %w", event, err)
197 }
198
199 task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
200 HookID: w.ID,
201 PayloadContent: string(payload),
202 EventType: event,
203 PayloadVersion: 2,
204 })
205 if err != nil {
206 return fmt.Errorf("CreateHookTask for %s: %w", event, err)
207 }
208
209 return enqueueHookTask(task.ID)
210}
211
212// PrepareWebhooks adds new webhooks to task queue for given payload.
213func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error {
214 owner := source.Owner
215
216 var ws []*webhook_model.Webhook
217
218 if source.Repository != nil {
219 repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
220 RepoID: source.Repository.ID,
221 IsActive: optional.Some(true),
222 })
223 if err != nil {
224 return fmt.Errorf("ListWebhooksByOpts: %w", err)
225 }
226 ws = append(ws, repoHooks...)
227
228 owner = source.Repository.MustOwner(ctx)
229 }
230
231 // append additional webhooks of a user or organization
232 if owner != nil {
233 ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
234 OwnerID: owner.ID,
235 IsActive: optional.Some(true),
236 })
237 if err != nil {
238 return fmt.Errorf("ListWebhooksByOpts: %w", err)
239 }
240 ws = append(ws, ownerHooks...)
241 }
242
243 // Add any admin-defined system webhooks
244 systemHooks, err := webhook_model.GetSystemWebhooks(ctx, true)
245 if err != nil {
246 return fmt.Errorf("GetSystemWebhooks: %w", err)
247 }
248 ws = append(ws, systemHooks...)
249
250 if len(ws) == 0 {
251 return nil
252 }
253
254 for _, w := range ws {
255 if err := PrepareWebhook(ctx, w, event, p); err != nil {
256 return err
257 }
258 }
259 return nil
260}
261
262// ReplayHookTask replays a webhook task
263func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error {
264 task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid)
265 if err != nil {
266 return err
267 }
268
269 return enqueueHookTask(task.ID)
270}