From 66565d861ffa4690316b77ceeebf9faa24ac69f9 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Tue, 11 Feb 2025 00:19:37 +0800 Subject: [PATCH 1/7] fix: update varible --- models/actions/variable.go | 21 ++++++++---- routers/api/v1/org/action.go | 6 +++- routers/api/v1/repo/action.go | 2 +- routers/api/v1/user/action.go | 2 +- routers/web/repo/setting/variables.go | 44 ++++++++++++++++++++++++- routers/web/shared/actions/variables.go | 13 -------- services/actions/variables.go | 15 ++++----- 7 files changed, 71 insertions(+), 32 deletions(-) diff --git a/models/actions/variable.go b/models/actions/variable.go index d0f917d923280..c45de7c60d4dd 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -58,6 +58,7 @@ func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data strin type FindVariablesOpts struct { db.ListOptions + IDs []int64 RepoID int64 OwnerID int64 // it will be ignored if RepoID is set Name string @@ -65,6 +66,15 @@ type FindVariablesOpts struct { func (opts FindVariablesOpts) ToConds() builder.Cond { cond := builder.NewCond() + + if len(opts.IDs) > 0 { + if len(opts.IDs) == 1 { + cond = cond.And(builder.Eq{"id": opts.IDs[0]}) + } else { + cond = cond.And(builder.In("id", opts.IDs)) + } + } + // Since we now support instance-level variables, // there is no need to check for null values for `owner_id` and `repo_id` cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) @@ -85,12 +95,11 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab return db.Find[ActionVariable](ctx, opts) } -func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { - count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data"). - Update(&ActionVariable{ - Name: variable.Name, - Data: variable.Data, - }) +func UpdateVariable(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) { + count, err := db.GetEngine(ctx). + ID(variable.ID). + Cols(cols...). + Update(variable) return count != 0, err } diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 199ee7d7773b9..1c1ce8e582eb4 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -450,7 +450,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := actions_service.UpdateVariable(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d27e8d2427b39..cf9fb3839319f 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -414,7 +414,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + if _, err := actions_service.UpdateVariable(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 22707196f4cca..43385278ef278 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -212,7 +212,7 @@ func UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + if _, err := actions_service.UpdateVariable(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go index 9b5453f043649..78b79edabc22f 100644 --- a/routers/web/repo/setting/variables.go +++ b/routers/web/repo/setting/variables.go @@ -7,11 +7,16 @@ import ( "errors" "net/http" + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/web" shared "code.gitea.io/gitea/routers/web/shared/actions" shared_user "code.gitea.io/gitea/routers/web/shared/user" + actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" ) const ( @@ -127,7 +132,44 @@ func VariableUpdate(ctx *context.Context) { return } - shared.UpdateVariable(ctx, vCtx.RedirectLink) + id := ctx.PathParamInt64("variable_id") + + opts := actions_model.FindVariablesOpts{ + IDs: []int64{id}, + } + switch { + case vCtx.IsRepo: + opts.RepoID = vCtx.RepoID + case vCtx.IsOrg: + opts.OwnerID = vCtx.OwnerID + case vCtx.IsUser: + opts.OwnerID = vCtx.OwnerID + case vCtx.IsGlobal: + // do nothing + } + + var variable *actions_model.ActionVariable + if got, err := actions_model.FindVariables(ctx, opts); err != nil { + ctx.ServerError("FindVariables", err) + return + } else if len(got) == 0 { + ctx.NotFound("FindVariables", nil) + return + } else { + variable = got[0] + } + + form := web.GetForm(ctx).(*forms.EditVariableForm) + variable.Name = form.Name + variable.Data = form.Data + + if ok, err := actions_service.UpdateVariable(ctx, variable); err != nil || !ok { + log.Error("UpdateVariable: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.update.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) + ctx.JSONRedirect(vCtx.RedirectLink) } func VariableDelete(ctx *context.Context) { diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index f895475748a5b..aebe7094a5f0e 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -39,19 +39,6 @@ func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL str ctx.JSONRedirect(redirectURL) } -func UpdateVariable(ctx *context.Context, redirectURL string) { - id := ctx.PathParamInt64("variable_id") - form := web.GetForm(ctx).(*forms.EditVariableForm) - - if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok { - log.Error("UpdateVariable: %v", err) - ctx.JSONError(ctx.Tr("actions.variables.update.failed")) - return - } - ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) - ctx.JSONRedirect(redirectURL) -} - func DeleteVariable(ctx *context.Context, redirectURL string) { id := ctx.PathParamInt64("variable_id") diff --git a/services/actions/variables.go b/services/actions/variables.go index 8dde9c4af5c16..7fc428115401b 100644 --- a/services/actions/variables.go +++ b/services/actions/variables.go @@ -6,7 +6,6 @@ package actions import ( "context" "regexp" - "strings" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/log" @@ -31,20 +30,18 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin return v, nil } -func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) { - if err := secret_service.ValidateName(name); err != nil { +func UpdateVariable(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) { + if err := secret_service.ValidateName(variable.Name); err != nil { return false, err } - if err := envNameCIRegexMatch(name); err != nil { + if err := envNameCIRegexMatch(variable.Name); err != nil { return false, err } - return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{ - ID: variableID, - Name: strings.ToUpper(name), - Data: util.ReserveLineBreakForTextarea(data), - }) + variable.Data = util.ReserveLineBreakForTextarea(variable.Data) + + return actions_model.UpdateVariable(ctx, variable, "name", "data") } func DeleteVariableByID(ctx context.Context, variableID int64) error { From 0b783681a4b312fd90a3dda38c16e8257c005150 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Tue, 11 Feb 2025 00:28:51 +0800 Subject: [PATCH 2/7] fix: find before delete --- routers/web/repo/setting/variables.go | 75 ++++++++++++++++++------- routers/web/shared/actions/variables.go | 47 ---------------- 2 files changed, 56 insertions(+), 66 deletions(-) diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go index 78b79edabc22f..fee32075e2198 100644 --- a/routers/web/repo/setting/variables.go +++ b/routers/web/repo/setting/variables.go @@ -8,11 +8,11 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" - shared "code.gitea.io/gitea/routers/web/shared/actions" shared_user "code.gitea.io/gitea/routers/web/shared/user" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" @@ -97,10 +97,15 @@ func Variables(ctx *context.Context) { return } - shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID) - if ctx.Written() { + variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ + OwnerID: vCtx.OwnerID, + RepoID: vCtx.RepoID, + }) + if err != nil { + ctx.ServerError("FindVariables", err) return } + ctx.Data["Variables"] = variables ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) } @@ -117,7 +122,17 @@ func VariableCreate(ctx *context.Context) { return } - shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink) + form := web.GetForm(ctx).(*forms.EditVariableForm) + + v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data) + if err != nil { + log.Error("CreateVariable: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) + return + } + + ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) + ctx.JSONRedirect(vCtx.RedirectLink) } func VariableUpdate(ctx *context.Context) { @@ -134,6 +149,25 @@ func VariableUpdate(ctx *context.Context) { id := ctx.PathParamInt64("variable_id") + variable, ok := findVariable(ctx, id, vCtx) + if !ok { + return + } + + form := web.GetForm(ctx).(*forms.EditVariableForm) + variable.Name = form.Name + variable.Data = form.Data + + if ok, err := actions_service.UpdateVariable(ctx, variable); err != nil || !ok { + log.Error("UpdateVariable: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.update.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) + ctx.JSONRedirect(vCtx.RedirectLink) +} + +func findVariable(ctx *context.Context, id int64, vCtx *variablesCtx) (*actions_model.ActionVariable, bool) { opts := actions_model.FindVariablesOpts{ IDs: []int64{id}, } @@ -151,25 +185,14 @@ func VariableUpdate(ctx *context.Context) { var variable *actions_model.ActionVariable if got, err := actions_model.FindVariables(ctx, opts); err != nil { ctx.ServerError("FindVariables", err) - return + return nil, false } else if len(got) == 0 { ctx.NotFound("FindVariables", nil) - return + return nil, false } else { variable = got[0] } - - form := web.GetForm(ctx).(*forms.EditVariableForm) - variable.Name = form.Name - variable.Data = form.Data - - if ok, err := actions_service.UpdateVariable(ctx, variable); err != nil || !ok { - log.Error("UpdateVariable: %v", err) - ctx.JSONError(ctx.Tr("actions.variables.update.failed")) - return - } - ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) - ctx.JSONRedirect(vCtx.RedirectLink) + return variable, true } func VariableDelete(ctx *context.Context) { @@ -178,5 +201,19 @@ func VariableDelete(ctx *context.Context) { ctx.ServerError("getVariablesCtx", err) return } - shared.DeleteVariable(ctx, vCtx.RedirectLink) + + id := ctx.PathParamInt64("variable_id") + + variable, ok := findVariable(ctx, id, vCtx) + if !ok { + return + } + + if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil { + log.Error("Delete variable [%d] failed: %v", id, err) + ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) + ctx.JSONRedirect(vCtx.RedirectLink) } diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index aebe7094a5f0e..876485bbcd3a8 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -3,50 +3,3 @@ package actions -import ( - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/web" - actions_service "code.gitea.io/gitea/services/actions" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" -) - -func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) { - variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ - OwnerID: ownerID, - RepoID: repoID, - }) - if err != nil { - ctx.ServerError("FindVariables", err) - return - } - ctx.Data["Variables"] = variables -} - -func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) { - form := web.GetForm(ctx).(*forms.EditVariableForm) - - v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data) - if err != nil { - log.Error("CreateVariable: %v", err) - ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) - return - } - - ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) - ctx.JSONRedirect(redirectURL) -} - -func DeleteVariable(ctx *context.Context, redirectURL string) { - id := ctx.PathParamInt64("variable_id") - - if err := actions_service.DeleteVariableByID(ctx, id); err != nil { - log.Error("Delete variable [%d] failed: %v", id, err) - ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) - return - } - ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) - ctx.JSONRedirect(redirectURL) -} From f60a43a55cac2439a627797b023284e530b75909 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Tue, 11 Feb 2025 00:32:31 +0800 Subject: [PATCH 3/7] refactor: move to shared --- routers/web/repo/setting/variables.go | 219 ------------------------ routers/web/shared/actions/variables.go | 214 +++++++++++++++++++++++ routers/web/web.go | 9 +- 3 files changed, 219 insertions(+), 223 deletions(-) delete mode 100644 routers/web/repo/setting/variables.go diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go deleted file mode 100644 index fee32075e2198..0000000000000 --- a/routers/web/repo/setting/variables.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - "errors" - "net/http" - - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/web" - shared_user "code.gitea.io/gitea/routers/web/shared/user" - actions_service "code.gitea.io/gitea/services/actions" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" -) - -const ( - tplRepoVariables templates.TplName = "repo/settings/actions" - tplOrgVariables templates.TplName = "org/settings/actions" - tplUserVariables templates.TplName = "user/settings/actions" - tplAdminVariables templates.TplName = "admin/actions" -) - -type variablesCtx struct { - OwnerID int64 - RepoID int64 - IsRepo bool - IsOrg bool - IsUser bool - IsGlobal bool - VariablesTemplate templates.TplName - RedirectLink string -} - -func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { - if ctx.Data["PageIsRepoSettings"] == true { - return &variablesCtx{ - OwnerID: 0, - RepoID: ctx.Repo.Repository.ID, - IsRepo: true, - VariablesTemplate: tplRepoVariables, - RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables", - }, nil - } - - if ctx.Data["PageIsOrgSettings"] == true { - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return nil, nil - } - return &variablesCtx{ - OwnerID: ctx.ContextUser.ID, - RepoID: 0, - IsOrg: true, - VariablesTemplate: tplOrgVariables, - RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables", - }, nil - } - - if ctx.Data["PageIsUserSettings"] == true { - return &variablesCtx{ - OwnerID: ctx.Doer.ID, - RepoID: 0, - IsUser: true, - VariablesTemplate: tplUserVariables, - RedirectLink: setting.AppSubURL + "/user/settings/actions/variables", - }, nil - } - - if ctx.Data["PageIsAdmin"] == true { - return &variablesCtx{ - OwnerID: 0, - RepoID: 0, - IsGlobal: true, - VariablesTemplate: tplAdminVariables, - RedirectLink: setting.AppSubURL + "/-/admin/actions/variables", - }, nil - } - - return nil, errors.New("unable to set Variables context") -} - -func Variables(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("actions.variables") - ctx.Data["PageType"] = "variables" - ctx.Data["PageIsSharedSettingsVariables"] = true - - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - - variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ - OwnerID: vCtx.OwnerID, - RepoID: vCtx.RepoID, - }) - if err != nil { - ctx.ServerError("FindVariables", err) - return - } - ctx.Data["Variables"] = variables - - ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) -} - -func VariableCreate(ctx *context.Context) { - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - - if ctx.HasError() { // form binding validation error - ctx.JSONError(ctx.GetErrMsg()) - return - } - - form := web.GetForm(ctx).(*forms.EditVariableForm) - - v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data) - if err != nil { - log.Error("CreateVariable: %v", err) - ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) - return - } - - ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) - ctx.JSONRedirect(vCtx.RedirectLink) -} - -func VariableUpdate(ctx *context.Context) { - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - - if ctx.HasError() { // form binding validation error - ctx.JSONError(ctx.GetErrMsg()) - return - } - - id := ctx.PathParamInt64("variable_id") - - variable, ok := findVariable(ctx, id, vCtx) - if !ok { - return - } - - form := web.GetForm(ctx).(*forms.EditVariableForm) - variable.Name = form.Name - variable.Data = form.Data - - if ok, err := actions_service.UpdateVariable(ctx, variable); err != nil || !ok { - log.Error("UpdateVariable: %v", err) - ctx.JSONError(ctx.Tr("actions.variables.update.failed")) - return - } - ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) - ctx.JSONRedirect(vCtx.RedirectLink) -} - -func findVariable(ctx *context.Context, id int64, vCtx *variablesCtx) (*actions_model.ActionVariable, bool) { - opts := actions_model.FindVariablesOpts{ - IDs: []int64{id}, - } - switch { - case vCtx.IsRepo: - opts.RepoID = vCtx.RepoID - case vCtx.IsOrg: - opts.OwnerID = vCtx.OwnerID - case vCtx.IsUser: - opts.OwnerID = vCtx.OwnerID - case vCtx.IsGlobal: - // do nothing - } - - var variable *actions_model.ActionVariable - if got, err := actions_model.FindVariables(ctx, opts); err != nil { - ctx.ServerError("FindVariables", err) - return nil, false - } else if len(got) == 0 { - ctx.NotFound("FindVariables", nil) - return nil, false - } else { - variable = got[0] - } - return variable, true -} - -func VariableDelete(ctx *context.Context) { - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - - id := ctx.PathParamInt64("variable_id") - - variable, ok := findVariable(ctx, id, vCtx) - if !ok { - return - } - - if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil { - log.Error("Delete variable [%d] failed: %v", id, err) - ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) - return - } - ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) - ctx.JSONRedirect(vCtx.RedirectLink) -} diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index 876485bbcd3a8..88c78685b2131 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -3,3 +3,217 @@ package actions +import ( + "errors" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" + actions_service "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" +) + +const ( + tplRepoVariables templates.TplName = "repo/settings/actions" + tplOrgVariables templates.TplName = "org/settings/actions" + tplUserVariables templates.TplName = "user/settings/actions" + tplAdminVariables templates.TplName = "admin/actions" +) + +type variablesCtx struct { + OwnerID int64 + RepoID int64 + IsRepo bool + IsOrg bool + IsUser bool + IsGlobal bool + VariablesTemplate templates.TplName + RedirectLink string +} + +func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { + if ctx.Data["PageIsRepoSettings"] == true { + return &variablesCtx{ + OwnerID: 0, + RepoID: ctx.Repo.Repository.ID, + IsRepo: true, + VariablesTemplate: tplRepoVariables, + RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsOrgSettings"] == true { + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return nil, nil + } + return &variablesCtx{ + OwnerID: ctx.ContextUser.ID, + RepoID: 0, + IsOrg: true, + VariablesTemplate: tplOrgVariables, + RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsUserSettings"] == true { + return &variablesCtx{ + OwnerID: ctx.Doer.ID, + RepoID: 0, + IsUser: true, + VariablesTemplate: tplUserVariables, + RedirectLink: setting.AppSubURL + "/user/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsAdmin"] == true { + return &variablesCtx{ + OwnerID: 0, + RepoID: 0, + IsGlobal: true, + VariablesTemplate: tplAdminVariables, + RedirectLink: setting.AppSubURL + "/-/admin/actions/variables", + }, nil + } + + return nil, errors.New("unable to set Variables context") +} + +func Variables(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.variables") + ctx.Data["PageType"] = "variables" + ctx.Data["PageIsSharedSettingsVariables"] = true + + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ + OwnerID: vCtx.OwnerID, + RepoID: vCtx.RepoID, + }) + if err != nil { + ctx.ServerError("FindVariables", err) + return + } + ctx.Data["Variables"] = variables + + ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) +} + +func VariableCreate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + + form := web.GetForm(ctx).(*forms.EditVariableForm) + + v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data) + if err != nil { + log.Error("CreateVariable: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) + return + } + + ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) + ctx.JSONRedirect(vCtx.RedirectLink) +} + +func VariableUpdate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + + id := ctx.PathParamInt64("variable_id") + + variable, ok := findVariable(ctx, id, vCtx) + if !ok { + return + } + + form := web.GetForm(ctx).(*forms.EditVariableForm) + variable.Name = form.Name + variable.Data = form.Data + + if ok, err := actions_service.UpdateVariable(ctx, variable); err != nil || !ok { + log.Error("UpdateVariable: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.update.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) + ctx.JSONRedirect(vCtx.RedirectLink) +} + +func findVariable(ctx *context.Context, id int64, vCtx *variablesCtx) (*actions_model.ActionVariable, bool) { + opts := actions_model.FindVariablesOpts{ + IDs: []int64{id}, + } + switch { + case vCtx.IsRepo: + opts.RepoID = vCtx.RepoID + case vCtx.IsOrg: + opts.OwnerID = vCtx.OwnerID + case vCtx.IsUser: + opts.OwnerID = vCtx.OwnerID + case vCtx.IsGlobal: + // do nothing + } + + var variable *actions_model.ActionVariable + if got, err := actions_model.FindVariables(ctx, opts); err != nil { + ctx.ServerError("FindVariables", err) + return nil, false + } else if len(got) == 0 { + ctx.NotFound("FindVariables", nil) + return nil, false + } else { + variable = got[0] + } + return variable, true +} + +func VariableDelete(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + id := ctx.PathParamInt64("variable_id") + + variable, ok := findVariable(ctx, id, vCtx) + if !ok { + return + } + + if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil { + log.Error("Delete variable [%d] failed: %v", id, err) + ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) + ctx.JSONRedirect(vCtx.RedirectLink) +} diff --git a/routers/web/web.go b/routers/web/web.go index 0963565c6adb0..2745f7df417d6 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -37,6 +37,7 @@ import ( "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo/actions" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" + shared_actions "code.gitea.io/gitea/routers/web/shared/actions" "code.gitea.io/gitea/routers/web/shared/project" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -449,10 +450,10 @@ func registerRoutes(m *web.Router) { addSettingsVariablesRoutes := func() { m.Group("/variables", func() { - m.Get("", repo_setting.Variables) - m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate) - m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate) - m.Post("/{variable_id}/delete", repo_setting.VariableDelete) + m.Get("", shared_actions.Variables) + m.Post("/new", web.Bind(forms.EditVariableForm{}), shared_actions.VariableCreate) + m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), shared_actions.VariableUpdate) + m.Post("/{variable_id}/delete", shared_actions.VariableDelete) }) } From 0ec499ff5823c15a4d9e22d8d016a5cad06e2992 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Tue, 11 Feb 2025 01:17:09 +0800 Subject: [PATCH 4/7] fix: update name and data --- routers/api/v1/repo/action.go | 4 ++++ routers/api/v1/user/action.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index cf9fb3839319f..abf3b2e928f2b 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -414,6 +414,10 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } + + v.Name = opt.Name + v.Data = opt.Value + if _, err := actions_service.UpdateVariable(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 43385278ef278..8994def795eb0 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -212,6 +212,10 @@ func UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } + + v.Name = opt.Name + v.Data = opt.Value + if _, err := actions_service.UpdateVariable(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) From ff99fe3ca7fc87c8aed51b07a1cf2feb1028ab19 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Tue, 11 Feb 2025 01:20:52 +0800 Subject: [PATCH 5/7] fix: default --- routers/web/shared/actions/variables.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index 88c78685b2131..ccf98001f41c2 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -174,12 +174,13 @@ func findVariable(ctx *context.Context, id int64, vCtx *variablesCtx) (*actions_ switch { case vCtx.IsRepo: opts.RepoID = vCtx.RepoID - case vCtx.IsOrg: - opts.OwnerID = vCtx.OwnerID - case vCtx.IsUser: + case vCtx.IsOrg, vCtx.IsUser: opts.OwnerID = vCtx.OwnerID case vCtx.IsGlobal: // do nothing + default: + ctx.ServerError("findVariable", errors.New("unable to determine")) + return nil, false } var variable *actions_model.ActionVariable From 9e5888cb10c17a26f8b0f9c30abd4312b2467088 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Tue, 11 Feb 2025 01:24:51 +0800 Subject: [PATCH 6/7] chore: lint --- routers/web/shared/actions/variables.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index ccf98001f41c2..a7d12548c0376 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -183,17 +183,15 @@ func findVariable(ctx *context.Context, id int64, vCtx *variablesCtx) (*actions_ return nil, false } - var variable *actions_model.ActionVariable - if got, err := actions_model.FindVariables(ctx, opts); err != nil { + got, err := actions_model.FindVariables(ctx, opts) + if err != nil { ctx.ServerError("FindVariables", err) return nil, false } else if len(got) == 0 { ctx.NotFound("FindVariables", nil) return nil, false - } else { - variable = got[0] } - return variable, true + return got[0], true } func VariableDelete(ctx *context.Context) { From 5f756ddab5571e4eb22bd9d6b23b9bc0b48af1f4 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 11 Feb 2025 02:01:10 +0800 Subject: [PATCH 7/7] fix names --- models/actions/variable.go | 3 +- routers/api/v1/org/action.go | 2 +- routers/api/v1/repo/action.go | 2 +- routers/api/v1/user/action.go | 2 +- routers/web/shared/actions/variables.go | 27 ++-- services/actions/variables.go | 4 +- tests/integration/actions_variables_test.go | 149 ++++++++++++++++++++ 7 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 tests/integration/actions_variables_test.go diff --git a/models/actions/variable.go b/models/actions/variable.go index c45de7c60d4dd..163bb12c9360c 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -95,7 +95,8 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab return db.Find[ActionVariable](ctx, opts) } -func UpdateVariable(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) { +func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) { + variable.Name = strings.ToUpper(variable.Name) count, err := db.GetEngine(ctx). ID(variable.ID). Cols(cols...). diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 1c1ce8e582eb4..05919c523439b 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -454,7 +454,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { v.Name = opt.Name v.Data = opt.Value - if _, err := actions_service.UpdateVariable(ctx, v); err != nil { + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index abf3b2e928f2b..8f464c8dbd9c3 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -418,7 +418,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { v.Name = opt.Name v.Data = opt.Value - if _, err := actions_service.UpdateVariable(ctx, v); err != nil { + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 8994def795eb0..baa4b3b81ec74 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -216,7 +216,7 @@ func UpdateVariable(ctx *context.APIContext) { v.Name = opt.Name v.Data = opt.Value - if _, err := actions_service.UpdateVariable(ctx, v); err != nil { + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index a7d12548c0376..052a8fdd1835b 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -149,8 +149,8 @@ func VariableUpdate(ctx *context.Context) { id := ctx.PathParamInt64("variable_id") - variable, ok := findVariable(ctx, id, vCtx) - if !ok { + variable := findActionsVariable(ctx, id, vCtx) + if ctx.Written() { return } @@ -158,7 +158,7 @@ func VariableUpdate(ctx *context.Context) { variable.Name = form.Name variable.Data = form.Data - if ok, err := actions_service.UpdateVariable(ctx, variable); err != nil || !ok { + if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok { log.Error("UpdateVariable: %v", err) ctx.JSONError(ctx.Tr("actions.variables.update.failed")) return @@ -167,31 +167,36 @@ func VariableUpdate(ctx *context.Context) { ctx.JSONRedirect(vCtx.RedirectLink) } -func findVariable(ctx *context.Context, id int64, vCtx *variablesCtx) (*actions_model.ActionVariable, bool) { +func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *actions_model.ActionVariable { opts := actions_model.FindVariablesOpts{ IDs: []int64{id}, } switch { case vCtx.IsRepo: opts.RepoID = vCtx.RepoID + if opts.RepoID == 0 { + panic("RepoID is 0") + } case vCtx.IsOrg, vCtx.IsUser: opts.OwnerID = vCtx.OwnerID + if opts.OwnerID == 0 { + panic("OwnerID is 0") + } case vCtx.IsGlobal: // do nothing default: - ctx.ServerError("findVariable", errors.New("unable to determine")) - return nil, false + panic("invalid actions variable") } got, err := actions_model.FindVariables(ctx, opts) if err != nil { ctx.ServerError("FindVariables", err) - return nil, false + return nil } else if len(got) == 0 { ctx.NotFound("FindVariables", nil) - return nil, false + return nil } - return got[0], true + return got[0] } func VariableDelete(ctx *context.Context) { @@ -203,8 +208,8 @@ func VariableDelete(ctx *context.Context) { id := ctx.PathParamInt64("variable_id") - variable, ok := findVariable(ctx, id, vCtx) - if !ok { + variable := findActionsVariable(ctx, id, vCtx) + if ctx.Written() { return } diff --git a/services/actions/variables.go b/services/actions/variables.go index 7fc428115401b..95f088dbd334e 100644 --- a/services/actions/variables.go +++ b/services/actions/variables.go @@ -30,7 +30,7 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin return v, nil } -func UpdateVariable(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) { +func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) { if err := secret_service.ValidateName(variable.Name); err != nil { return false, err } @@ -41,7 +41,7 @@ func UpdateVariable(ctx context.Context, variable *actions_model.ActionVariable) variable.Data = util.ReserveLineBreakForTextarea(variable.Data) - return actions_model.UpdateVariable(ctx, variable, "name", "data") + return actions_model.UpdateVariableCols(ctx, variable, "name", "data") } func DeleteVariableByID(ctx context.Context, variableID int64) error { diff --git a/tests/integration/actions_variables_test.go b/tests/integration/actions_variables_test.go new file mode 100644 index 0000000000000..12c1c3f628fd6 --- /dev/null +++ b/tests/integration/actions_variables_test.go @@ -0,0 +1,149 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestActionsVariables(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + ctx := context.Background() + + require.NoError(t, db.DeleteAllRecords("action_variable")) + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + _, _ = actions_model.InsertVariable(ctx, user2.ID, 0, "VAR", "user2-var") + user2Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: user2.ID, Name: "VAR"}) + userWebURL := "/user/settings/actions/variables" + + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization}) + _, _ = actions_model.InsertVariable(ctx, org3.ID, 0, "VAR", "org3-var") + org3Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: org3.ID, Name: "VAR"}) + orgWebURL := "/org/org3/settings/actions/variables" + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + _, _ = actions_model.InsertVariable(ctx, 0, repo1.ID, "VAR", "repo1-var") + repo1Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{RepoID: repo1.ID, Name: "VAR"}) + repoWebURL := "/user2/repo1/settings/actions/variables" + + _, _ = actions_model.InsertVariable(ctx, 0, 0, "VAR", "global-var") + globalVar := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{Name: "VAR", Data: "global-var"}) + adminWebURL := "/-/admin/actions/variables" + + sessionAdmin := loginUser(t, "user1") + sessionUser2 := loginUser(t, user2.Name) + + doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, data string, expectedStatus int) { + req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", baseURL, id), map[string]string{ + "_csrf": GetUserCSRFToken(t, sess), + "name": "VAR", + "data": data, + }) + sess.MakeRequest(t, req, expectedStatus) + } + + doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) { + req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{ + "_csrf": GetUserCSRFToken(t, sess), + }) + sess.MakeRequest(t, req, expectedStatus) + } + + assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) { + doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusNotFound) + doDelete(t, sess, baseURL, id, http.StatusNotFound) + v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id}) + assert.Contains(t, v.Data, "-var") + } + + assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) { + doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusOK) + v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id}) + assert.Equal(t, "ChangedData", v.Data) + doDelete(t, sess, baseURL, id, http.StatusOK) + unittest.AssertNotExistsBean(t, &actions_model.ActionVariable{ID: id}) + } + + t.Run("UpdateUserVar", func(t *testing.T) { + theVar := user2Var + t.Run("FromOrg", func(t *testing.T) { + assertDenied(t, sessionAdmin, orgWebURL, theVar.ID) + }) + t.Run("FromRepo", func(t *testing.T) { + assertDenied(t, sessionAdmin, repoWebURL, theVar.ID) + }) + t.Run("FromAdmin", func(t *testing.T) { + assertDenied(t, sessionAdmin, adminWebURL, theVar.ID) + }) + }) + + t.Run("UpdateOrgVar", func(t *testing.T) { + theVar := org3Var + t.Run("FromRepo", func(t *testing.T) { + assertDenied(t, sessionAdmin, repoWebURL, theVar.ID) + }) + t.Run("FromUser", func(t *testing.T) { + assertDenied(t, sessionAdmin, userWebURL, theVar.ID) + }) + t.Run("FromAdmin", func(t *testing.T) { + assertDenied(t, sessionAdmin, adminWebURL, theVar.ID) + }) + }) + + t.Run("UpdateRepoVar", func(t *testing.T) { + theVar := repo1Var + t.Run("FromOrg", func(t *testing.T) { + assertDenied(t, sessionAdmin, orgWebURL, theVar.ID) + }) + t.Run("FromUser", func(t *testing.T) { + assertDenied(t, sessionAdmin, userWebURL, theVar.ID) + }) + t.Run("FromAdmin", func(t *testing.T) { + assertDenied(t, sessionAdmin, adminWebURL, theVar.ID) + }) + }) + + t.Run("UpdateGlobalVar", func(t *testing.T) { + theVar := globalVar + t.Run("FromOrg", func(t *testing.T) { + assertDenied(t, sessionAdmin, orgWebURL, theVar.ID) + }) + t.Run("FromUser", func(t *testing.T) { + assertDenied(t, sessionAdmin, userWebURL, theVar.ID) + }) + t.Run("FromRepo", func(t *testing.T) { + assertDenied(t, sessionAdmin, repoWebURL, theVar.ID) + }) + }) + + t.Run("UpdateSuccess", func(t *testing.T) { + t.Run("User", func(t *testing.T) { + assertSuccess(t, sessionUser2, userWebURL, user2Var.ID) + }) + t.Run("Org", func(t *testing.T) { + assertSuccess(t, sessionAdmin, orgWebURL, org3Var.ID) + }) + t.Run("Repo", func(t *testing.T) { + assertSuccess(t, sessionUser2, repoWebURL, repo1Var.ID) + }) + t.Run("Admin", func(t *testing.T) { + assertSuccess(t, sessionAdmin, adminWebURL, globalVar.ID) + }) + }) +}