From 780c7e1d9542ad67cb917de79ccad59332e7a75a Mon Sep 17 00:00:00 2001 From: kerwin612 Date: Fri, 13 Jun 2025 16:46:40 +0800 Subject: [PATCH 1/4] Add prevention for duplicate form submissions --- templates/repo/pulls/fork.tmpl | 4 ++-- web_src/js/index.ts | 2 ++ web_src/js/modules/form.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 web_src/js/modules/form.ts diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl index adf9e250c1dee..0a384151aee21 100644 --- a/templates/repo/pulls/fork.tmpl +++ b/templates/repo/pulls/fork.tmpl @@ -6,7 +6,7 @@
{{template "base/alert" .}} -
+ {{.CsrfTokenHtml}}
@@ -75,7 +75,7 @@
-
diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 7e84773bc18fa..0c70f2f437d71 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -65,6 +65,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; import {callInitFunctions} from './modules/init.ts'; import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; +import {initFormSubmitHandlers} from './modules/form.ts'; initGiteaFomantic(); initSubmitEventPolyfill(); @@ -85,6 +86,7 @@ onDomReady(() => { initGlobalComboMarkdownEditor, initGlobalDeleteButton, initGlobalInput, + initFormSubmitHandlers, initCommonOrganization, initCommonIssueListQuickGoto, diff --git a/web_src/js/modules/form.ts b/web_src/js/modules/form.ts new file mode 100644 index 0000000000000..dd48eb003930e --- /dev/null +++ b/web_src/js/modules/form.ts @@ -0,0 +1,30 @@ +/** + * Form handling utilities + */ + +import {addDelegatedEventListener} from '../utils/dom.ts'; + +/** + * Prevent duplicate form submission + * @param form The form element + * @param submitter The submit button element + */ +function preventDuplicateSubmit(form: HTMLFormElement, submitter: HTMLElement) { + form.addEventListener('submit', () => { + submitter.classList.add('disabled'); + submitter.setAttribute('disabled', 'disabled'); + }); +} + +/** + * Initialize form submit handlers + */ +export function initFormSubmitHandlers() { + // Add delegated event listener for forms with data-prevent-duplicate attribute + addDelegatedEventListener(document, 'submit', 'form[data-prevent-duplicate]', (form: HTMLFormElement) => { + const submitter = form.querySelector('button[type="submit"]'); + if (submitter) { + preventDuplicateSubmit(form, submitter); + } + }); +} From b3a0ec49c26455e7c8457e0df736d179ecc63f62 Mon Sep 17 00:00:00 2001 From: kerwin612 Date: Fri, 13 Jun 2025 19:13:49 +0800 Subject: [PATCH 2/4] fix --- routers/web/repo/fork.go | 2 +- templates/repo/pulls/fork.tmpl | 4 ++-- tests/integration/repo_fork_test.go | 2 +- web_src/js/index.ts | 2 -- web_src/js/modules/form.ts | 30 ----------------------------- 5 files changed, 4 insertions(+), 36 deletions(-) delete mode 100644 web_src/js/modules/form.ts diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 79f033659b46c..6ae459c0ad8b2 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -228,5 +228,5 @@ func ForkPost(ctx *context.Context) { } log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) } diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl index 0a384151aee21..0d775ed6a0ed8 100644 --- a/templates/repo/pulls/fork.tmpl +++ b/templates/repo/pulls/fork.tmpl @@ -6,7 +6,7 @@
{{template "base/alert" .}} - + {{.CsrfTokenHtml}}
@@ -75,7 +75,7 @@
-
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index a7010af14add7..94f8e61c5b7a7 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -51,7 +51,7 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO "repo_name": forkRepoName, "fork_single_branch": forkBranch, }) - session.MakeRequest(t, req, http.StatusSeeOther) + session.MakeRequest(t, req, http.StatusOK) // Step4: check the existence of the forked repo req = NewRequestf(t, "GET", "/%s/%s", forkOwnerName, forkRepoName) diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 0c70f2f437d71..7e84773bc18fa 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -65,7 +65,6 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; import {callInitFunctions} from './modules/init.ts'; import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; -import {initFormSubmitHandlers} from './modules/form.ts'; initGiteaFomantic(); initSubmitEventPolyfill(); @@ -86,7 +85,6 @@ onDomReady(() => { initGlobalComboMarkdownEditor, initGlobalDeleteButton, initGlobalInput, - initFormSubmitHandlers, initCommonOrganization, initCommonIssueListQuickGoto, diff --git a/web_src/js/modules/form.ts b/web_src/js/modules/form.ts deleted file mode 100644 index dd48eb003930e..0000000000000 --- a/web_src/js/modules/form.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Form handling utilities - */ - -import {addDelegatedEventListener} from '../utils/dom.ts'; - -/** - * Prevent duplicate form submission - * @param form The form element - * @param submitter The submit button element - */ -function preventDuplicateSubmit(form: HTMLFormElement, submitter: HTMLElement) { - form.addEventListener('submit', () => { - submitter.classList.add('disabled'); - submitter.setAttribute('disabled', 'disabled'); - }); -} - -/** - * Initialize form submit handlers - */ -export function initFormSubmitHandlers() { - // Add delegated event listener for forms with data-prevent-duplicate attribute - addDelegatedEventListener(document, 'submit', 'form[data-prevent-duplicate]', (form: HTMLFormElement) => { - const submitter = form.querySelector('button[type="submit"]'); - if (submitter) { - preventDuplicateSubmit(form, submitter); - } - }); -} From 3bc926b0c2dd5c0a6242429d546e62ed5e1a38b1 Mon Sep 17 00:00:00 2001 From: kerwin612 Date: Fri, 13 Jun 2025 22:02:57 +0800 Subject: [PATCH 3/4] fix --- routers/web/repo/fork.go | 26 +++++++++++++------------- tests/integration/repo_fork_test.go | 4 +++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 6ae459c0ad8b2..dd15fc845ec5d 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -151,7 +151,7 @@ func ForkPost(ctx *context.Context) { ctx.Data["ContextUser"] = ctxUser if ctx.HasError() { - ctx.HTML(http.StatusOK, tplFork) + ctx.JSONError(tplFork) return } @@ -159,12 +159,12 @@ func ForkPost(ctx *context.Context) { traverseParentRepo := forkRepo for { if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) { - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo")) return } repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID) if repo != nil { - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) return } if !traverseParentRepo.IsFork { @@ -184,7 +184,7 @@ func ForkPost(ctx *context.Context) { ctx.ServerError("CanCreateOrgRepo", err) return } else if !isAllowedToFork { - ctx.HTTPError(http.StatusForbidden) + ctx.JSONError(http.StatusForbidden) return } } @@ -201,26 +201,26 @@ func ForkPost(ctx *context.Context) { case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := ctxUser.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tplFork, &form) + ctx.JSONError(msg) case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo")) case repo_model.IsErrRepoFilesAlreadyExist(err): switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete")) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt")) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete")) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist")) } case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name)) case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern)) case errors.Is(err, user_model.ErrBlockedUser): - ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form) + ctx.JSONError(ctx.Tr("repo.fork.blocked_user")) default: ctx.ServerError("ForkPost", err) } diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index 94f8e61c5b7a7..db2caaf6ca0e0 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" org_service "code.gitea.io/gitea/services/org" "code.gitea.io/gitea/tests" @@ -51,7 +52,8 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO "repo_name": forkRepoName, "fork_single_branch": forkBranch, }) - session.MakeRequest(t, req, http.StatusOK) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, fmt.Sprintf("/%s/%s", forkOwnerName, forkRepoName), test.RedirectURL(resp)) // Step4: check the existence of the forked repo req = NewRequestf(t, "GET", "/%s/%s", forkOwnerName, forkRepoName) From a2cb2331322733682e2c321adbd087aaac4b7808 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 13 Jun 2025 11:36:09 -0700 Subject: [PATCH 4/4] Some fixes --- routers/web/repo/fork.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index dd15fc845ec5d..9f5cda10c28c3 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -151,7 +151,7 @@ func ForkPost(ctx *context.Context) { ctx.Data["ContextUser"] = ctxUser if ctx.HasError() { - ctx.JSONError(tplFork) + ctx.JSONError(ctx.GetErrMsg()) return } @@ -184,7 +184,7 @@ func ForkPost(ctx *context.Context) { ctx.ServerError("CanCreateOrgRepo", err) return } else if !isAllowedToFork { - ctx.JSONError(http.StatusForbidden) + ctx.HTTPError(http.StatusForbidden) return } }