From 0f12ca9222243e05ae992515125f13f2b39ca7da Mon Sep 17 00:00:00 2001
From: Earl Warren <contact@earl-warren.org>
Date: Sat, 14 Jan 2023 10:07:01 +0100
Subject: [PATCH] [BRANDING] define the forgejo webhook type

templates/swagger/v1_json.tmpl updated with `make generate-swagger`

(cherry picked from commit 0f9842fc582057068e907ef2c7b3129848288f56)
---
 models/webhook/webhook.go                     |  1 +
 modules/setting/webhook.go                    |  2 +-
 modules/structs/hook.go                       |  2 +-
 routers/web/repo/webhook.go                   | 28 +++++++++++++
 routers/web/web.go                            |  6 +++
 services/webhook/webhook.go                   |  4 +-
 templates/admin/hook_new.tmpl                 |  7 +++-
 templates/org/settings/hook_new.tmpl          |  6 ++-
 .../repo/settings/webhook/base_list.tmpl      |  5 ++-
 templates/repo/settings/webhook/forgejo.tmpl  | 40 +++++++++++++++++++
 templates/repo/settings/webhook/new.tmpl      |  7 +++-
 templates/swagger/v1_json.tmpl                |  1 +
 tests/integration/links_test.go               | 38 ++++++++++++++++++
 13 files changed, 136 insertions(+), 11 deletions(-)
 create mode 100644 templates/repo/settings/webhook/forgejo.tmpl

diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index aebe0d6e72..108330bca2 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -155,6 +155,7 @@ type HookType = string
 
 // Types of webhooks
 const (
+	FORGEJO    HookType = "forgejo"
 	GITEA      HookType = "gitea"
 	GOGS       HookType = "gogs"
 	SLACK      HookType = "slack"
diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go
index 0bfd7dcb4d..be78066a3d 100644
--- a/modules/setting/webhook.go
+++ b/modules/setting/webhook.go
@@ -36,7 +36,7 @@ func newWebhookService() {
 	Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
 	Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
 	Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString("")
-	Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"}
+	Webhook.Types = []string{"forgejo", "gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"}
 	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
 	Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("")
 	if Webhook.ProxyURL != "" {
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index 8321a15a8f..503f983151 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -40,7 +40,7 @@ type CreateHookOptionConfig map[string]string
 // CreateHookOption options when create a hook
 type CreateHookOption struct {
 	// required: true
-	// enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist
+	// enum: forgejo,dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist
 	Type string `json:"type" binding:"Required"`
 	// required: true
 	Config       CreateHookOptionConfig `json:"config" binding:"Required"`
diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go
index ee980333b7..f37454a42c 100644
--- a/routers/web/repo/webhook.go
+++ b/routers/web/repo/webhook.go
@@ -297,6 +297,34 @@ 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.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))
diff --git a/routers/web/web.go b/routers/web/web.go
index 7757e9c30c..3f41592a17 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -534,6 +534,7 @@ func RegisterRoutes(m *web.Route) {
 				m.Get("", repo.WebHooksEdit)
 				m.Post("/replay/{uuid}", repo.ReplayWebhook)
 			})
+			m.Post("/forgejo/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksEditPost)
 			m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksEditPost)
 			m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
 			m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
@@ -549,6 +550,7 @@ func RegisterRoutes(m *web.Route) {
 
 		m.Group("/{configType:default-hooks|system-hooks}", func() {
 			m.Get("/{type}/new", repo.WebhooksNew)
+			m.Post("/forgejo/new", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksNewPost)
 			m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
 			m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
 			m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
@@ -712,6 +714,7 @@ func RegisterRoutes(m *web.Route) {
 					m.Get("", org.Webhooks)
 					m.Post("/delete", org.DeleteWebhook)
 					m.Get("/{type}/new", repo.WebhooksNew)
+					m.Post("/forgejo/new", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksNewPost)
 					m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
 					m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
 					m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
@@ -726,6 +729,7 @@ func RegisterRoutes(m *web.Route) {
 						m.Get("", repo.WebHooksEdit)
 						m.Post("/replay/{uuid}", repo.ReplayWebhook)
 					})
+					m.Post("/forgejo/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksEditPost)
 					m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksEditPost)
 					m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
 					m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
@@ -835,6 +839,7 @@ func RegisterRoutes(m *web.Route) {
 				m.Get("", repo.Webhooks)
 				m.Post("/delete", repo.DeleteWebhook)
 				m.Get("/{type}/new", repo.WebhooksNew)
+				m.Post("/forgejo/new", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksNewPost)
 				m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
 				m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
 				m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
@@ -851,6 +856,7 @@ func RegisterRoutes(m *web.Route) {
 					m.Post("/test", repo.TestWebhook)
 					m.Post("/replay/{uuid}", repo.ReplayWebhook)
 				})
+				m.Post("/forgejo/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksEditPost)
 				m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksEditPost)
 				m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
 				m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index 342e764f4d..5205cbcdc8 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -74,7 +74,7 @@ func RegisterWebhook(name string, webhook *webhook) {
 
 // IsValidHookTaskType returns true if a webhook registered
 func IsValidHookTaskType(name string) bool {
-	if name == webhook_model.GITEA || name == webhook_model.GOGS {
+	if name == webhook_model.FORGEJO || name == webhook_model.GITEA || name == webhook_model.GOGS {
 		return true
 	}
 	_, ok := webhooks[name]
@@ -170,7 +170,7 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
 	// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
 	// Integration webhooks (e.g. drone) still receive the required data.
 	if pushEvent, ok := p.(*api.PushPayload); ok &&
-		w.Type != webhook_model.GITEA && w.Type != webhook_model.GOGS &&
+		w.Type != webhook_model.FORGEJO && w.Type != webhook_model.GITEA && w.Type != webhook_model.GOGS &&
 		len(pushEvent.Commits) == 0 {
 		return nil
 	}
diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl
index c5196fce4e..f197dda55f 100644
--- a/templates/admin/hook_new.tmpl
+++ b/templates/admin/hook_new.tmpl
@@ -14,8 +14,10 @@
 				{{.locale.Tr "admin.defaulthooks.update_webhook"}}
 			{{end}}
 			<div class="ui right">
-				{{if eq .HookType "gitea"}}
-					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg">
+				{{if eq .HookType "forgejo"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+				{{else if eq .HookType "gitea"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
 				{{else if eq .HookType "gogs"}}
 					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico">
 				{{else if eq .HookType "slack"}}
@@ -40,6 +42,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
+			{{template "repo/settings/webhook/forgejo" .}}
 			{{template "repo/settings/webhook/gitea" .}}
 			{{template "repo/settings/webhook/gogs" .}}
 			{{template "repo/settings/webhook/slack" .}}
diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl
index 7308cafa20..a30ed6dfd9 100644
--- a/templates/org/settings/hook_new.tmpl
+++ b/templates/org/settings/hook_new.tmpl
@@ -9,8 +9,10 @@
 				<h4 class="ui top attached header">
 					{{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}}
 					<div class="ui right">
-						{{if eq .HookType "gitea"}}
-							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg">
+						{{if eq .HookType "forgejo"}}
+							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+						{{else if eq .HookType "gitea"}}
+							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
 						{{else if eq .HookType "gogs"}}
 							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico">
 						{{else if eq .HookType "slack"}}
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index fa68e12ce9..c9f0a3bd7b 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -4,8 +4,11 @@
 		<div class="ui floating1 jump dropdown">
 			<div class="ui primary tiny button">{{.locale.Tr "repo.settings.add_webhook"}}</div>
 			<div class="menu">
+				<a class="item" href="{{.BaseLinkNew}}/forgejo/new">
+					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/forgejo.svg">{{.locale.Tr "repo.settings.web_hook_name_forgejo"}}
+				</a>
 				<a class="item" href="{{.BaseLinkNew}}/gitea/new">
-					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/gitea.svg">{{.locale.Tr "repo.settings.web_hook_name_gitea"}}
+					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/gitea-original.svg">{{.locale.Tr "repo.settings.web_hook_name_gitea"}}
 				</a>
 				<a class="item" href="{{.BaseLinkNew}}/gogs/new">
 					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/gogs.ico">{{.locale.Tr "repo.settings.web_hook_name_gogs"}}
diff --git a/templates/repo/settings/webhook/forgejo.tmpl b/templates/repo/settings/webhook/forgejo.tmpl
new file mode 100644
index 0000000000..0d733f3362
--- /dev/null
+++ b/templates/repo/settings/webhook/forgejo.tmpl
@@ -0,0 +1,40 @@
+{{if eq .HookType "forgejo"}}
+	<p>{{.locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.io/en-us/webhooks/" (.locale.Tr "repo.settings.web_hook_name_forgejo") | Str2html}}</p>
+	<form class="ui form" action="{{.BaseLink}}/forgejo/{{or .Webhook.ID "new"}}" method="post">
+		{{template "base/disable_form_autofill"}}
+		{{.CsrfTokenHtml}}
+		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
+			<label for="payload_url">{{.locale.Tr "repo.settings.payload_url"}}</label>
+			<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
+		</div>
+		<div class="field">
+			<label>{{.locale.Tr "repo.settings.http_method"}}</label>
+			<div class="ui selection dropdown">
+				<input type="hidden" id="http_method" name="http_method" value="{{if .Webhook.HTTPMethod}}{{.Webhook.HTTPMethod}}{{else}}POST{{end}}">
+				<div class="default text"></div>
+				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+				<div class="menu">
+					<div class="item" data-value="POST">POST</div>
+					<div class="item" data-value="GET">GET</div>
+				</div>
+			</div>
+		</div>
+		<div class="field">
+			<label>{{.locale.Tr "repo.settings.content_type"}}</label>
+			<div class="ui selection dropdown">
+				<input type="hidden" id="content_type" name="content_type" value="{{if .Webhook.ContentType}}{{.Webhook.ContentType}}{{else}}1{{end}}">
+				<div class="default text"></div>
+				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+				<div class="menu">
+					<div class="item" data-value="1">application/json</div>
+					<div class="item" data-value="2">application/x-www-form-urlencoded</div>
+				</div>
+			</div>
+		</div>
+		<div class="field {{if .Err_Secret}}error{{end}}">
+			<label for="secret">{{.locale.Tr "repo.settings.secret"}}</label>
+			<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off">
+		</div>
+		{{template "repo/settings/webhook/settings" .}}
+	</form>
+{{end}}
diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl
index ea5a3eca3c..066d0045d0 100644
--- a/templates/repo/settings/webhook/new.tmpl
+++ b/templates/repo/settings/webhook/new.tmpl
@@ -7,8 +7,10 @@
 		<h4 class="ui top attached header">
 			{{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}}
 			<div class="ui right">
-				{{if eq .HookType "gitea"}}
-					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg">
+				{{if eq .HookType "forgejo"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+				{{else if eq .HookType "gitea"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
 				{{else if eq .HookType "gogs"}}
 					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico">
 				{{else if eq .HookType "slack"}}
@@ -33,6 +35,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
+			{{template "repo/settings/webhook/forgejo" .}}
 			{{template "repo/settings/webhook/gitea" .}}
 			{{template "repo/settings/webhook/gogs" .}}
 			{{template "repo/settings/webhook/slack" .}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 9ce8a02e9d..7080e7e794 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -14465,6 +14465,7 @@
         "type": {
           "type": "string",
           "enum": [
+            "forgejo",
             "dingtalk",
             "discord",
             "gitea",
diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go
index 4eb29f0cee..d40419a018 100644
--- a/tests/integration/links_test.go
+++ b/tests/integration/links_test.go
@@ -174,3 +174,41 @@ func TestLinksLogin(t *testing.T) {
 
 	testLinksAsUser("user2", t)
 }
+
+func TestRedirectsWebhooks(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	//
+	// A redirect means the route exists but not if it performs as intended.
+	//
+	for _, kind := range []string{"forgejo", "gitea"} {
+		{
+			redirects := map[string]string{
+				"/user2/repo1/settings/hooks/" + kind + "/new": "/user/login",
+				"/admin/system-hooks/" + kind + "/new":         "/user/login",
+				"/admin/default-hooks/" + kind + "/new":        "/user/login",
+			}
+			for link, redirectLink := range redirects {
+				req := NewRequest(t, "GET", link)
+				resp := MakeRequest(t, req, http.StatusSeeOther)
+				assert.EqualValues(t, path.Join(setting.AppSubURL, redirectLink), test.RedirectURL(resp))
+			}
+		}
+
+		{
+			redirects := map[string]string{
+				"/user2/repo1/settings/hooks/" + kind + "/new": "/",
+				"/admin/system-hooks/" + kind + "/new":         "/",
+				"/admin/default-hooks/" + kind + "/new":        "/",
+
+				"/user2/repo1/settings/hooks/" + kind + "/1": "/",
+				"/admin/hooks/" + kind + "/1":                "/",
+			}
+			for link, redirectLink := range redirects {
+				req := NewRequest(t, "POST", link)
+				resp := MakeRequest(t, req, http.StatusSeeOther)
+				assert.EqualValues(t, path.Join(setting.AppSubURL, redirectLink), test.RedirectURL(resp))
+			}
+		}
+	}
+}