mirror of
https://github.com/go-gitea/gitea
synced 2025-02-19 20:07:02 +01:00
This PR created a mock webhook server in the tests and added integration tests for generic webhooks. It also fixes bugs in package webhooks and pull request comment webhooks. This also corrected an error on the package webhook. The previous implementation uses a `User` struct as an organization, now it has been corrected but it will not be consistent with the previous implementation, some fields which not belong to the organization have been removed. Backport #33396 Backport part of #33337
507 lines
16 KiB
Go
507 lines
16 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/optional"
|
|
"code.gitea.io/gitea/modules/secret"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
// ErrWebhookNotExist represents a "WebhookNotExist" kind of error.
|
|
type ErrWebhookNotExist struct {
|
|
ID int64
|
|
}
|
|
|
|
// IsErrWebhookNotExist checks if an error is a ErrWebhookNotExist.
|
|
func IsErrWebhookNotExist(err error) bool {
|
|
_, ok := err.(ErrWebhookNotExist)
|
|
return ok
|
|
}
|
|
|
|
func (err ErrWebhookNotExist) Error() string {
|
|
return fmt.Sprintf("webhook does not exist [id: %d]", err.ID)
|
|
}
|
|
|
|
func (err ErrWebhookNotExist) Unwrap() error {
|
|
return util.ErrNotExist
|
|
}
|
|
|
|
// ErrHookTaskNotExist represents a "HookTaskNotExist" kind of error.
|
|
type ErrHookTaskNotExist struct {
|
|
TaskID int64
|
|
HookID int64
|
|
UUID string
|
|
}
|
|
|
|
// IsErrHookTaskNotExist checks if an error is a ErrHookTaskNotExist.
|
|
func IsErrHookTaskNotExist(err error) bool {
|
|
_, ok := err.(ErrHookTaskNotExist)
|
|
return ok
|
|
}
|
|
|
|
func (err ErrHookTaskNotExist) Error() string {
|
|
return fmt.Sprintf("hook task does not exist [task: %d, hook: %d, uuid: %s]", err.TaskID, err.HookID, err.UUID)
|
|
}
|
|
|
|
func (err ErrHookTaskNotExist) Unwrap() error {
|
|
return util.ErrNotExist
|
|
}
|
|
|
|
// HookContentType is the content type of a web hook
|
|
type HookContentType int
|
|
|
|
const (
|
|
// ContentTypeJSON is a JSON payload for web hooks
|
|
ContentTypeJSON HookContentType = iota + 1
|
|
// ContentTypeForm is an url-encoded form payload for web hook
|
|
ContentTypeForm
|
|
)
|
|
|
|
var hookContentTypes = map[string]HookContentType{
|
|
"json": ContentTypeJSON,
|
|
"form": ContentTypeForm,
|
|
}
|
|
|
|
// ToHookContentType returns HookContentType by given name.
|
|
func ToHookContentType(name string) HookContentType {
|
|
return hookContentTypes[name]
|
|
}
|
|
|
|
// HookTaskCleanupType is the type of cleanup to perform on hook_task
|
|
type HookTaskCleanupType int
|
|
|
|
const (
|
|
// OlderThan hook_task rows will be cleaned up by the age of the row
|
|
OlderThan HookTaskCleanupType = iota
|
|
// PerWebhook hook_task rows will be cleaned up by leaving the most recent deliveries for each webhook
|
|
PerWebhook
|
|
)
|
|
|
|
var hookTaskCleanupTypes = map[string]HookTaskCleanupType{
|
|
"OlderThan": OlderThan,
|
|
"PerWebhook": PerWebhook,
|
|
}
|
|
|
|
// ToHookTaskCleanupType returns HookTaskCleanupType by given name.
|
|
func ToHookTaskCleanupType(name string) HookTaskCleanupType {
|
|
return hookTaskCleanupTypes[name]
|
|
}
|
|
|
|
// Name returns the name of a given web hook's content type
|
|
func (t HookContentType) Name() string {
|
|
switch t {
|
|
case ContentTypeJSON:
|
|
return "json"
|
|
case ContentTypeForm:
|
|
return "form"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// IsValidHookContentType returns true if given name is a valid hook content type.
|
|
func IsValidHookContentType(name string) bool {
|
|
_, ok := hookContentTypes[name]
|
|
return ok
|
|
}
|
|
|
|
// Webhook represents a web hook object.
|
|
type Webhook struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook
|
|
OwnerID int64 `xorm:"INDEX"`
|
|
IsSystemWebhook bool
|
|
URL string `xorm:"url TEXT"`
|
|
HTTPMethod string `xorm:"http_method"`
|
|
ContentType HookContentType
|
|
Secret string `xorm:"TEXT"`
|
|
Events string `xorm:"TEXT"`
|
|
*webhook_module.HookEvent `xorm:"-"`
|
|
IsActive bool `xorm:"INDEX"`
|
|
Type webhook_module.HookType `xorm:"VARCHAR(16) 'type'"`
|
|
Meta string `xorm:"TEXT"` // store hook-specific attributes
|
|
LastStatus webhook_module.HookStatus // Last delivery status
|
|
|
|
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
|
|
HeaderAuthorizationEncrypted string `xorm:"TEXT"`
|
|
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(Webhook))
|
|
}
|
|
|
|
// AfterLoad updates the webhook object upon setting a column
|
|
func (w *Webhook) AfterLoad() {
|
|
w.HookEvent = &webhook_module.HookEvent{}
|
|
if err := json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
|
|
log.Error("Unmarshal[%d]: %v", w.ID, err)
|
|
}
|
|
}
|
|
|
|
// History returns history of webhook by given conditions.
|
|
func (w *Webhook) History(ctx context.Context, page int) ([]*HookTask, error) {
|
|
return HookTasks(ctx, w.ID, page)
|
|
}
|
|
|
|
// UpdateEvent handles conversion from HookEvent to Events.
|
|
func (w *Webhook) UpdateEvent() error {
|
|
data, err := json.Marshal(w.HookEvent)
|
|
w.Events = string(data)
|
|
return err
|
|
}
|
|
|
|
// HasCreateEvent returns true if hook enabled create event.
|
|
func (w *Webhook) HasCreateEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Create)
|
|
}
|
|
|
|
// HasDeleteEvent returns true if hook enabled delete event.
|
|
func (w *Webhook) HasDeleteEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Delete)
|
|
}
|
|
|
|
// HasForkEvent returns true if hook enabled fork event.
|
|
func (w *Webhook) HasForkEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Fork)
|
|
}
|
|
|
|
// HasIssuesEvent returns true if hook enabled issues event.
|
|
func (w *Webhook) HasIssuesEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Issues)
|
|
}
|
|
|
|
// HasIssuesAssignEvent returns true if hook enabled issues assign event.
|
|
func (w *Webhook) HasIssuesAssignEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.IssueAssign)
|
|
}
|
|
|
|
// HasIssuesLabelEvent returns true if hook enabled issues label event.
|
|
func (w *Webhook) HasIssuesLabelEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.IssueLabel)
|
|
}
|
|
|
|
// HasIssuesMilestoneEvent returns true if hook enabled issues milestone event.
|
|
func (w *Webhook) HasIssuesMilestoneEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.IssueMilestone)
|
|
}
|
|
|
|
// HasIssueCommentEvent returns true if hook enabled issue_comment event.
|
|
func (w *Webhook) HasIssueCommentEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.IssueComment)
|
|
}
|
|
|
|
// HasPushEvent returns true if hook enabled push event.
|
|
func (w *Webhook) HasPushEvent() bool {
|
|
return w.PushOnly || w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Push)
|
|
}
|
|
|
|
// HasPullRequestEvent returns true if hook enabled pull request event.
|
|
func (w *Webhook) HasPullRequestEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequest)
|
|
}
|
|
|
|
// HasPullRequestAssignEvent returns true if hook enabled pull request assign event.
|
|
func (w *Webhook) HasPullRequestAssignEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestAssign)
|
|
}
|
|
|
|
// HasPullRequestLabelEvent returns true if hook enabled pull request label event.
|
|
func (w *Webhook) HasPullRequestLabelEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestLabel)
|
|
}
|
|
|
|
// HasPullRequestMilestoneEvent returns true if hook enabled pull request milestone event.
|
|
func (w *Webhook) HasPullRequestMilestoneEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestMilestone)
|
|
}
|
|
|
|
// HasPullRequestCommentEvent returns true if hook enabled pull_request_comment event.
|
|
func (w *Webhook) HasPullRequestCommentEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestComment)
|
|
}
|
|
|
|
// HasPullRequestApprovedEvent returns true if hook enabled pull request review event.
|
|
func (w *Webhook) HasPullRequestApprovedEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
|
}
|
|
|
|
// HasPullRequestRejectedEvent returns true if hook enabled pull request review event.
|
|
func (w *Webhook) HasPullRequestRejectedEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
|
}
|
|
|
|
// HasPullRequestReviewCommentEvent returns true if hook enabled pull request review event.
|
|
func (w *Webhook) HasPullRequestReviewCommentEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
|
}
|
|
|
|
// HasPullRequestSyncEvent returns true if hook enabled pull request sync event.
|
|
func (w *Webhook) HasPullRequestSyncEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestSync)
|
|
}
|
|
|
|
// HasWikiEvent returns true if hook enabled wiki event.
|
|
func (w *Webhook) HasWikiEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvent.Wiki)
|
|
}
|
|
|
|
// HasReleaseEvent returns if hook enabled release event.
|
|
func (w *Webhook) HasReleaseEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Release)
|
|
}
|
|
|
|
// HasRepositoryEvent returns if hook enabled repository event.
|
|
func (w *Webhook) HasRepositoryEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Repository)
|
|
}
|
|
|
|
// HasPackageEvent returns if hook enabled package event.
|
|
func (w *Webhook) HasPackageEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Package)
|
|
}
|
|
|
|
func (w *Webhook) HasStatusEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.Status)
|
|
}
|
|
|
|
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
|
|
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
|
|
return w.SendEverything ||
|
|
(w.ChooseEvents && w.HookEvents.PullRequestReviewRequest)
|
|
}
|
|
|
|
// EventCheckers returns event checkers
|
|
func (w *Webhook) EventCheckers() []struct {
|
|
Has func() bool
|
|
Type webhook_module.HookEventType
|
|
} {
|
|
return []struct {
|
|
Has func() bool
|
|
Type webhook_module.HookEventType
|
|
}{
|
|
{w.HasCreateEvent, webhook_module.HookEventCreate},
|
|
{w.HasDeleteEvent, webhook_module.HookEventDelete},
|
|
{w.HasForkEvent, webhook_module.HookEventFork},
|
|
{w.HasPushEvent, webhook_module.HookEventPush},
|
|
{w.HasIssuesEvent, webhook_module.HookEventIssues},
|
|
{w.HasIssuesAssignEvent, webhook_module.HookEventIssueAssign},
|
|
{w.HasIssuesLabelEvent, webhook_module.HookEventIssueLabel},
|
|
{w.HasIssuesMilestoneEvent, webhook_module.HookEventIssueMilestone},
|
|
{w.HasIssueCommentEvent, webhook_module.HookEventIssueComment},
|
|
{w.HasPullRequestEvent, webhook_module.HookEventPullRequest},
|
|
{w.HasPullRequestAssignEvent, webhook_module.HookEventPullRequestAssign},
|
|
{w.HasPullRequestLabelEvent, webhook_module.HookEventPullRequestLabel},
|
|
{w.HasPullRequestMilestoneEvent, webhook_module.HookEventPullRequestMilestone},
|
|
{w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestComment},
|
|
{w.HasPullRequestApprovedEvent, webhook_module.HookEventPullRequestReviewApproved},
|
|
{w.HasPullRequestRejectedEvent, webhook_module.HookEventPullRequestReviewRejected},
|
|
{w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestReviewComment},
|
|
{w.HasPullRequestSyncEvent, webhook_module.HookEventPullRequestSync},
|
|
{w.HasWikiEvent, webhook_module.HookEventWiki},
|
|
{w.HasRepositoryEvent, webhook_module.HookEventRepository},
|
|
{w.HasReleaseEvent, webhook_module.HookEventRelease},
|
|
{w.HasPackageEvent, webhook_module.HookEventPackage},
|
|
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
|
|
{w.HasStatusEvent, webhook_module.HookEventStatus},
|
|
}
|
|
}
|
|
|
|
// EventsArray returns an array of hook events
|
|
func (w *Webhook) EventsArray() []string {
|
|
events := make([]string, 0, 7)
|
|
|
|
for _, c := range w.EventCheckers() {
|
|
if c.Has() {
|
|
events = append(events, string(c.Type))
|
|
}
|
|
}
|
|
return events
|
|
}
|
|
|
|
// HeaderAuthorization returns the decrypted Authorization header.
|
|
// Not on the reference (*w), to be accessible on WebhooksNew.
|
|
func (w Webhook) HeaderAuthorization() (string, error) {
|
|
if w.HeaderAuthorizationEncrypted == "" {
|
|
return "", nil
|
|
}
|
|
return secret.DecryptSecret(setting.SecretKey, w.HeaderAuthorizationEncrypted)
|
|
}
|
|
|
|
// SetHeaderAuthorization encrypts and sets the Authorization header.
|
|
func (w *Webhook) SetHeaderAuthorization(cleartext string) error {
|
|
if cleartext == "" {
|
|
w.HeaderAuthorizationEncrypted = ""
|
|
return nil
|
|
}
|
|
ciphertext, err := secret.EncryptSecret(setting.SecretKey, cleartext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.HeaderAuthorizationEncrypted = ciphertext
|
|
return nil
|
|
}
|
|
|
|
// CreateWebhook creates a new web hook.
|
|
func CreateWebhook(ctx context.Context, w *Webhook) error {
|
|
w.Type = strings.TrimSpace(w.Type)
|
|
return db.Insert(ctx, w)
|
|
}
|
|
|
|
// CreateWebhooks creates multiple web hooks
|
|
func CreateWebhooks(ctx context.Context, ws []*Webhook) error {
|
|
// xorm returns err "no element on slice when insert" for empty slices.
|
|
if len(ws) == 0 {
|
|
return nil
|
|
}
|
|
for i := 0; i < len(ws); i++ {
|
|
ws[i].Type = strings.TrimSpace(ws[i].Type)
|
|
}
|
|
return db.Insert(ctx, ws)
|
|
}
|
|
|
|
// GetWebhookByID returns webhook of repository by given ID.
|
|
func GetWebhookByID(ctx context.Context, id int64) (*Webhook, error) {
|
|
bean := new(Webhook)
|
|
has, err := db.GetEngine(ctx).ID(id).Get(bean)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrWebhookNotExist{ID: id}
|
|
}
|
|
return bean, nil
|
|
}
|
|
|
|
// GetWebhookByRepoID returns webhook of repository by given ID.
|
|
func GetWebhookByRepoID(ctx context.Context, repoID, id int64) (*Webhook, error) {
|
|
webhook := new(Webhook)
|
|
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(webhook)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrWebhookNotExist{ID: id}
|
|
}
|
|
return webhook, nil
|
|
}
|
|
|
|
// GetWebhookByOwnerID returns webhook of a user or organization by given ID.
|
|
func GetWebhookByOwnerID(ctx context.Context, ownerID, id int64) (*Webhook, error) {
|
|
webhook := new(Webhook)
|
|
has, err := db.GetEngine(ctx).Where("id=? AND owner_id=?", id, ownerID).Get(webhook)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrWebhookNotExist{ID: id}
|
|
}
|
|
return webhook, nil
|
|
}
|
|
|
|
// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
|
|
type ListWebhookOptions struct {
|
|
db.ListOptions
|
|
RepoID int64
|
|
OwnerID int64
|
|
IsActive optional.Option[bool]
|
|
}
|
|
|
|
func (opts ListWebhookOptions) ToConds() builder.Cond {
|
|
cond := builder.NewCond()
|
|
if opts.RepoID != 0 {
|
|
cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID})
|
|
}
|
|
if opts.OwnerID != 0 {
|
|
cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
|
|
}
|
|
if opts.IsActive.Has() {
|
|
cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()})
|
|
}
|
|
return cond
|
|
}
|
|
|
|
// UpdateWebhook updates information of webhook.
|
|
func UpdateWebhook(ctx context.Context, w *Webhook) error {
|
|
_, err := db.GetEngine(ctx).ID(w.ID).AllCols().Update(w)
|
|
return err
|
|
}
|
|
|
|
// UpdateWebhookLastStatus updates last status of webhook.
|
|
func UpdateWebhookLastStatus(ctx context.Context, w *Webhook) error {
|
|
_, err := db.GetEngine(ctx).ID(w.ID).Cols("last_status").Update(w)
|
|
return err
|
|
}
|
|
|
|
// DeleteWebhookByID uses argument bean as query condition,
|
|
// ID must be specified and do not assign unnecessary fields.
|
|
func DeleteWebhookByID(ctx context.Context, id int64) (err error) {
|
|
ctx, committer, err := db.TxContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer committer.Close()
|
|
|
|
if count, err := db.DeleteByID[Webhook](ctx, id); err != nil {
|
|
return err
|
|
} else if count == 0 {
|
|
return ErrWebhookNotExist{ID: id}
|
|
} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: id}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return committer.Commit()
|
|
}
|
|
|
|
// DeleteWebhookByRepoID deletes webhook of repository by given ID.
|
|
func DeleteWebhookByRepoID(ctx context.Context, repoID, id int64) error {
|
|
if _, err := GetWebhookByRepoID(ctx, repoID, id); err != nil {
|
|
return err
|
|
}
|
|
return DeleteWebhookByID(ctx, id)
|
|
}
|
|
|
|
// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
|
|
func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
|
|
if _, err := GetWebhookByOwnerID(ctx, ownerID, id); err != nil {
|
|
return err
|
|
}
|
|
return DeleteWebhookByID(ctx, id)
|
|
}
|