code.gitea.io/gitea@v1.22.3/routers/api/v1/utils/hook.go (about)

     1  // Copyright 2016 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package utils
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	user_model "code.gitea.io/gitea/models/user"
    14  	"code.gitea.io/gitea/models/webhook"
    15  	"code.gitea.io/gitea/modules/json"
    16  	"code.gitea.io/gitea/modules/setting"
    17  	api "code.gitea.io/gitea/modules/structs"
    18  	"code.gitea.io/gitea/modules/util"
    19  	webhook_module "code.gitea.io/gitea/modules/webhook"
    20  	"code.gitea.io/gitea/services/context"
    21  	webhook_service "code.gitea.io/gitea/services/webhook"
    22  )
    23  
    24  // ListOwnerHooks lists the webhooks of the provided owner
    25  func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
    26  	opts := &webhook.ListWebhookOptions{
    27  		ListOptions: GetListOptions(ctx),
    28  		OwnerID:     owner.ID,
    29  	}
    30  
    31  	hooks, count, err := db.FindAndCount[webhook.Webhook](ctx, opts)
    32  	if err != nil {
    33  		ctx.InternalServerError(err)
    34  		return
    35  	}
    36  
    37  	apiHooks := make([]*api.Hook, len(hooks))
    38  	for i, hook := range hooks {
    39  		apiHooks[i], err = webhook_service.ToHook(owner.HomeLink(), hook)
    40  		if err != nil {
    41  			ctx.InternalServerError(err)
    42  			return
    43  		}
    44  	}
    45  
    46  	ctx.SetTotalCountHeader(count)
    47  	ctx.JSON(http.StatusOK, apiHooks)
    48  }
    49  
    50  // GetOwnerHook gets an user or organization webhook. Errors are written to ctx.
    51  func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webhook, error) {
    52  	w, err := webhook.GetWebhookByOwnerID(ctx, ownerID, hookID)
    53  	if err != nil {
    54  		if webhook.IsErrWebhookNotExist(err) {
    55  			ctx.NotFound()
    56  		} else {
    57  			ctx.Error(http.StatusInternalServerError, "GetWebhookByOwnerID", err)
    58  		}
    59  		return nil, err
    60  	}
    61  	return w, nil
    62  }
    63  
    64  // GetRepoHook get a repo's webhook. If there is an error, write to `ctx`
    65  // accordingly and return the error
    66  func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhook, error) {
    67  	w, err := webhook.GetWebhookByRepoID(ctx, repoID, hookID)
    68  	if err != nil {
    69  		if webhook.IsErrWebhookNotExist(err) {
    70  			ctx.NotFound()
    71  		} else {
    72  			ctx.Error(http.StatusInternalServerError, "GetWebhookByID", err)
    73  		}
    74  		return nil, err
    75  	}
    76  	return w, nil
    77  }
    78  
    79  // checkCreateHookOption check if a CreateHookOption form is valid. If invalid,
    80  // write the appropriate error to `ctx`. Return whether the form is valid
    81  func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool {
    82  	if !webhook_service.IsValidHookTaskType(form.Type) {
    83  		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Invalid hook type: %s", form.Type))
    84  		return false
    85  	}
    86  	for _, name := range []string{"url", "content_type"} {
    87  		if _, ok := form.Config[name]; !ok {
    88  			ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: "+name)
    89  			return false
    90  		}
    91  	}
    92  	if !webhook.IsValidHookContentType(form.Config["content_type"]) {
    93  		ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type")
    94  		return false
    95  	}
    96  	return true
    97  }
    98  
    99  // AddSystemHook add a system hook
   100  func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) {
   101  	hook, ok := addHook(ctx, form, 0, 0)
   102  	if ok {
   103  		h, err := webhook_service.ToHook(setting.AppSubURL+"/admin", hook)
   104  		if err != nil {
   105  			ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
   106  			return
   107  		}
   108  		ctx.JSON(http.StatusCreated, h)
   109  	}
   110  }
   111  
   112  // AddOwnerHook adds a hook to an user or organization
   113  func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) {
   114  	hook, ok := addHook(ctx, form, owner.ID, 0)
   115  	if !ok {
   116  		return
   117  	}
   118  	apiHook, ok := toAPIHook(ctx, owner.HomeLink(), hook)
   119  	if !ok {
   120  		return
   121  	}
   122  	ctx.JSON(http.StatusCreated, apiHook)
   123  }
   124  
   125  // AddRepoHook add a hook to a repo. Writes to `ctx` accordingly
   126  func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) {
   127  	repo := ctx.Repo
   128  	hook, ok := addHook(ctx, form, 0, repo.Repository.ID)
   129  	if !ok {
   130  		return
   131  	}
   132  	apiHook, ok := toAPIHook(ctx, repo.RepoLink, hook)
   133  	if !ok {
   134  		return
   135  	}
   136  	ctx.JSON(http.StatusCreated, apiHook)
   137  }
   138  
   139  // toAPIHook converts the hook to its API representation.
   140  // If there is an error, write to `ctx` accordingly. Return (hook, ok)
   141  func toAPIHook(ctx *context.APIContext, repoLink string, hook *webhook.Webhook) (*api.Hook, bool) {
   142  	apiHook, err := webhook_service.ToHook(repoLink, hook)
   143  	if err != nil {
   144  		ctx.Error(http.StatusInternalServerError, "ToHook", err)
   145  		return nil, false
   146  	}
   147  	return apiHook, true
   148  }
   149  
   150  func issuesHook(events []string, event string) bool {
   151  	return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventIssues), true)
   152  }
   153  
   154  func pullHook(events []string, event string) bool {
   155  	return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
   156  }
   157  
   158  // addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
   159  // an error, write to `ctx` accordingly. Return (webhook, ok)
   160  func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) {
   161  	var isSystemWebhook bool
   162  	if !checkCreateHookOption(ctx, form) {
   163  		return nil, false
   164  	}
   165  
   166  	if len(form.Events) == 0 {
   167  		form.Events = []string{"push"}
   168  	}
   169  	if form.Config["is_system_webhook"] != "" {
   170  		sw, err := strconv.ParseBool(form.Config["is_system_webhook"])
   171  		if err != nil {
   172  			ctx.Error(http.StatusUnprocessableEntity, "", "Invalid is_system_webhook value")
   173  			return nil, false
   174  		}
   175  		isSystemWebhook = sw
   176  	}
   177  	w := &webhook.Webhook{
   178  		OwnerID:         ownerID,
   179  		RepoID:          repoID,
   180  		URL:             form.Config["url"],
   181  		ContentType:     webhook.ToHookContentType(form.Config["content_type"]),
   182  		Secret:          form.Config["secret"],
   183  		HTTPMethod:      "POST",
   184  		IsSystemWebhook: isSystemWebhook,
   185  		HookEvent: &webhook_module.HookEvent{
   186  			ChooseEvents: true,
   187  			HookEvents: webhook_module.HookEvents{
   188  				Create:                   util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
   189  				Delete:                   util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
   190  				Fork:                     util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
   191  				Issues:                   issuesHook(form.Events, "issues_only"),
   192  				IssueAssign:              issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
   193  				IssueLabel:               issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
   194  				IssueMilestone:           issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
   195  				IssueComment:             issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
   196  				Push:                     util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
   197  				PullRequest:              pullHook(form.Events, "pull_request_only"),
   198  				PullRequestAssign:        pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
   199  				PullRequestLabel:         pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
   200  				PullRequestMilestone:     pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
   201  				PullRequestComment:       pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
   202  				PullRequestReview:        pullHook(form.Events, "pull_request_review"),
   203  				PullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
   204  				PullRequestSync:          pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
   205  				Wiki:                     util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
   206  				Repository:               util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
   207  				Release:                  util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
   208  			},
   209  			BranchFilter: form.BranchFilter,
   210  		},
   211  		IsActive: form.Active,
   212  		Type:     form.Type,
   213  	}
   214  	err := w.SetHeaderAuthorization(form.AuthorizationHeader)
   215  	if err != nil {
   216  		ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err)
   217  		return nil, false
   218  	}
   219  	if w.Type == webhook_module.SLACK {
   220  		channel, ok := form.Config["channel"]
   221  		if !ok {
   222  			ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: channel")
   223  			return nil, false
   224  		}
   225  		channel = strings.TrimSpace(channel)
   226  
   227  		if !webhook_service.IsValidSlackChannel(channel) {
   228  			ctx.Error(http.StatusBadRequest, "", "Invalid slack channel name")
   229  			return nil, false
   230  		}
   231  
   232  		meta, err := json.Marshal(&webhook_service.SlackMeta{
   233  			Channel:  channel,
   234  			Username: form.Config["username"],
   235  			IconURL:  form.Config["icon_url"],
   236  			Color:    form.Config["color"],
   237  		})
   238  		if err != nil {
   239  			ctx.Error(http.StatusInternalServerError, "slack: JSON marshal failed", err)
   240  			return nil, false
   241  		}
   242  		w.Meta = string(meta)
   243  	}
   244  
   245  	if err := w.UpdateEvent(); err != nil {
   246  		ctx.Error(http.StatusInternalServerError, "UpdateEvent", err)
   247  		return nil, false
   248  	} else if err := webhook.CreateWebhook(ctx, w); err != nil {
   249  		ctx.Error(http.StatusInternalServerError, "CreateWebhook", err)
   250  		return nil, false
   251  	}
   252  	return w, true
   253  }
   254  
   255  // EditSystemHook edit system webhook `w` according to `form`. Writes to `ctx` accordingly
   256  func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) {
   257  	hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
   258  	if err != nil {
   259  		ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
   260  		return
   261  	}
   262  	if !editHook(ctx, form, hook) {
   263  		ctx.Error(http.StatusInternalServerError, "editHook", err)
   264  		return
   265  	}
   266  	updated, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
   267  	if err != nil {
   268  		ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
   269  		return
   270  	}
   271  	h, err := webhook_service.ToHook(setting.AppURL+"/admin", updated)
   272  	if err != nil {
   273  		ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
   274  		return
   275  	}
   276  	ctx.JSON(http.StatusOK, h)
   277  }
   278  
   279  // EditOwnerHook updates a webhook of an user or organization
   280  func EditOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.EditHookOption, hookID int64) {
   281  	hook, err := GetOwnerHook(ctx, owner.ID, hookID)
   282  	if err != nil {
   283  		return
   284  	}
   285  	if !editHook(ctx, form, hook) {
   286  		return
   287  	}
   288  	updated, err := GetOwnerHook(ctx, owner.ID, hookID)
   289  	if err != nil {
   290  		return
   291  	}
   292  	apiHook, ok := toAPIHook(ctx, owner.HomeLink(), updated)
   293  	if !ok {
   294  		return
   295  	}
   296  	ctx.JSON(http.StatusOK, apiHook)
   297  }
   298  
   299  // EditRepoHook edit webhook `w` according to `form`. Writes to `ctx` accordingly
   300  func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) {
   301  	repo := ctx.Repo
   302  	hook, err := GetRepoHook(ctx, repo.Repository.ID, hookID)
   303  	if err != nil {
   304  		return
   305  	}
   306  	if !editHook(ctx, form, hook) {
   307  		return
   308  	}
   309  	updated, err := GetRepoHook(ctx, repo.Repository.ID, hookID)
   310  	if err != nil {
   311  		return
   312  	}
   313  	apiHook, ok := toAPIHook(ctx, repo.RepoLink, updated)
   314  	if !ok {
   315  		return
   316  	}
   317  	ctx.JSON(http.StatusOK, apiHook)
   318  }
   319  
   320  // editHook edit the webhook `w` according to `form`. If an error occurs, write
   321  // to `ctx` accordingly and return the error. Return whether successful
   322  func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool {
   323  	if form.Config != nil {
   324  		if url, ok := form.Config["url"]; ok {
   325  			w.URL = url
   326  		}
   327  		if ct, ok := form.Config["content_type"]; ok {
   328  			if !webhook.IsValidHookContentType(ct) {
   329  				ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type")
   330  				return false
   331  			}
   332  			w.ContentType = webhook.ToHookContentType(ct)
   333  		}
   334  
   335  		if w.Type == webhook_module.SLACK {
   336  			if channel, ok := form.Config["channel"]; ok {
   337  				meta, err := json.Marshal(&webhook_service.SlackMeta{
   338  					Channel:  channel,
   339  					Username: form.Config["username"],
   340  					IconURL:  form.Config["icon_url"],
   341  					Color:    form.Config["color"],
   342  				})
   343  				if err != nil {
   344  					ctx.Error(http.StatusInternalServerError, "slack: JSON marshal failed", err)
   345  					return false
   346  				}
   347  				w.Meta = string(meta)
   348  			}
   349  		}
   350  	}
   351  
   352  	// Update events
   353  	if len(form.Events) == 0 {
   354  		form.Events = []string{"push"}
   355  	}
   356  	w.PushOnly = false
   357  	w.SendEverything = false
   358  	w.ChooseEvents = true
   359  	w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
   360  	w.Push = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
   361  	w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
   362  	w.Delete = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
   363  	w.Fork = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
   364  	w.Repository = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
   365  	w.Wiki = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
   366  	w.Release = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
   367  	w.BranchFilter = form.BranchFilter
   368  
   369  	err := w.SetHeaderAuthorization(form.AuthorizationHeader)
   370  	if err != nil {
   371  		ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err)
   372  		return false
   373  	}
   374  
   375  	// Issues
   376  	w.Issues = issuesHook(form.Events, "issues_only")
   377  	w.IssueAssign = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
   378  	w.IssueLabel = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
   379  	w.IssueMilestone = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
   380  	w.IssueComment = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
   381  
   382  	// Pull requests
   383  	w.PullRequest = pullHook(form.Events, "pull_request_only")
   384  	w.PullRequestAssign = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
   385  	w.PullRequestLabel = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
   386  	w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
   387  	w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
   388  	w.PullRequestReview = pullHook(form.Events, "pull_request_review")
   389  	w.PullRequestReviewRequest = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
   390  	w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
   391  
   392  	if err := w.UpdateEvent(); err != nil {
   393  		ctx.Error(http.StatusInternalServerError, "UpdateEvent", err)
   394  		return false
   395  	}
   396  
   397  	if form.Active != nil {
   398  		w.IsActive = *form.Active
   399  	}
   400  
   401  	if err := webhook.UpdateWebhook(ctx, w); err != nil {
   402  		ctx.Error(http.StatusInternalServerError, "UpdateWebhook", err)
   403  		return false
   404  	}
   405  	return true
   406  }
   407  
   408  // DeleteOwnerHook deletes the hook owned by the owner.
   409  func DeleteOwnerHook(ctx *context.APIContext, owner *user_model.User, hookID int64) {
   410  	if err := webhook.DeleteWebhookByOwnerID(ctx, owner.ID, hookID); err != nil {
   411  		if webhook.IsErrWebhookNotExist(err) {
   412  			ctx.NotFound()
   413  		} else {
   414  			ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOwnerID", err)
   415  		}
   416  		return
   417  	}
   418  	ctx.Status(http.StatusNoContent)
   419  }