gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package webhook
  4. import (
  5. "context"
  6. "errors"
  7. "time"
  8. "code.gitea.io/gitea/models/db"
  9. "code.gitea.io/gitea/modules/json"
  10. "code.gitea.io/gitea/modules/log"
  11. "code.gitea.io/gitea/modules/setting"
  12. "code.gitea.io/gitea/modules/timeutil"
  13. webhook_module "code.gitea.io/gitea/modules/webhook"
  14. gouuid "github.com/google/uuid"
  15. "xorm.io/builder"
  16. )
  17. // ___ ___ __ ___________ __
  18. // / | \ ____ ____ | | _\__ ___/____ _____| | __
  19. // / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
  20. // \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
  21. // \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
  22. // \/ \/ \/ \/ \/
  23. // HookRequest represents hook task request information.
  24. type HookRequest struct {
  25. URL string `json:"url"`
  26. HTTPMethod string `json:"http_method"`
  27. Headers map[string]string `json:"headers"`
  28. Body string `json:"body"`
  29. }
  30. // HookResponse represents hook task response information.
  31. type HookResponse struct {
  32. Status int `json:"status"`
  33. Headers map[string]string `json:"headers"`
  34. Body string `json:"body"`
  35. }
  36. // HookTask represents a hook task.
  37. type HookTask struct {
  38. ID int64 `xorm:"pk autoincr"`
  39. HookID int64 `xorm:"index"`
  40. UUID string `xorm:"unique"`
  41. PayloadContent string `xorm:"LONGTEXT"`
  42. // PayloadVersion number to allow for smooth version upgrades:
  43. // - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL
  44. // - PayloadVersion 2: PayloadContent contains the original event
  45. PayloadVersion int `xorm:"DEFAULT 1"`
  46. EventType webhook_module.HookEventType
  47. IsDelivered bool
  48. Delivered timeutil.TimeStampNano
  49. // History info.
  50. IsSucceed bool
  51. RequestContent string `xorm:"LONGTEXT"`
  52. RequestInfo *HookRequest `xorm:"-"`
  53. ResponseContent string `xorm:"LONGTEXT"`
  54. ResponseInfo *HookResponse `xorm:"-"`
  55. }
  56. func init() {
  57. db.RegisterModel(new(HookTask))
  58. }
  59. // BeforeUpdate will be invoked by XORM before updating a record
  60. // representing this object
  61. func (t *HookTask) BeforeUpdate() {
  62. if t.RequestInfo != nil {
  63. t.RequestContent = t.simpleMarshalJSON(t.RequestInfo)
  64. }
  65. if t.ResponseInfo != nil {
  66. t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo)
  67. }
  68. }
  69. // AfterLoad updates the webhook object upon setting a column
  70. func (t *HookTask) AfterLoad() {
  71. if len(t.RequestContent) == 0 {
  72. return
  73. }
  74. t.RequestInfo = &HookRequest{}
  75. if err := json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
  76. log.Error("Unmarshal RequestContent[%d]: %v", t.ID, err)
  77. }
  78. if len(t.ResponseContent) > 0 {
  79. t.ResponseInfo = &HookResponse{}
  80. if err := json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
  81. log.Error("Unmarshal ResponseContent[%d]: %v", t.ID, err)
  82. }
  83. }
  84. }
  85. func (t *HookTask) simpleMarshalJSON(v any) string {
  86. p, err := json.Marshal(v)
  87. if err != nil {
  88. log.Error("Marshal [%d]: %v", t.ID, err)
  89. }
  90. return string(p)
  91. }
  92. // HookTasks returns a list of hook tasks by given conditions.
  93. func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error) {
  94. tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
  95. return tasks, db.GetEngine(ctx).
  96. Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).
  97. Where("hook_id=?", hookID).
  98. Desc("id").
  99. Find(&tasks)
  100. }
  101. // CreateHookTask creates a new hook task,
  102. // it handles conversion from Payload to PayloadContent.
  103. func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) {
  104. t.UUID = gouuid.New().String()
  105. if t.Delivered == 0 {
  106. t.Delivered = timeutil.TimeStampNanoNow()
  107. }
  108. if t.PayloadVersion == 0 {
  109. return nil, errors.New("missing HookTask.PayloadVersion")
  110. }
  111. return t, db.Insert(ctx, t)
  112. }
  113. func GetHookTaskByID(ctx context.Context, id int64) (*HookTask, error) {
  114. t := &HookTask{}
  115. has, err := db.GetEngine(ctx).ID(id).Get(t)
  116. if err != nil {
  117. return nil, err
  118. }
  119. if !has {
  120. return nil, ErrHookTaskNotExist{
  121. TaskID: id,
  122. }
  123. }
  124. return t, nil
  125. }
  126. // UpdateHookTask updates information of hook task.
  127. func UpdateHookTask(ctx context.Context, t *HookTask) error {
  128. _, err := db.GetEngine(ctx).ID(t.ID).AllCols().Update(t)
  129. return err
  130. }
  131. // ReplayHookTask copies a hook task to get re-delivered
  132. func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
  133. task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})
  134. if err != nil {
  135. return nil, err
  136. } else if !exist {
  137. return nil, ErrHookTaskNotExist{
  138. HookID: hookID,
  139. UUID: uuid,
  140. }
  141. }
  142. return CreateHookTask(ctx, &HookTask{
  143. HookID: task.HookID,
  144. PayloadContent: task.PayloadContent,
  145. EventType: task.EventType,
  146. PayloadVersion: task.PayloadVersion,
  147. })
  148. }
  149. // FindUndeliveredHookTaskIDs will find the next 100 undelivered hook tasks with ID greater than the provided lowerID
  150. func FindUndeliveredHookTaskIDs(ctx context.Context, lowerID int64) ([]int64, error) {
  151. const batchSize = 100
  152. tasks := make([]int64, 0, batchSize)
  153. return tasks, db.GetEngine(ctx).
  154. Select("id").
  155. Table(new(HookTask)).
  156. Where("is_delivered=?", false).
  157. And("id > ?", lowerID).
  158. Asc("id").
  159. Limit(batchSize).
  160. Find(&tasks)
  161. }
  162. func MarkTaskDelivered(ctx context.Context, task *HookTask) (bool, error) {
  163. count, err := db.GetEngine(ctx).ID(task.ID).Where("is_delivered = ?", false).Cols("is_delivered").Update(&HookTask{
  164. ID: task.ID,
  165. IsDelivered: true,
  166. })
  167. return count != 0, err
  168. }
  169. // CleanupHookTaskTable deletes rows from hook_task as needed.
  170. func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error {
  171. log.Trace("Doing: CleanupHookTaskTable")
  172. switch cleanupType {
  173. case OlderThan:
  174. deleteOlderThan := time.Now().Add(-olderThan).UnixNano()
  175. deletes, err := db.GetEngine(ctx).
  176. Where("is_delivered = ? and delivered < ?", true, deleteOlderThan).
  177. Delete(new(HookTask))
  178. if err != nil {
  179. return err
  180. }
  181. log.Trace("Deleted %d rows from hook_task", deletes)
  182. case PerWebhook:
  183. hookIDs := make([]int64, 0, 10)
  184. err := db.GetEngine(ctx).
  185. Table("webhook").
  186. Where("id > 0").
  187. Cols("id").
  188. Find(&hookIDs)
  189. if err != nil {
  190. return err
  191. }
  192. for _, hookID := range hookIDs {
  193. select {
  194. case <-ctx.Done():
  195. return db.ErrCancelledf("Before deleting hook_task records for hook id %d", hookID)
  196. default:
  197. }
  198. if err = deleteDeliveredHookTasksByWebhook(ctx, hookID, numberToKeep); err != nil {
  199. return err
  200. }
  201. }
  202. }
  203. log.Trace("Finished: CleanupHookTaskTable")
  204. return nil
  205. }
  206. func deleteDeliveredHookTasksByWebhook(ctx context.Context, hookID int64, numberDeliveriesToKeep int) error {
  207. log.Trace("Deleting hook_task rows for webhook %d, keeping the most recent %d deliveries", hookID, numberDeliveriesToKeep)
  208. deliveryDates := make([]int64, 0, 10)
  209. err := db.GetEngine(ctx).Table("hook_task").
  210. Where("hook_task.hook_id = ? AND hook_task.is_delivered = ? AND hook_task.delivered is not null", hookID, true).
  211. Cols("hook_task.delivered").
  212. Join("INNER", "webhook", "hook_task.hook_id = webhook.id").
  213. OrderBy("hook_task.delivered desc").
  214. Limit(1, numberDeliveriesToKeep).
  215. Find(&deliveryDates)
  216. if err != nil {
  217. return err
  218. }
  219. if len(deliveryDates) > 0 {
  220. deletes, err := db.GetEngine(ctx).
  221. Where("hook_id = ? and is_delivered = ? and delivered <= ?", hookID, true, deliveryDates[0]).
  222. Delete(new(HookTask))
  223. if err != nil {
  224. return err
  225. }
  226. log.Trace("Deleted %d hook_task rows for webhook %d", deletes, hookID)
  227. } else {
  228. log.Trace("No hook_task rows to delete for webhook %d", hookID)
  229. }
  230. return nil
  231. }