1// Copyright 2017 The Gitea Authors. All rights reserved. 2// Use of this source code is governed by a MIT-style 3// license that can be found in the LICENSE file. 4 5package webhook 6 7import ( 8 "context" 9 "time" 10 11 "code.gitea.io/gitea/models/db" 12 "code.gitea.io/gitea/modules/json" 13 "code.gitea.io/gitea/modules/log" 14 "code.gitea.io/gitea/modules/setting" 15 api "code.gitea.io/gitea/modules/structs" 16 17 gouuid "github.com/google/uuid" 18) 19 20// ___ ___ __ ___________ __ 21// / | \ ____ ____ | | _\__ ___/____ _____| | __ 22// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ / 23// \ Y ( <_> | <_> ) < | | / __ \_\___ \| < 24// \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \ 25// \/ \/ \/ \/ \/ 26 27// HookEventType is the type of an hook event 28type HookEventType string 29 30// Types of hook events 31const ( 32 HookEventCreate HookEventType = "create" 33 HookEventDelete HookEventType = "delete" 34 HookEventFork HookEventType = "fork" 35 HookEventPush HookEventType = "push" 36 HookEventIssues HookEventType = "issues" 37 HookEventIssueAssign HookEventType = "issue_assign" 38 HookEventIssueLabel HookEventType = "issue_label" 39 HookEventIssueMilestone HookEventType = "issue_milestone" 40 HookEventIssueComment HookEventType = "issue_comment" 41 HookEventPullRequest HookEventType = "pull_request" 42 HookEventPullRequestAssign HookEventType = "pull_request_assign" 43 HookEventPullRequestLabel HookEventType = "pull_request_label" 44 HookEventPullRequestMilestone HookEventType = "pull_request_milestone" 45 HookEventPullRequestComment HookEventType = "pull_request_comment" 46 HookEventPullRequestReviewApproved HookEventType = "pull_request_review_approved" 47 HookEventPullRequestReviewRejected HookEventType = "pull_request_review_rejected" 48 HookEventPullRequestReviewComment HookEventType = "pull_request_review_comment" 49 HookEventPullRequestSync HookEventType = "pull_request_sync" 50 HookEventRepository HookEventType = "repository" 51 HookEventRelease HookEventType = "release" 52) 53 54// Event returns the HookEventType as an event string 55func (h HookEventType) Event() string { 56 switch h { 57 case HookEventCreate: 58 return "create" 59 case HookEventDelete: 60 return "delete" 61 case HookEventFork: 62 return "fork" 63 case HookEventPush: 64 return "push" 65 case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone: 66 return "issues" 67 case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone, 68 HookEventPullRequestSync: 69 return "pull_request" 70 case HookEventIssueComment, HookEventPullRequestComment: 71 return "issue_comment" 72 case HookEventPullRequestReviewApproved: 73 return "pull_request_approved" 74 case HookEventPullRequestReviewRejected: 75 return "pull_request_rejected" 76 case HookEventPullRequestReviewComment: 77 return "pull_request_comment" 78 case HookEventRepository: 79 return "repository" 80 case HookEventRelease: 81 return "release" 82 } 83 return "" 84} 85 86// HookRequest represents hook task request information. 87type HookRequest struct { 88 URL string `json:"url"` 89 HTTPMethod string `json:"http_method"` 90 Headers map[string]string `json:"headers"` 91} 92 93// HookResponse represents hook task response information. 94type HookResponse struct { 95 Status int `json:"status"` 96 Headers map[string]string `json:"headers"` 97 Body string `json:"body"` 98} 99 100// HookTask represents a hook task. 101type HookTask struct { 102 ID int64 `xorm:"pk autoincr"` 103 RepoID int64 `xorm:"INDEX"` 104 HookID int64 105 UUID string 106 api.Payloader `xorm:"-"` 107 PayloadContent string `xorm:"TEXT"` 108 EventType HookEventType 109 IsDelivered bool 110 Delivered int64 111 DeliveredString string `xorm:"-"` 112 113 // History info. 114 IsSucceed bool 115 RequestContent string `xorm:"TEXT"` 116 RequestInfo *HookRequest `xorm:"-"` 117 ResponseContent string `xorm:"TEXT"` 118 ResponseInfo *HookResponse `xorm:"-"` 119} 120 121func init() { 122 db.RegisterModel(new(HookTask)) 123} 124 125// BeforeUpdate will be invoked by XORM before updating a record 126// representing this object 127func (t *HookTask) BeforeUpdate() { 128 if t.RequestInfo != nil { 129 t.RequestContent = t.simpleMarshalJSON(t.RequestInfo) 130 } 131 if t.ResponseInfo != nil { 132 t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo) 133 } 134} 135 136// AfterLoad updates the webhook object upon setting a column 137func (t *HookTask) AfterLoad() { 138 t.DeliveredString = time.Unix(0, t.Delivered).Format("2006-01-02 15:04:05 MST") 139 140 if len(t.RequestContent) == 0 { 141 return 142 } 143 144 t.RequestInfo = &HookRequest{} 145 if err := json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil { 146 log.Error("Unmarshal RequestContent[%d]: %v", t.ID, err) 147 } 148 149 if len(t.ResponseContent) > 0 { 150 t.ResponseInfo = &HookResponse{} 151 if err := json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil { 152 log.Error("Unmarshal ResponseContent[%d]: %v", t.ID, err) 153 } 154 } 155} 156 157func (t *HookTask) simpleMarshalJSON(v interface{}) string { 158 p, err := json.Marshal(v) 159 if err != nil { 160 log.Error("Marshal [%d]: %v", t.ID, err) 161 } 162 return string(p) 163} 164 165// HookTasks returns a list of hook tasks by given conditions. 166func HookTasks(hookID int64, page int) ([]*HookTask, error) { 167 tasks := make([]*HookTask, 0, setting.Webhook.PagingNum) 168 return tasks, db.GetEngine(db.DefaultContext). 169 Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum). 170 Where("hook_id=?", hookID). 171 Desc("id"). 172 Find(&tasks) 173} 174 175// CreateHookTask creates a new hook task, 176// it handles conversion from Payload to PayloadContent. 177func CreateHookTask(t *HookTask) error { 178 data, err := t.Payloader.JSONPayload() 179 if err != nil { 180 return err 181 } 182 t.UUID = gouuid.New().String() 183 t.PayloadContent = string(data) 184 return db.Insert(db.DefaultContext, t) 185} 186 187// UpdateHookTask updates information of hook task. 188func UpdateHookTask(t *HookTask) error { 189 _, err := db.GetEngine(db.DefaultContext).ID(t.ID).AllCols().Update(t) 190 return err 191} 192 193// ReplayHookTask copies a hook task to get re-delivered 194func ReplayHookTask(hookID int64, uuid string) (*HookTask, error) { 195 var newTask *HookTask 196 197 err := db.WithTx(func(ctx context.Context) error { 198 task := &HookTask{ 199 HookID: hookID, 200 UUID: uuid, 201 } 202 has, err := db.GetByBean(ctx, task) 203 if err != nil { 204 return err 205 } else if !has { 206 return ErrHookTaskNotExist{ 207 HookID: hookID, 208 UUID: uuid, 209 } 210 } 211 212 newTask = &HookTask{ 213 UUID: gouuid.New().String(), 214 RepoID: task.RepoID, 215 HookID: task.HookID, 216 PayloadContent: task.PayloadContent, 217 EventType: task.EventType, 218 } 219 return db.Insert(ctx, newTask) 220 }) 221 222 return newTask, err 223} 224 225// FindUndeliveredHookTasks represents find the undelivered hook tasks 226func FindUndeliveredHookTasks() ([]*HookTask, error) { 227 tasks := make([]*HookTask, 0, 10) 228 if err := db.GetEngine(db.DefaultContext).Where("is_delivered=?", false).Find(&tasks); err != nil { 229 return nil, err 230 } 231 return tasks, nil 232} 233 234// FindRepoUndeliveredHookTasks represents find the undelivered hook tasks of one repository 235func FindRepoUndeliveredHookTasks(repoID int64) ([]*HookTask, error) { 236 tasks := make([]*HookTask, 0, 5) 237 if err := db.GetEngine(db.DefaultContext).Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil { 238 return nil, err 239 } 240 return tasks, nil 241} 242 243// CleanupHookTaskTable deletes rows from hook_task as needed. 244func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error { 245 log.Trace("Doing: CleanupHookTaskTable") 246 247 if cleanupType == OlderThan { 248 deleteOlderThan := time.Now().Add(-olderThan).UnixNano() 249 deletes, err := db.GetEngine(db.DefaultContext). 250 Where("is_delivered = ? and delivered < ?", true, deleteOlderThan). 251 Delete(new(HookTask)) 252 if err != nil { 253 return err 254 } 255 log.Trace("Deleted %d rows from hook_task", deletes) 256 } else if cleanupType == PerWebhook { 257 hookIDs := make([]int64, 0, 10) 258 err := db.GetEngine(db.DefaultContext).Table("webhook"). 259 Where("id > 0"). 260 Cols("id"). 261 Find(&hookIDs) 262 if err != nil { 263 return err 264 } 265 for _, hookID := range hookIDs { 266 select { 267 case <-ctx.Done(): 268 return db.ErrCancelledf("Before deleting hook_task records for hook id %d", hookID) 269 default: 270 } 271 if err = deleteDeliveredHookTasksByWebhook(hookID, numberToKeep); err != nil { 272 return err 273 } 274 } 275 } 276 log.Trace("Finished: CleanupHookTaskTable") 277 return nil 278} 279 280func deleteDeliveredHookTasksByWebhook(hookID int64, numberDeliveriesToKeep int) error { 281 log.Trace("Deleting hook_task rows for webhook %d, keeping the most recent %d deliveries", hookID, numberDeliveriesToKeep) 282 deliveryDates := make([]int64, 0, 10) 283 err := db.GetEngine(db.DefaultContext).Table("hook_task"). 284 Where("hook_task.hook_id = ? AND hook_task.is_delivered = ? AND hook_task.delivered is not null", hookID, true). 285 Cols("hook_task.delivered"). 286 Join("INNER", "webhook", "hook_task.hook_id = webhook.id"). 287 OrderBy("hook_task.delivered desc"). 288 Limit(1, int(numberDeliveriesToKeep)). 289 Find(&deliveryDates) 290 if err != nil { 291 return err 292 } 293 294 if len(deliveryDates) > 0 { 295 deletes, err := db.GetEngine(db.DefaultContext). 296 Where("hook_id = ? and is_delivered = ? and delivered <= ?", hookID, true, deliveryDates[0]). 297 Delete(new(HookTask)) 298 if err != nil { 299 return err 300 } 301 log.Trace("Deleted %d hook_task rows for webhook %d", deletes, hookID) 302 } else { 303 log.Trace("No hook_task rows to delete for webhook %d", hookID) 304 } 305 306 return nil 307} 308