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