1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-08-08 03:15:23 +02:00

[gitea] week 2025-13 cherry pick (gitea/main -> forgejo) (#7397)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7397
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Gusted 2025-04-01 13:35:02 +00:00
commit 979cc5cd93
18 changed files with 116 additions and 77 deletions

View file

@ -12,6 +12,9 @@ insert_final_newline = true
[{*.{go,tmpl,html},Makefile,go.mod}] [{*.{go,tmpl,html},Makefile,go.mod}]
indent_style = tab indent_style = tab
[go.*]
indent_style = tab
[templates/custom/*.tmpl] [templates/custom/*.tmpl]
insert_final_newline = false insert_final_newline = false

View file

@ -523,7 +523,7 @@ lint-yaml: .venv
.PHONY: security-check .PHONY: security-check
security-check: security-check:
go run $(GOVULNCHECK_PACKAGE) ./... go run $(GOVULNCHECK_PACKAGE) -show color ./...
### ###
# Development and testing targets # Development and testing targets

View file

@ -179,7 +179,8 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
return err return err
} else if !typesniffer.DetectContentType(fileContents).IsText() { } else if !typesniffer.DetectContentType(fileContents).IsText() {
// FIXME: UTF-16 files will probably fail here // FIXME: UTF-16 files will probably fail here
return nil // Even if the file is not recognized as a "text file", we could still put its name into the indexers to make the filename become searchable, while leave the content to empty.
fileContents = nil
} }
if _, err = batchReader.Discard(1); err != nil { if _, err = batchReader.Discard(1); err != nil {

View file

@ -3,6 +3,8 @@
package optional package optional
import "strconv"
type Option[T any] []T type Option[T any] []T
func None[T any]() Option[T] { func None[T any]() Option[T] {
@ -43,3 +45,12 @@ func (o Option[T]) ValueOrDefault(v T) T {
} }
return v return v
} }
// ParseBool get the corresponding optional.Option[bool] of a string using strconv.ParseBool
func ParseBool(s string) Option[bool] {
v, e := strconv.ParseBool(s)
if e != nil {
return None[bool]()
}
return Some(v)
}

View file

@ -57,3 +57,16 @@ func TestOption(t *testing.T) {
assert.True(t, opt3.Has()) assert.True(t, opt3.Has())
assert.Equal(t, int(1), opt3.Value()) assert.Equal(t, int(1), opt3.Value())
} }
func Test_ParseBool(t *testing.T) {
assert.Equal(t, optional.None[bool](), optional.ParseBool(""))
assert.Equal(t, optional.None[bool](), optional.ParseBool("x"))
assert.Equal(t, optional.Some(false), optional.ParseBool("0"))
assert.Equal(t, optional.Some(false), optional.ParseBool("f"))
assert.Equal(t, optional.Some(false), optional.ParseBool("False"))
assert.Equal(t, optional.Some(true), optional.ParseBool("1"))
assert.Equal(t, optional.Some(true), optional.ParseBool("t"))
assert.Equal(t, optional.Some(true), optional.ParseBool("True"))
}

View file

@ -12,8 +12,8 @@ import (
"time" "time"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/modules/user" "forgejo.org/modules/user"
"forgejo.org/modules/util"
) )
var ForgejoVersion = "1.0.0" var ForgejoVersion = "1.0.0"
@ -162,7 +162,7 @@ func loadRunModeFrom(rootCfg ConfigProvider) {
// The following is a purposefully undocumented option. Please do not run Forgejo as root. It will only cause future headaches. // The following is a purposefully undocumented option. Please do not run Forgejo as root. It will only cause future headaches.
// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || util.OptionalBoolParse(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
RunMode = os.Getenv("GITEA_RUN_MODE") RunMode = os.Getenv("GITEA_RUN_MODE")
if RunMode == "" { if RunMode == "" {
RunMode = rootSec.Key("RUN_MODE").MustString("prod") RunMode = rootSec.Key("RUN_MODE").MustString("prod")

View file

@ -14,22 +14,11 @@ import (
"strconv" "strconv"
"strings" "strings"
"forgejo.org/modules/optional"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
func OptionalBoolParse(s string) optional.Option[bool] {
v, e := strconv.ParseBool(s)
if e != nil {
return optional.None[bool]()
}
return optional.Some(v)
}
// IsEmptyString checks if the provided string is empty // IsEmptyString checks if the provided string is empty
func IsEmptyString(s string) bool { func IsEmptyString(s string) bool {
return len(strings.TrimSpace(s)) == 0 return len(strings.TrimSpace(s)) == 0

View file

@ -11,7 +11,6 @@ import (
"strings" "strings"
"testing" "testing"
"forgejo.org/modules/optional"
"forgejo.org/modules/test" "forgejo.org/modules/test"
"forgejo.org/modules/util" "forgejo.org/modules/util"
@ -181,19 +180,6 @@ func Test_RandomBytes(t *testing.T) {
assert.NotEqual(t, bytes3, bytes4) assert.NotEqual(t, bytes3, bytes4)
} }
func TestOptionalBoolParse(t *testing.T) {
assert.Equal(t, optional.None[bool](), util.OptionalBoolParse(""))
assert.Equal(t, optional.None[bool](), util.OptionalBoolParse("x"))
assert.Equal(t, optional.Some(false), util.OptionalBoolParse("0"))
assert.Equal(t, optional.Some(false), util.OptionalBoolParse("f"))
assert.Equal(t, optional.Some(false), util.OptionalBoolParse("False"))
assert.Equal(t, optional.Some(true), util.OptionalBoolParse("1"))
assert.Equal(t, optional.Some(true), util.OptionalBoolParse("t"))
assert.Equal(t, optional.Some(true), util.OptionalBoolParse("True"))
}
// Test case for any function which accepts and returns a single string. // Test case for any function which accepts and returns a single string.
type StringTest struct { type StringTest struct {
in, out string in, out string

View file

@ -22,7 +22,6 @@ import (
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional" "forgejo.org/modules/optional"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/util"
"forgejo.org/modules/validation" "forgejo.org/modules/validation"
"forgejo.org/modules/web" "forgejo.org/modules/web"
"forgejo.org/routers/web/explore" "forgejo.org/routers/web/explore"
@ -77,11 +76,11 @@ func Users(ctx *context.Context) {
PageSize: setting.UI.Admin.UserPagingNum, PageSize: setting.UI.Admin.UserPagingNum,
}, },
SearchByEmail: true, SearchByEmail: true,
IsActive: util.OptionalBoolParse(statusFilterMap["is_active"]), IsActive: optional.ParseBool(statusFilterMap["is_active"]),
IsAdmin: util.OptionalBoolParse(statusFilterMap["is_admin"]), IsAdmin: optional.ParseBool(statusFilterMap["is_admin"]),
IsRestricted: util.OptionalBoolParse(statusFilterMap["is_restricted"]), IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]), IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]), IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
Load2FAStatus: true, Load2FAStatus: true,
ExtraParamStrings: extraParamStrings, ExtraParamStrings: extraParamStrings,

View file

@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
ctx.Status(http.StatusAccepted) ctx.Status(http.StatusAccepted)
return return
} }
ctx.ServerError("GetCodeFrequencyData", err) ctx.ServerError("GetContributorStats", err)
} else { } else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
} }

View file

@ -4,12 +4,10 @@
package repo package repo
import ( import (
"errors"
"net/http" "net/http"
"forgejo.org/modules/base" "forgejo.org/modules/base"
"forgejo.org/services/context" "forgejo.org/services/context"
contributors_service "forgejo.org/services/repository"
) )
const ( const (
@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRecentCommits) ctx.HTML(http.StatusOK, tplRecentCommits)
} }
// RecentCommitsData returns JSON of recent commits data
func RecentCommitsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return
}
ctx.ServerError("RecentCommitsData", err)
} else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
}
}

View file

@ -1455,7 +1455,7 @@ func registerRoutes(m *web.Route) {
}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode)) }, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
m.Group("/recent-commits", func() { m.Group("/recent-commits", func() {
m.Get("", repo.RecentCommits) m.Get("", repo.RecentCommits)
m.Get("/data", repo.RecentCommitsData) m.Get("/data", repo.CodeFrequencyData)
}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode)) }, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
}, context.RepoRef(), context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases)) }, context.RepoRef(), context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))

View file

@ -361,7 +361,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
if ctx.Req.URL.RawQuery != "" { if ctx.Req.URL.RawQuery != "" {
redirectPath += "?" + ctx.Req.URL.RawQuery redirectPath += "?" + ctx.Req.URL.RawQuery
} }
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) // Git client needs a 301 redirect by default to follow the new location
// It's not documentated in git documentation, but it's the behavior of git client
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
} }
func repoAssignment(ctx *Context, repo *repo_model.Repository) { func repoAssignment(ctx *Context, repo *repo_model.Repository) {

View file

@ -244,6 +244,24 @@ func pruneBrokenReferences(ctx context.Context,
return pruneErr return pruneErr
} }
// checkRecoverableSyncError takes an error message from a git fetch command and returns false if it should be a fatal/blocking error
func checkRecoverableSyncError(stderrMessage string) bool {
switch {
case strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken"):
return true
case strings.Contains(stderrMessage, "remote error") && strings.Contains(stderrMessage, "not our ref"):
return true
case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "but expected"):
return true
case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "unable to resolve reference"):
return true
case strings.Contains(stderrMessage, "Unable to create") && strings.Contains(stderrMessage, ".lock"):
return true
default:
return false
}
}
// runSync returns true if sync finished without error. // runSync returns true if sync finished without error.
func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) { func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) {
repoPath := m.Repo.RepoPath() repoPath := m.Repo.RepoPath()
@ -286,7 +304,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
stdoutMessage := util.SanitizeCredentialURLs(stdout) stdoutMessage := util.SanitizeCredentialURLs(stdout)
// Now check if the error is a resolve reference due to broken reference // Now check if the error is a resolve reference due to broken reference
if strings.Contains(stderr, "unable to resolve reference") && strings.Contains(stderr, "reference broken") { if checkRecoverableSyncError(stderr) {
log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err) log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
err = nil err = nil
@ -337,6 +355,15 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
return nil, false return nil, false
} }
if m.LFS && setting.LFS.StartServer {
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
lfsClient := lfs.NewClient(endpoint, nil)
if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
}
}
log.Trace("SyncMirrors [repo: %-v]: syncing branches...", m.Repo) log.Trace("SyncMirrors [repo: %-v]: syncing branches...", m.Repo)
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0); err != nil { if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to synchronize branches: %v", m.Repo, err) log.Error("SyncMirrors [repo: %-v]: failed to synchronize branches: %v", m.Repo, err)
@ -346,15 +373,6 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
if err = repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo); err != nil { if err = repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err) log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err)
} }
if m.LFS && setting.LFS.StartServer {
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
lfsClient := lfs.NewClient(endpoint, nil)
if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
}
}
gitRepo.Close() gitRepo.Close()
log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo) log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
@ -382,7 +400,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
stdoutMessage := util.SanitizeCredentialURLs(stdout) stdoutMessage := util.SanitizeCredentialURLs(stdout)
// Now check if the error is a resolve reference due to broken reference // Now check if the error is a resolve reference due to broken reference
if strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken") { if checkRecoverableSyncError(stderrMessage) {
log.Warn("SyncMirrors [repo: %-v Wiki]: failed to update mirror wiki repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err) log.Warn("SyncMirrors [repo: %-v Wiki]: failed to update mirror wiki repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
err = nil err = nil

View file

@ -64,3 +64,31 @@ func Test_parseRemoteUpdateOutput(t *testing.T) {
assert.Equal(t, "1c97ebc746", results[9].oldCommitID) assert.Equal(t, "1c97ebc746", results[9].oldCommitID)
assert.Equal(t, "976d27d52f", results[9].newCommitID) assert.Equal(t, "976d27d52f", results[9].newCommitID)
} }
func Test_checkRecoverableSyncError(t *testing.T) {
cases := []struct {
recoverable bool
message string
}{
// A race condition in http git-fetch where certain refs were listed on the remote and are no longer there, would exit status 128
{true, "fatal: remote error: upload-pack: not our ref 988881adc9fc3655077dc2d4d757d480b5ea0e11"},
// A race condition where a local gc/prune removes a named ref during a git-fetch would exit status 1
{true, "cannot lock ref 'refs/pull/123456/merge': unable to resolve reference 'refs/pull/134153/merge'"},
// A race condition in http git-fetch where named refs were listed on the remote and are no longer there
{true, "error: cannot lock ref 'refs/remotes/origin/foo': unable to resolve reference 'refs/remotes/origin/foo': reference broken"},
// A race condition in http git-fetch where named refs were force-pushed during the update, would exit status 128
{true, "error: cannot lock ref 'refs/pull/123456/merge': is at 988881adc9fc3655077dc2d4d757d480b5ea0e11 but expected 7f894307ffc9553edbd0b671cab829786866f7b2"},
// A race condition with other local git operations, such as git-maintenance, would exit status 128 (well, "Unable" the "U" is uppercase)
{true, "fatal: Unable to create '/data/gitea-repositories/foo-org/bar-repo.git/./objects/info/commit-graphs/commit-graph-chain.lock': File exists."},
// Missing or unauthorized credentials, would exit status 128
{false, "fatal: Authentication failed for 'https://example.com/foo-does-not-exist/bar.git/'"},
// A non-existent remote repository, would exit status 128
{false, "fatal: Could not read from remote repository."},
// A non-functioning proxy, would exit status 128
{false, "fatal: unable to access 'https://example.com/foo-does-not-exist/bar.git/': Failed to connect to configured-https-proxy port 1080 after 0 ms: Couldn't connect to server"},
}
for _, c := range cases {
assert.Equal(t, c.recoverable, checkRecoverableSyncError(c.message), "test case: %s", c.message)
}
}

View file

@ -1,7 +1,10 @@
{{$canReadCode := $.Permission.CanRead $.UnitTypeCode}}
<div class="ui fluid vertical menu"> <div class="ui fluid vertical menu">
<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity"> <a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
{{ctx.Locale.Tr "repo.activity.navbar.pulse"}} {{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
</a> </a>
{{if $canReadCode}}
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> <a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}} {{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
</a> </a>
@ -11,4 +14,5 @@
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits"> <a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}} {{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
</a> </a>
{{end}}
</div> </div>

View file

@ -40,7 +40,7 @@
<div class="field"> <div class="field">
<button class="ui primary button" data-url="{{.Link}}">{{ctx.Locale.Tr "actions.runners.update_runner"}}</button> <button class="ui primary button" data-url="{{.Link}}">{{ctx.Locale.Tr "actions.runners.update_runner"}}</button>
<button class="ui red button delete-button show-modal" data-url="{{.Link}}/delete" data-modal="#runner-delete-modal"> <button class="ui red button delete-button" data-url="{{.Link}}/delete" data-modal="#runner-delete-modal">
{{ctx.Locale.Tr "actions.runners.delete_runner"}}</button> {{ctx.Locale.Tr "actions.runners.delete_runner"}}</button>
</div> </div>
</form> </form>

View file

@ -11,12 +11,12 @@
<div class="inline field tw-text-center required"> <div class="inline field tw-text-center required">
<div id="captcha" data-captcha-type="g-recaptcha" class="g-recaptcha-style" data-sitekey="{{.RecaptchaSitekey}}"></div> <div id="captcha" data-captcha-type="g-recaptcha" class="g-recaptcha-style" data-sitekey="{{.RecaptchaSitekey}}"></div>
</div> </div>
<script src='{{URLJoin .RecaptchaURL "api.js"}}'></script> <script defer src='{{URLJoin .RecaptchaURL "api.js"}}'></script>
{{else if eq .CaptchaType "hcaptcha"}} {{else if eq .CaptchaType "hcaptcha"}}
<div class="inline field tw-text-center required"> <div class="inline field tw-text-center required">
<div id="captcha" data-captcha-type="h-captcha" class="h-captcha-style" data-sitekey="{{.HcaptchaSitekey}}"></div> <div id="captcha" data-captcha-type="h-captcha" class="h-captcha-style" data-sitekey="{{.HcaptchaSitekey}}"></div>
</div> </div>
<script src='https://hcaptcha.com/1/api.js'></script> <script defer src='https://hcaptcha.com/1/api.js'></script>
{{else if eq .CaptchaType "mcaptcha"}} {{else if eq .CaptchaType "mcaptcha"}}
<div class="inline field tw-text-center"> <div class="inline field tw-text-center">
<div class="m-captcha-style" id="mcaptcha__widget-container"></div> <div class="m-captcha-style" id="mcaptcha__widget-container"></div>
@ -26,5 +26,5 @@
<div class="inline field tw-text-center"> <div class="inline field tw-text-center">
<div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div> <div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
</div> </div>
<script src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script> <script defer src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script>
{{end}}{{end}} {{end}}{{end}}