diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 4891e43f2..8fa71a81b 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -79,8 +79,8 @@ func GetInclude(field reflect.StructField) string { return getRuleBody(field, "Include(") } -// Validate validate TODO: -func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors { +// Validate populates the data with validation error (if any). +func Validate(errs binding.Errors, data map[string]any, f any, l translation.Locale) binding.Errors { if errs.Len() == 0 { return errs } diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 1f78681da..e89fcef39 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -23,12 +23,14 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/modules/web/middleware" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/forms" webhook_service "code.gitea.io/gitea/services/webhook" + + "gitea.com/go-chi/binding" ) const ( @@ -201,6 +203,29 @@ type webhookParams struct { Meta any } +func WebhookCreate(ctx *context.Context) { + typ := ctx.Params(":type") + handler := webhook_service.GetWebhookHandler(typ) + if handler == nil { + ctx.NotFound("GetWebhookHandler", nil) + return + } + + fields := handler.FormFields(func(form any) { + errs := binding.Bind(ctx.Req, form) + middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error will be checked later in ctx.HasError + }) + createWebhook(ctx, webhookParams{ + Type: typ, + URL: fields.URL, + ContentType: fields.ContentType, + Secret: fields.Secret, + HTTPMethod: fields.HTTPMethod, + WebhookForm: fields.WebhookForm, + Meta: fields.Metadata, + }) +} + func createWebhook(ctx *context.Context, params webhookParams) { ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["PageIsSettingsHooks"] = true @@ -260,6 +285,29 @@ func createWebhook(ctx *context.Context, params webhookParams) { ctx.Redirect(orCtx.Link) } +func WebhookUpdate(ctx *context.Context) { + typ := ctx.Params(":type") + handler := webhook_service.GetWebhookHandler(typ) + if handler == nil { + ctx.NotFound("GetWebhookHandler", nil) + return + } + + fields := handler.FormFields(func(form any) { + errs := binding.Bind(ctx.Req, form) + middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error will be checked later in ctx.HasError + }) + editWebhook(ctx, webhookParams{ + Type: typ, + URL: fields.URL, + ContentType: fields.ContentType, + Secret: fields.Secret, + HTTPMethod: fields.HTTPMethod, + WebhookForm: fields.WebhookForm, + Meta: fields.Metadata, + }) +} + func editWebhook(ctx *context.Context, params webhookParams) { ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook") ctx.Data["PageIsSettingsHooks"] = true @@ -312,304 +360,6 @@ func editWebhook(ctx *context.Context, params webhookParams) { ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID)) } -// ForgejoHooksNewPost response for creating Forgejo webhook -func ForgejoHooksNewPost(ctx *context.Context) { - createWebhook(ctx, forgejoHookParams(ctx)) -} - -// ForgejoHooksEditPost response for editing Forgejo webhook -func ForgejoHooksEditPost(ctx *context.Context) { - editWebhook(ctx, forgejoHookParams(ctx)) -} - -func forgejoHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewWebhookForm) - - contentType := webhook.ContentTypeJSON - if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm { - contentType = webhook.ContentTypeForm - } - - return webhookParams{ - Type: webhook_module.FORGEJO, - URL: form.PayloadURL, - ContentType: contentType, - Secret: form.Secret, - HTTPMethod: form.HTTPMethod, - WebhookForm: form.WebhookForm, - } -} - -// GiteaHooksNewPost response for creating Gitea webhook -func GiteaHooksNewPost(ctx *context.Context) { - createWebhook(ctx, giteaHookParams(ctx)) -} - -// GiteaHooksEditPost response for editing Gitea webhook -func GiteaHooksEditPost(ctx *context.Context) { - editWebhook(ctx, giteaHookParams(ctx)) -} - -func giteaHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewWebhookForm) - - contentType := webhook.ContentTypeJSON - if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm { - contentType = webhook.ContentTypeForm - } - - return webhookParams{ - Type: webhook_module.GITEA, - URL: form.PayloadURL, - ContentType: contentType, - Secret: form.Secret, - HTTPMethod: form.HTTPMethod, - WebhookForm: form.WebhookForm, - } -} - -// GogsHooksNewPost response for creating Gogs webhook -func GogsHooksNewPost(ctx *context.Context) { - createWebhook(ctx, gogsHookParams(ctx)) -} - -// GogsHooksEditPost response for editing Gogs webhook -func GogsHooksEditPost(ctx *context.Context) { - editWebhook(ctx, gogsHookParams(ctx)) -} - -func gogsHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewGogshookForm) - - contentType := webhook.ContentTypeJSON - if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm { - contentType = webhook.ContentTypeForm - } - - return webhookParams{ - Type: webhook_module.GOGS, - URL: form.PayloadURL, - ContentType: contentType, - Secret: form.Secret, - WebhookForm: form.WebhookForm, - } -} - -// DiscordHooksNewPost response for creating Discord webhook -func DiscordHooksNewPost(ctx *context.Context) { - createWebhook(ctx, discordHookParams(ctx)) -} - -// DiscordHooksEditPost response for editing Discord webhook -func DiscordHooksEditPost(ctx *context.Context) { - editWebhook(ctx, discordHookParams(ctx)) -} - -func discordHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewDiscordHookForm) - - return webhookParams{ - Type: webhook_module.DISCORD, - URL: form.PayloadURL, - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - Meta: &webhook_service.DiscordMeta{ - Username: form.Username, - IconURL: form.IconURL, - }, - } -} - -// DingtalkHooksNewPost response for creating Dingtalk webhook -func DingtalkHooksNewPost(ctx *context.Context) { - createWebhook(ctx, dingtalkHookParams(ctx)) -} - -// DingtalkHooksEditPost response for editing Dingtalk webhook -func DingtalkHooksEditPost(ctx *context.Context) { - editWebhook(ctx, dingtalkHookParams(ctx)) -} - -func dingtalkHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewDingtalkHookForm) - - return webhookParams{ - Type: webhook_module.DINGTALK, - URL: form.PayloadURL, - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - } -} - -// TelegramHooksNewPost response for creating Telegram webhook -func TelegramHooksNewPost(ctx *context.Context) { - createWebhook(ctx, telegramHookParams(ctx)) -} - -// TelegramHooksEditPost response for editing Telegram webhook -func TelegramHooksEditPost(ctx *context.Context) { - editWebhook(ctx, telegramHookParams(ctx)) -} - -func telegramHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewTelegramHookForm) - - return webhookParams{ - Type: webhook_module.TELEGRAM, - URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - Meta: &webhook_service.TelegramMeta{ - BotToken: form.BotToken, - ChatID: form.ChatID, - ThreadID: form.ThreadID, - }, - } -} - -// MatrixHooksNewPost response for creating Matrix webhook -func MatrixHooksNewPost(ctx *context.Context) { - createWebhook(ctx, matrixHookParams(ctx)) -} - -// MatrixHooksEditPost response for editing Matrix webhook -func MatrixHooksEditPost(ctx *context.Context) { - editWebhook(ctx, matrixHookParams(ctx)) -} - -func matrixHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewMatrixHookForm) - - return webhookParams{ - Type: webhook_module.MATRIX, - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), - ContentType: webhook.ContentTypeJSON, - HTTPMethod: http.MethodPut, - WebhookForm: form.WebhookForm, - Meta: &webhook_service.MatrixMeta{ - HomeserverURL: form.HomeserverURL, - Room: form.RoomID, - MessageType: form.MessageType, - }, - } -} - -// MSTeamsHooksNewPost response for creating MSTeams webhook -func MSTeamsHooksNewPost(ctx *context.Context) { - createWebhook(ctx, mSTeamsHookParams(ctx)) -} - -// MSTeamsHooksEditPost response for editing MSTeams webhook -func MSTeamsHooksEditPost(ctx *context.Context) { - editWebhook(ctx, mSTeamsHookParams(ctx)) -} - -func mSTeamsHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm) - - return webhookParams{ - Type: webhook_module.MSTEAMS, - URL: form.PayloadURL, - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - } -} - -// SlackHooksNewPost response for creating Slack webhook -func SlackHooksNewPost(ctx *context.Context) { - createWebhook(ctx, slackHookParams(ctx)) -} - -// SlackHooksEditPost response for editing Slack webhook -func SlackHooksEditPost(ctx *context.Context) { - editWebhook(ctx, slackHookParams(ctx)) -} - -func slackHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewSlackHookForm) - - return webhookParams{ - Type: webhook_module.SLACK, - URL: form.PayloadURL, - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - Meta: &webhook_service.SlackMeta{ - Channel: strings.TrimSpace(form.Channel), - Username: form.Username, - IconURL: form.IconURL, - Color: form.Color, - }, - } -} - -// FeishuHooksNewPost response for creating Feishu webhook -func FeishuHooksNewPost(ctx *context.Context) { - createWebhook(ctx, feishuHookParams(ctx)) -} - -// FeishuHooksEditPost response for editing Feishu webhook -func FeishuHooksEditPost(ctx *context.Context) { - editWebhook(ctx, feishuHookParams(ctx)) -} - -func feishuHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewFeishuHookForm) - - return webhookParams{ - Type: webhook_module.FEISHU, - URL: form.PayloadURL, - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - } -} - -// WechatworkHooksNewPost response for creating Wechatwork webhook -func WechatworkHooksNewPost(ctx *context.Context) { - createWebhook(ctx, wechatworkHookParams(ctx)) -} - -// WechatworkHooksEditPost response for editing Wechatwork webhook -func WechatworkHooksEditPost(ctx *context.Context) { - editWebhook(ctx, wechatworkHookParams(ctx)) -} - -func wechatworkHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewWechatWorkHookForm) - - return webhookParams{ - Type: webhook_module.WECHATWORK, - URL: form.PayloadURL, - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - } -} - -// PackagistHooksNewPost response for creating Packagist webhook -func PackagistHooksNewPost(ctx *context.Context) { - createWebhook(ctx, packagistHookParams(ctx)) -} - -// PackagistHooksEditPost response for editing Packagist webhook -func PackagistHooksEditPost(ctx *context.Context) { - editWebhook(ctx, packagistHookParams(ctx)) -} - -func packagistHookParams(ctx *context.Context) webhookParams { - form := web.GetForm(ctx).(*forms.NewPackagistHookForm) - - return webhookParams{ - Type: webhook_module.PACKAGIST, - URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)), - ContentType: webhook.ContentTypeJSON, - WebhookForm: form.WebhookForm, - Meta: &webhook_service.PackagistMeta{ - Username: form.Username, - APIToken: form.APIToken, - PackageURL: form.PackageURL, - }, - } -} - func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { orCtx, err := getOwnerRepoCtx(ctx) if err != nil { diff --git a/routers/web/web.go b/routers/web/web.go index 50a73e9b8..348b9546c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -402,33 +402,11 @@ func registerRoutes(m *web.Route) { addWebhookAddRoutes := func() { m.Get("/{type}/new", repo_setting.WebhooksNew) - m.Post("/forgejo/new", web.Bind(forms.NewWebhookForm{}), repo_setting.ForgejoHooksNewPost) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksNewPost) - m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksNewPost) + m.Post("/{type}/new", repo_setting.WebhookCreate) } addWebhookEditRoutes := func() { - m.Post("/forgejo/{id}", web.Bind(forms.NewWebhookForm{}), repo_setting.ForgejoHooksEditPost) - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksEditPost) - m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksEditPost) + m.Post("/{type}/{id:[0-9]+}", repo_setting.WebhookUpdate) } addSettingsVariablesRoutes := func() { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index f9ebb6eba..0f7665804 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/webhook" "gitea.com/go-chi/binding" ) @@ -279,162 +278,6 @@ func (f WebhookForm) ChooseEvents() bool { return f.Events == "choose_events" } -// NewWebhookForm form for creating web hook -type NewWebhookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - HTTPMethod string `binding:"Required;In(POST,GET)"` - ContentType int `binding:"Required"` - Secret string - WebhookForm -} - -// Validate validates the fields -func (f *NewWebhookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewGogshookForm form for creating gogs hook -type NewGogshookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - ContentType int `binding:"Required"` - Secret string - WebhookForm -} - -// Validate validates the fields -func (f *NewGogshookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewSlackHookForm form for creating slack hook -type NewSlackHookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - Channel string `binding:"Required"` - Username string - IconURL string - Color string - WebhookForm -} - -// Validate validates the fields -func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - if !webhook.IsValidSlackChannel(strings.TrimSpace(f.Channel)) { - errs = append(errs, binding.Error{ - FieldNames: []string{"Channel"}, - Classification: "", - Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"), - }) - } - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewDiscordHookForm form for creating discord hook -type NewDiscordHookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - Username string - IconURL string - WebhookForm -} - -// Validate validates the fields -func (f *NewDiscordHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewDingtalkHookForm form for creating dingtalk hook -type NewDingtalkHookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - WebhookForm -} - -// Validate validates the fields -func (f *NewDingtalkHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewTelegramHookForm form for creating telegram hook -type NewTelegramHookForm struct { - BotToken string `binding:"Required"` - ChatID string `binding:"Required"` - ThreadID string - WebhookForm -} - -// Validate validates the fields -func (f *NewTelegramHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewMatrixHookForm form for creating Matrix hook -type NewMatrixHookForm struct { - HomeserverURL string `binding:"Required;ValidUrl"` - RoomID string `binding:"Required"` - MessageType int - WebhookForm -} - -// Validate validates the fields -func (f *NewMatrixHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewMSTeamsHookForm form for creating MS Teams hook -type NewMSTeamsHookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - WebhookForm -} - -// Validate validates the fields -func (f *NewMSTeamsHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewFeishuHookForm form for creating feishu hook -type NewFeishuHookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - WebhookForm -} - -// Validate validates the fields -func (f *NewFeishuHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewWechatWorkHookForm form for creating wechatwork hook -type NewWechatWorkHookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - WebhookForm -} - -// Validate validates the fields -func (f *NewWechatWorkHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// NewPackagistHookForm form for creating packagist hook -type NewPackagistHookForm struct { - Username string `binding:"Required"` - APIToken string `binding:"Required"` - PackageURL string `binding:"Required;ValidUrl"` - WebhookForm -} - -// Validate validates the fields -func (f *NewPackagistHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - // .___ // | | ______ ________ __ ____ // | |/ ___// ___/ | \_/ __ \ diff --git a/services/webhook/default.go b/services/webhook/default.go index 874668bb4..f725f8a78 100644 --- a/services/webhook/default.go +++ b/services/webhook/default.go @@ -18,6 +18,7 @@ import ( webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/log" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) var _ Handler = defaultHandler{} @@ -35,6 +36,30 @@ func (dh defaultHandler) Type() webhook_module.HookType { func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (defaultHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + HTTPMethod string `binding:"Required;In(POST,GET)"` + ContentType int `binding:"Required"` + Secret string + } + bind(&form) + + contentType := webhook_model.ContentTypeJSON + if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm { + contentType = webhook_model.ContentTypeForm + } + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: contentType, + Secret: form.Secret, + HTTPMethod: form.HTTPMethod, + Metadata: nil, + } +} + func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { switch w.HTTPMethod { case "": diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 4e7bef89f..562e3e86e 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -15,6 +15,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" dingtalk "gitea.com/lunny/dingtalk_webhook" ) @@ -23,6 +24,22 @@ type dingtalkHandler struct{} func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK } func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (dingtalkHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: nil, + } +} type ( // DingtalkPayload represents diff --git a/services/webhook/discord.go b/services/webhook/discord.go index a84aa9d7b..3a0a97386 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -20,12 +20,35 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type discordHandler struct{} func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD } +func (discordHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + Username string + IconURL string + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: &DiscordMeta{ + Username: form.Username, + IconURL: form.IconURL, + }, + } +} + type ( // DiscordEmbedFooter for Embed Footer Structure. DiscordEmbedFooter struct { diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 2c3508e3c..b27acc907 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -13,11 +13,30 @@ import ( "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type feishuHandler struct{} -func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } +func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } + +func (feishuHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: nil, + } +} + func (feishuHandler) Metadata(*webhook_model.Webhook) any { return nil } type ( diff --git a/services/webhook/gogs.go b/services/webhook/gogs.go index 2ecd9e6d0..e23673ed3 100644 --- a/services/webhook/gogs.go +++ b/services/webhook/gogs.go @@ -4,9 +4,36 @@ package webhook import ( + "net/http" + + webhook_model "code.gitea.io/gitea/models/webhook" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type gogsHandler struct{ defaultHandler } func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS } + +func (gogsHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + ContentType int `binding:"Required"` + Secret string + } + bind(&form) + + contentType := webhook_model.ContentTypeJSON + if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm { + contentType = webhook_model.ContentTypeForm + } + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: contentType, + Secret: form.Secret, + HTTPMethod: http.MethodPost, + Metadata: nil, + } +} diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index d1c0ec33f..d04f0f367 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -22,12 +22,40 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type matrixHandler struct{} func (matrixHandler) Type() webhook_module.HookType { return webhook_module.MATRIX } +func (matrixHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + HomeserverURL string `binding:"Required;ValidUrl"` + RoomID string `binding:"Required"` + MessageType int + + // enforce requirement of authorization_header + // (value will still be set in the embedded WebhookForm) + AuthorizationHeader string `binding:"Required"` + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPut, + Metadata: &MatrixMeta{ + HomeserverURL: form.HomeserverURL, + Room: form.RoomID, + MessageType: form.MessageType, + }, + } +} + func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &MatrixMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 6d9070d08..849093e9b 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -15,6 +15,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type msteamsHandler struct{} @@ -22,6 +23,23 @@ type msteamsHandler struct{} func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS } func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (msteamsHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: nil, + } +} + type ( // MSTeamsFact for Fact Structure MSTeamsFact struct { diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index d8d1937e6..d8bf9ea23 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -7,17 +7,42 @@ import ( "context" "fmt" "net/http" + "net/url" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type packagistHandler struct{} func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST } +func (packagistHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + Username string `binding:"Required"` + APIToken string `binding:"Required"` + PackageURL string `binding:"Required;ValidUrl"` + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)), + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: &PackagistMeta{ + Username: form.Username, + APIToken: form.APIToken, + PackageURL: form.PackageURL, + }, + } +} + type ( // PackagistPayload represents a packagist payload // as expected by https://packagist.org/about diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 9b1c367e3..683ef4101 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -17,12 +17,59 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + gitea_context "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + + "gitea.com/go-chi/binding" ) type slackHandler struct{} func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK } +type slackForm struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + Channel string `binding:"Required"` + Username string + IconURL string + Color string +} + +var _ binding.Validator = &slackForm{} + +// Validate implements binding.Validator. +func (s *slackForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := gitea_context.GetWebContext(req) + if !IsValidSlackChannel(strings.TrimSpace(s.Channel)) { + errs = append(errs, binding.Error{ + FieldNames: []string{"Channel"}, + Classification: "", + Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"), + }) + } + return errs +} + +func (slackHandler) FormFields(bind func(any)) FormFields { + var form slackForm + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: &SlackMeta{ + Channel: strings.TrimSpace(form.Channel), + Username: form.Username, + IconURL: form.IconURL, + Color: form.Color, + }, + } +} + // SlackMeta contains the slack metadata type SlackMeta struct { Channel string `json:"channel"` diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 84e2c038f..2ede28dbd 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "net/http" + "net/url" "strings" webhook_model "code.gitea.io/gitea/models/webhook" @@ -15,12 +16,36 @@ import ( "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type telegramHandler struct{} func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM } +func (telegramHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + BotToken string `binding:"Required"` + ChatID string `binding:"Required"` + ThreadID string + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: &TelegramMeta{ + BotToken: form.BotToken, + ChatID: form.ChatID, + ThreadID: form.ThreadID, + }, + } +} + type ( // TelegramPayload represents TelegramPayload struct { diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 62e5374fc..a7802d27d 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -23,14 +23,27 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" "github.com/gobwas/glob" ) type Handler interface { Type() webhook_module.HookType - NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) Metadata(*webhook_model.Webhook) any + // FormFields provides a function to bind the request to the form. + // If form implements the [binding.Validator] interface, the Validate method will be called + FormFields(bind func(form any)) FormFields + NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) +} + +type FormFields struct { + forms.WebhookForm + URL string + ContentType webhook_model.HookContentType + Secret string + HTTPMethod string + Metadata any } var webhookHandlers = []Handler{ diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 184d83308..2ad2acd01 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" ) type wechatworkHandler struct{} @@ -20,6 +21,23 @@ type wechatworkHandler struct{} func (wechatworkHandler) Type() webhook_module.HookType { return webhook_module.WECHATWORK } func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil } +func (wechatworkHandler) FormFields(bind func(any)) FormFields { + var form struct { + forms.WebhookForm + PayloadURL string `binding:"Required;ValidUrl"` + } + bind(&form) + + return FormFields{ + WebhookForm: form.WebhookForm, + URL: form.PayloadURL, + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: nil, + } +} + type ( // WechatworkPayload represents WechatworkPayload struct { diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 3a2fa48c9..47c7a1e43 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -203,8 +203,8 @@ func TestWebhookForms(t *testing.T) { "homeserver_url": "https://matrix.example.com", "room_id": "123", "authorization_header": "Bearer 123456", - // }, map[string]string{ // authorization_header is actually required, but not enforced (yet) - // "authorization_header": "", + }, map[string]string{ + "authorization_header": "", })) t.Run("matrix/optional", testWebhookForms("matrix", session, map[string]string{ "homeserver_url": "https://matrix.example.com",