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