Add form-fetch-action to some forms, fix "fetch action" resp bug (#37305)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
PineBale
2026-04-21 02:58:44 +08:00
committed by GitHub
parent 1d25bb22f4
commit ca44b5fca8
7 changed files with 37 additions and 35 deletions

View File

@@ -47,7 +47,7 @@ func ParseJSONError(buf []byte) (ret struct {
} }
func ParseJSONRedirect(buf []byte) (ret struct { func ParseJSONRedirect(buf []byte) (ret struct {
Redirect string `json:"redirect"` Redirect *string `json:"redirect"`
}, },
) { ) {
_ = json.Unmarshal(buf, &ret) _ = json.Unmarshal(buf, &ret)

View File

@@ -725,16 +725,16 @@ func handleSettingsPostConvert(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm) form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() { if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound) ctx.JSONErrorNotFound()
return return
} }
if repo.Name != form.RepoName { if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return return
} }
if !repo.IsMirror { if !repo.IsMirror {
ctx.HTTPError(http.StatusNotFound) ctx.JSONErrorNotFound()
return return
} }
repo.IsMirror = false repo.IsMirror = false
@@ -748,14 +748,14 @@ func handleSettingsPostConvert(ctx *context.Context) {
} }
log.Trace("Repository converted from mirror to regular: %s", repo.FullName()) log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed")) ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
ctx.Redirect(repo.Link()) ctx.JSONRedirect(repo.Link())
} }
func handleSettingsPostConvertFork(ctx *context.Context) { func handleSettingsPostConvertFork(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm) form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() { if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound) ctx.JSONErrorNotFound()
return return
} }
if err := repo.LoadOwner(ctx); err != nil { if err := repo.LoadOwner(ctx); err != nil {
@@ -763,12 +763,12 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
return return
} }
if repo.Name != form.RepoName { if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return return
} }
if !repo.IsFork { if !repo.IsFork {
ctx.HTTPError(http.StatusNotFound) ctx.JSONErrorNotFound()
return return
} }
@@ -776,7 +776,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit() maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.Flash.Error(msg) ctx.Flash.Error(msg)
ctx.Redirect(repo.Link() + "/settings") ctx.JSONRedirect(repo.Link() + "/settings")
return return
} }
@@ -788,25 +788,25 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
log.Trace("Repository converted from fork to regular: %s", repo.FullName()) log.Trace("Repository converted from fork to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed")) ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
ctx.Redirect(repo.Link()) ctx.JSONRedirect(repo.Link())
} }
func handleSettingsPostTransfer(ctx *context.Context) { func handleSettingsPostTransfer(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm) form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() { if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound) ctx.JSONErrorNotFound()
return return
} }
if repo.Name != form.RepoName { if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return return
} }
newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name")) newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("form.enterred_invalid_owner_name"))
return return
} }
ctx.ServerError("IsUserExist", err) ctx.ServerError("IsUserExist", err)
@@ -816,7 +816,7 @@ func handleSettingsPostTransfer(ctx *context.Context) {
if newOwner.Type == user_model.UserTypeOrganization { if newOwner.Type == user_model.UserTypeOrganization {
if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
// The user shouldn't know about this organization // The user shouldn't know about this organization
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("form.enterred_invalid_owner_name"))
return return
} }
} }
@@ -830,14 +830,14 @@ func handleSettingsPostTransfer(ctx *context.Context) {
oldFullname := repo.FullName() oldFullname := repo.FullName()
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
if repo_model.IsErrRepoAlreadyExist(err) { if repo_model.IsErrRepoAlreadyExist(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
} else if repo_model.IsErrRepoTransferInProgress(err) { } else if repo_model.IsErrRepoTransferInProgress(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("repo.settings.transfer_in_progress"))
} else if repo_service.IsRepositoryLimitReached(err) { } else if repo_service.IsRepositoryLimitReached(err) {
limit := err.(repo_service.LimitReachedError).Limit limit := err.(repo_service.LimitReachedError).Limit
ctx.RenderWithErrDeprecated(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil) ctx.JSONError(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit))
} else if errors.Is(err, user_model.ErrBlockedUser) { } else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("repo.settings.transfer.blocked_user"))
} else { } else {
ctx.ServerError("TransferOwnership", err) ctx.ServerError("TransferOwnership", err)
} }
@@ -852,7 +852,7 @@ func handleSettingsPostTransfer(ctx *context.Context) {
log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
} }
ctx.Redirect(repo.Link() + "/settings") ctx.JSONRedirect(repo.Link() + "/settings")
} }
func handleSettingsPostCancelTransfer(ctx *context.Context) { func handleSettingsPostCancelTransfer(ctx *context.Context) {
@@ -887,11 +887,11 @@ func handleSettingsPostDelete(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm) form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() { if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound) ctx.JSONErrorNotFound()
return return
} }
if repo.Name != form.RepoName { if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return return
} }
@@ -907,18 +907,18 @@ func handleSettingsPostDelete(ctx *context.Context) {
log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success")) ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
ctx.Redirect(ctx.Repo.Owner.DashboardLink()) ctx.JSONRedirect(ctx.Repo.Owner.DashboardLink())
} }
func handleSettingsPostDeleteWiki(ctx *context.Context) { func handleSettingsPostDeleteWiki(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm) form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() { if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound) ctx.JSONErrorNotFound()
return return
} }
if repo.Name != form.RepoName { if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return return
} }
@@ -929,7 +929,7 @@ func handleSettingsPostDeleteWiki(ctx *context.Context) {
log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success")) ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings") ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings")
} }
func handleSettingsPostArchive(ctx *context.Context) { func handleSettingsPostArchive(ctx *context.Context) {

View File

@@ -885,7 +885,7 @@
<div class="ui warning message"> <div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_notices_1"}} {{ctx.Locale.Tr "repo.settings.convert_notices_1"}}
</div> </div>
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert"> <input type="hidden" name="action" value="convert">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_confirm"))}} {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_confirm"))}}
@@ -902,7 +902,7 @@
<div class="ui warning message"> <div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_fork_notices_1"}} {{ctx.Locale.Tr "repo.settings.convert_fork_notices_1"}}
</div> </div>
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert_fork"> <input type="hidden" name="action" value="convert_fork">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm"))}} {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm"))}}
@@ -921,7 +921,7 @@
{{ctx.Locale.Tr "repo.settings.transfer_notices_3"}} <br> {{ctx.Locale.Tr "repo.settings.transfer_notices_3"}} <br>
{{ctx.Locale.Tr "repo.settings.transfer_notices_4"}} {{ctx.Locale.Tr "repo.settings.transfer_notices_4"}}
</div> </div>
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="transfer"> <input type="hidden" name="action" value="transfer">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
<div class="required field"> <div class="required field">
@@ -946,7 +946,7 @@
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}} {{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{end}} {{end}}
</div> </div>
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete"> <input type="hidden" name="action" value="delete">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_delete"))}} {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_delete"))}}
@@ -1012,7 +1012,7 @@
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br> {{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}} {{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}}
</div> </div>
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete-wiki"> <input type="hidden" name="action" value="delete-wiki">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete"))}} {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete"))}}

View File

@@ -372,7 +372,8 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
"action": "convert_fork", "action": "convert_fork",
}, },
) )
session.MakeRequest(t, req, http.StatusSeeOther) resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
}) })
// Fork repository again, and check the existence of the forked repo with unique name // Fork repository again, and check the existence of the forked repo with unique name

View File

@@ -189,7 +189,8 @@ func testDeleteRepository(t *testing.T, session *TestSession, ownerName, repoNam
req := NewRequestWithValues(t, "POST", relURL+"?action=delete", map[string]string{ req := NewRequestWithValues(t, "POST", relURL+"?action=delete", map[string]string{
"repo_name": repoName, "repo_name": repoName,
}) })
session.MakeRequest(t, req, http.StatusSeeOther) resp := session.MakeRequest(t, req, http.StatusOK)
assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
} }
func TestPullBranchDelete(t *testing.T) { func TestPullBranchDelete(t *testing.T) {

View File

@@ -39,7 +39,7 @@ func TestRepositoryVisibilityChange(t *testing.T) {
"confirm_repo_name": "user2/repo1", "confirm_repo_name": "user2/repo1",
}) })
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.True(t, repo1.IsPrivate) assert.True(t, repo1.IsPrivate)
@@ -51,7 +51,7 @@ func TestRepositoryVisibilityChange(t *testing.T) {
"private": "false", "private": "false",
}) })
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
assert.False(t, repo2.IsPrivate) assert.False(t, repo2.IsPrivate)

View File

@@ -91,7 +91,7 @@ async function handleFetchActionSuccess(el: HTMLElement, opt: FetchActionOpts, r
async function handleFetchActionError(resp: Response) { async function handleFetchActionError(resp: Response) {
const isRespJson = resp.headers.get('content-type')?.includes('application/json'); const isRespJson = resp.headers.get('content-type')?.includes('application/json');
const respText = await resp.text(); const respText = await resp.text();
const respJson = isRespJson ? JSON.parse(await resp.text()) : null; const respJson = isRespJson ? JSON.parse(respText) : null;
if (respJson?.errorMessage) { if (respJson?.errorMessage) {
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.