diff --git a/models/webhook.go b/models/webhook.go index 29cfcf6ed..4545fd0e1 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -77,6 +77,13 @@ func IsValidHookContentType(name string) bool { return ok } +func IsValidHookHttpMethod(name string) bool { + if name == "POST" || name == "GET" { + return true + } + return false +} + // HookEvents is a set of web hook events type HookEvents struct { Create bool `json:"create"` @@ -151,6 +158,7 @@ type Webhook struct { Type HookType `xorm:"VARCHAR(16) 'type'"` Meta string `xorm:"TEXT"` // store hook-specific attributes LastStatus HookStatus // Last delivery status + BranchFilter string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -647,16 +655,23 @@ type HookResponse struct { // HookTask represents a hook task. type HookTask struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX"` - HookID int64 - UUID string + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + HookID int64 + UUID string + + Type HookType + URL string `xorm:"TEXT"` + Signature string `xorm:"TEXT"` + HTTPMethod string `xorm:"http_method"` + ContentType HookContentType api.Payloader `xorm:"-"` PayloadContent string `xorm:"TEXT"` EventType HookEventType IsDelivered bool Delivered int64 DeliveredString string `xorm:"-"` + IsSSL bool // History info. IsSucceed bool @@ -708,6 +723,18 @@ func (t *HookTask) simpleMarshalJSON(v interface{}) string { return string(p) } +func GetHookTasksByRepoIDAndHookID(repoID int64, hookID int64, listOptions ListOptions) ([]*HookTask, error) { + if listOptions.Page == 0 { + hookTasks := make([]*HookTask, 0, 5) + return hookTasks, x.Find(&hookTasks, &HookTask{RepoID: repoID, HookID: hookID}) + } + + sess := listOptions.getPaginatedSession() + hookTasks := make([]*HookTask, 0, listOptions.PageSize) + + return hookTasks, sess.Find(&hookTasks, &HookTask{RepoID: repoID, HookID: hookID}) +} + // HookTasks returns a list of hook tasks by given conditions. func HookTasks(hookID int64, page int) ([]*HookTask, error) { tasks := make([]*HookTask, 0, setting.Webhook.PagingNum) diff --git a/modules/convert/convert.go b/modules/convert/convert.go index f655277ff..1b73a1565 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -6,6 +6,7 @@ package convert import ( + "encoding/json" "fmt" "strconv" "strings" @@ -273,6 +274,7 @@ func ToHook(repoLink string, w *models.Webhook) *api.Hook { config := map[string]string{ "url": w.URL, "content_type": w.ContentType.Name(), + "http_method": w.HTTPMethod, } if w.Type == models.SLACK { s := webhook.GetSlackHook(w) @@ -283,14 +285,45 @@ func ToHook(repoLink string, w *models.Webhook) *api.Hook { } return &api.Hook{ - ID: w.ID, - Type: string(w.Type), - URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID), - Active: w.IsActive, - Config: config, - Events: w.EventsArray(), - Updated: w.UpdatedUnix.AsTime(), - Created: w.CreatedUnix.AsTime(), + ID: w.ID, + Type: string(w.Type), + URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID), + Active: w.IsActive, + Config: config, + Events: w.EventsArray(), + Updated: w.UpdatedUnix.AsTime(), + Created: w.CreatedUnix.AsTime(), + BranchFilter: w.HookEvent.BranchFilter, + } +} + +func ToHookTask(t *models.HookTask) *api.HookTask { + config := map[string]string{ + "url": t.URL, + "content_type": t.ContentType.Name(), + "http_method": t.HTTPMethod, + } + + payloadContent := make(map[string]interface{}) + requestContent := make(map[string]interface{}) + responseContent := make(map[string]interface{}) + _ = json.Unmarshal([]byte(t.PayloadContent), &payloadContent) + _ = json.Unmarshal([]byte(t.RequestContent), &requestContent) + _ = json.Unmarshal([]byte(t.ResponseContent), &responseContent) + + return &api.HookTask{ + ID: t.ID, + UUID: t.UUID, + Type: string(t.Type), + Config: config, + PayloadContent: payloadContent, + EventType: string(t.EventType), + IsSSL: t.IsSSL, + IsDelivered: t.IsDelivered, + Delivered: t.Delivered, + IsSucceed: t.IsSucceed, + RequestContent: requestContent, + ResponseContent: responseContent, } } diff --git a/modules/structs/hook.go b/modules/structs/hook.go index e4ec99df4..b985575ae 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -20,18 +20,34 @@ var ( // Hook a hook is a web hook when one repository changed type Hook struct { - ID int64 `json:"id"` - Type string `json:"type"` - URL string `json:"-"` - Config map[string]string `json:"config"` - Events []string `json:"events"` - Active bool `json:"active"` + ID int64 `json:"id"` + Type string `json:"type"` + URL string `json:"-"` + Config map[string]string `json:"config"` + Events []string `json:"events"` + Active bool `json:"active"` + BranchFilter string `json:"branch_filter"` // swagger:strfmt date-time Updated time.Time `json:"updated_at"` // swagger:strfmt date-time Created time.Time `json:"created_at"` } +type HookTask struct { + ID int64 `json:"id"` + UUID string `json:"uuid"` + Type string `json:"type"` + Config map[string]string `json:"config"` + PayloadContent map[string]interface{} `json:"payload_content"` + EventType string `json:"event_type"` + IsSSL bool `json:"is_ssl"` + IsDelivered bool `json:"is_delivered"` + Delivered int64 `json:"delivered"` + IsSucceed bool `json:"is_succeed"` + RequestContent map[string]interface{} `json:"request_info"` + ResponseContent map[string]interface{} `json:"response_content"` +} + // HookList represents a list of API hook. type HookList []*Hook diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 11a3eb922..c8a30ab45 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -747,6 +747,7 @@ func Routes() *web.Route { Patch(bind(api.EditHookOption{}), repo.EditHook). Delete(repo.DeleteHook) m.Post("/tests", context.RepoRefForAPI, repo.TestHook) + m.Get("/hooktasks", repo.ListHookTask) }) }, reqToken(), reqAdmin(), reqWebhooksEnabled()) m.Group("/collaborators", func() { diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index da0a2c501..4212ffe01 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/webhook" + // "code.gitea.io/gitea/" ) // ListHooks list all hooks of a repository @@ -269,3 +270,57 @@ func DeleteHook(ctx *context.APIContext) { } ctx.Status(http.StatusNoContent) } + +func ListHookTask(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/hooks/{id}/hooktasks repository repoGetHookTasks + // --- + // summary: Get a hooktasks *** + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of the hook + // type: integer + // format: int64 + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/HookTaskList" + + hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) + if err != nil { + ctx.NotFound() + return + } + hookTasks, err := models.GetHookTasksByRepoIDAndHookID(ctx.Repo.Repository.ID, hook.ID, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetHookTasksByRepoIDAndHookID", err) + return + } + + apiHookTasks := make([]*api.HookTask, len(hookTasks)) + for i := range hookTasks { + apiHookTasks[i] = convert.ToHookTask(hookTasks[i]) + } + + ctx.JSON(http.StatusOK, &apiHookTasks) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 4804ad9d2..61e8cf35b 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -119,6 +119,13 @@ type swaggerResponseHookList struct { Body []api.Hook `json:"body"` } +// HookTaskList +// swagger:response HookTaskList +type swaggerResponseHookTaskList struct { + // in:body + Body []api.HookTask `json:"body"` +} + // GitHook // swagger:response GitHook type swaggerResponseGitHook struct { diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 5f2be65a2..fe46708d4 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -66,6 +66,10 @@ func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type") return false } + if !models.IsValidHookHttpMethod(form.Config["http_method"]) { + ctx.Error(http.StatusUnprocessableEntity, "", "Invalid http method") + return false + } return true } @@ -212,6 +216,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho if url, ok := form.Config["url"]; ok { w.URL = url } + if secret, ok := form.Config["secret"]; ok { + w.Secret = secret + } + if ct, ok := form.Config["content_type"]; ok { if !models.IsValidHookContentType(ct) { ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type") @@ -220,6 +228,14 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho w.ContentType = models.ToHookContentType(ct) } + if hw, ok := form.Config["http_method"]; ok { + if !models.IsValidHookHttpMethod(hw) { + ctx.Error(http.StatusUnprocessableEntity, "", "Invalid http method") + return false + } + w.HTTPMethod = hw + } + if w.Type == models.SLACK { if channel, ok := form.Config["channel"]; ok { json := jsoniter.ConfigCompatibleWithStandardLibrary @@ -248,16 +264,29 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho w.Create = util.IsStringInSlice(string(models.HookEventCreate), form.Events, true) w.Push = util.IsStringInSlice(string(models.HookEventPush), form.Events, true) w.PullRequest = util.IsStringInSlice(string(models.HookEventPullRequest), form.Events, true) - w.Create = util.IsStringInSlice(string(models.HookEventCreate), form.Events, true) + // w.Create = util.IsStringInSlice(string(models.HookEventCreate), form.Events, true) w.Delete = util.IsStringInSlice(string(models.HookEventDelete), form.Events, true) w.Fork = util.IsStringInSlice(string(models.HookEventFork), form.Events, true) w.Issues = util.IsStringInSlice(string(models.HookEventIssues), form.Events, true) - w.IssueComment = util.IsStringInSlice(string(models.HookEventIssueComment), form.Events, true) + w.Issues = issuesHook(form.Events, "issues_only") + w.IssueAssign = issuesHook(form.Events, string(models.HookEventIssueAssign)) + w.IssueLabel = issuesHook(form.Events, string(models.HookEventIssueLabel)) + w.IssueMilestone = issuesHook(form.Events, string(models.HookEventIssueMilestone)) + w.IssueComment = issuesHook(form.Events, string(models.HookEventIssueComment)) + // w.IssueComment = util.IsStringInSlice(string(models.HookEventIssueComment), form.Events, true) w.Push = util.IsStringInSlice(string(models.HookEventPush), form.Events, true) - w.PullRequest = util.IsStringInSlice(string(models.HookEventPullRequest), form.Events, true) + // w.PullRequest = util.IsStringInSlice(string(models.HookEventPullRequest), form.Events, true) + w.PullRequest = pullHook(form.Events, "pull_request_only") + w.PullRequestAssign = pullHook(form.Events, string(models.HookEventPullRequestAssign)) + w.PullRequestLabel = pullHook(form.Events, string(models.HookEventPullRequestLabel)) + w.PullRequestMilestone = pullHook(form.Events, string(models.HookEventPullRequestMilestone)) + w.PullRequestComment = pullHook(form.Events, string(models.HookEventPullRequestComment)) + w.PullRequestReview = pullHook(form.Events, "pull_request_review") + w.PullRequestSync = pullHook(form.Events, string(models.HookEventPullRequestSync)) w.Repository = util.IsStringInSlice(string(models.HookEventRepository), form.Events, true) w.Release = util.IsStringInSlice(string(models.HookEventRelease), form.Events, true) w.BranchFilter = form.BranchFilter + w.HookEvent.BranchFilter = form.BranchFilter if err := w.UpdateEvent(); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateEvent", err) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index b7d04d484..39c312bb5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4258,6 +4258,59 @@ } } }, + "/repos/{owner}/{repo}/hooks/{id}/hooktasks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a hooktasks", + "operationId": "repoGetHookTasks", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the hook", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/HookTaskList" + } + } + } + }, "/repos/{owner}/{repo}/hooks/{id}/tests": { "post": { "produces": [ @@ -15324,6 +15377,10 @@ "type": "boolean", "x-go-name": "Active" }, + "branch_filter": { + "type": "string", + "x-go-name": "BranchFilter" + }, "config": { "type": "object", "additionalProperties": { @@ -15360,6 +15417,74 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "HookTask": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Config" + }, + "delivered": { + "type": "integer", + "format": "int64", + "x-go-name": "Delivered" + }, + "event_type": { + "type": "string", + "x-go-name": "EventType" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "is_delivered": { + "type": "boolean", + "x-go-name": "IsDelivered" + }, + "is_ssl": { + "type": "boolean", + "x-go-name": "IsSSL" + }, + "is_succeed": { + "type": "boolean", + "x-go-name": "IsSucceed" + }, + "payload_content": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "PayloadContent" + }, + "request_info": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "RequestContent" + }, + "response_content": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "ResponseContent" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "uuid": { + "type": "string", + "x-go-name": "UUID" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Identity": { "description": "Identity for a person's identity like an author or committer", "type": "object", @@ -17938,6 +18063,15 @@ } } }, + "HookTaskList": { + "description": "HookTaskList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/HookTask" + } + } + }, "Issue": { "description": "Issue", "schema": {