From d919301d36504d5fd79bb01b318b67befa3c7abc Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Wed, 4 Nov 2026 05:28:49 +0000
Subject: [PATCH] refactor: remove UnmarshalHandleDoubleEncode patch and fix
repo unit TODO (#39998)
Remove the `UnmarshalHandleDoubleEncode` runtime workaround for an old
xorm bug, and normalize legacy double-encoded JSON in `repo_unit.config`
and `login_source.cfg` with a migration instead.
- Config `FromDB` no longer fails on corrupted values. A broken auth
source is deactivated, so admins can still fix it in the admin panel
- Remove `ProjectsConfig.GetProjectsMode`, `FromDB` already defaults
`ProjectsMode`
Actually it doesn't need so many changes, just because AI thinks it
should handle "corrupted database value" for auth sources.
---------
Signed-off-by: wxiaoguang
Co-authored-by: silverwind
---
modelmigration/migrations.go | 1 +
modelmigration/v29/v365.go | 95 +++++++++++++++++++++++++++
modelmigration/v29/v365_test.go | 95 +++++++++++++++++++++++++++
models/actions/config.go | 4 +-
models/auth/source.go | 33 +++++++++-
models/auth/source_test.go | 42 +++++++++++-
models/db/convert.go | 19 ++++++
models/repo/repo_unit.go | 48 ++++----------
models/repo/repo_unit_actions.go | 5 +-
modules/json/json.go | 36 ----------
modules/json/json_test.go | 8 ---
services/auth/source/ldap/source.go | 18 +----
services/auth/source/oauth2/source.go | 5 +-
services/auth/source/pam/source.go | 12 +---
services/auth/source/smtp/source.go | 12 +---
services/auth/source/sspi/source.go | 18 ++---
templates/repo/settings/options.tmpl | 12 ++--
17 files changed, 318 insertions(+), 145 deletions(-)
create mode 100644 modelmigration/v29/v365.go
create mode 100644 modelmigration/v29/v365_test.go
diff --git a/modelmigration/migrations.go b/modelmigration/migrations.go
index fc4bc6a8937..dd1fa3cab38 100644
--- a/modelmigration/migrations.go
+++ b/modelmigration/migrations.go
@@ -441,6 +441,7 @@ func prepareMigrationTasks() []*migration {
newMigration(362, "Add reusable_workflow_ref column to action_run_job", v29.AddReusableWorkflowRefToActionRunJob),
newMigration(363, "Add release_id to reaction table", v29.AddReleaseIDToReaction),
newMigration(364, "Add new table project_workflow", v29.AddProjectWorkflow),
+ newMigration(365, "Normalize legacy repository unit and login source JSON", v29.NormalizeLegacyConfigJSON),
}
return preparedMigrations
}
diff --git a/modelmigration/v29/v365.go b/modelmigration/v29/v365.go
new file mode 100644
index 00000000000..3825decc5d6
--- /dev/null
+++ b/modelmigration/v29/v365.go
@@ -0,0 +1,95 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v29
+
+import (
+ "context"
+ "encoding/binary"
+
+ "gitea.dev/modelmigration/base"
+ "gitea.dev/modules/json"
+ "gitea.dev/modules/util"
+)
+
+// unmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
+// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
+func unmarshalHandleDoubleEncode(bs []byte, v any) error {
+ if len(bs) == 0 {
+ // json.Unmarshal will report errors if input is empty (nil or zero-length)
+ // It seems that XORM ignores the nil but still passes zero-length string into this function
+ // To be consistent, we should treat all empty inputs as success
+ return nil
+ }
+ err := json.Unmarshal(bs, v)
+ if err != nil {
+ ok := true
+ rs := []byte{}
+ temp := make([]byte, 2)
+ for _, rn := range string(bs) {
+ if rn > 0xffff {
+ ok = false
+ break
+ }
+ binary.LittleEndian.PutUint16(temp, uint16(rn))
+ rs = append(rs, temp...)
+ }
+ if ok {
+ if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
+ rs = rs[2:]
+ }
+ err = json.Unmarshal(rs, v)
+ }
+ }
+ if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
+ err = json.Unmarshal(bs[2:], v)
+ }
+ return err
+}
+
+func normalizeLegacyConfigJSON(config string) string {
+ if config == "" || json.Valid(util.UnsafeStringToBytes(config)) {
+ return config
+ }
+
+ m := map[any]any{}
+ err := unmarshalHandleDoubleEncode(util.UnsafeStringToBytes(config), &m)
+ if err != nil {
+ return config
+ }
+ if bs, err := json.Marshal(m); err == nil {
+ return string(bs)
+ }
+ return config
+}
+
+func NormalizeLegacyConfigJSON(ctx context.Context, x base.EngineMigration) error {
+ type RepoUnit struct {
+ ID int64
+ Config string
+ }
+ type LoginSource struct {
+ ID int64
+ Cfg string
+ }
+ return base.EngineTransaction(x, func(sess base.Session) error {
+ if err := base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, unit *RepoUnit) error {
+ config := normalizeLegacyConfigJSON(unit.Config)
+ if config == unit.Config {
+ return nil
+ }
+ _, err := sess.ID(unit.ID).Cols("config").Update(&RepoUnit{Config: config})
+ return err
+ }); err != nil {
+ return err
+ }
+ return base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, source *LoginSource) error {
+ config := normalizeLegacyConfigJSON(source.Cfg)
+ if config == source.Cfg {
+ return nil
+ }
+ _, err := sess.ID(source.ID).Cols("cfg").Update(&LoginSource{Cfg: config})
+ return err
+ })
+ })
+}
diff --git a/modelmigration/v29/v365_test.go b/modelmigration/v29/v365_test.go
new file mode 100644
index 00000000000..f8e4ddeac62
--- /dev/null
+++ b/modelmigration/v29/v365_test.go
@@ -0,0 +1,95 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v29
+
+import (
+ "testing"
+
+ "gitea.dev/modelmigration/migrationtest"
+ "gitea.dev/modules/setting"
+ "gitea.dev/modules/test"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNormalizeLegacyConfigJSONValue(t *testing.T) {
+ const (
+ config = `{"Host":"smtp.example.com"}`
+ packed = "≻潈瑳㨢猢瑭硥浡汰潣≭⁽"
+ )
+ for _, tc := range []struct {
+ name, input, want string
+ }{
+ {"empty", "", ""},
+ {"valid", config + " ", config + " "},
+ {"unicode and large number", ` {"name":"例😀","id":9007199254740993} `, ` {"name":"例😀","id":9007199254740993} `},
+ {"double encoded", packed, config},
+ {"double encoded with BOM", "\ufeff" + packed, config},
+ {"BOM", "\xff\xfe" + config, config},
+ {"BOM with non-BMP rune", "\xff\xfe" + `{"name":"😀"}`, `{"name":"😀"}`},
+ {"corrupt", "invalid", "invalid"},
+ {"corrupt non-BMP rune", "invalid😀", "invalid😀"},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ got := normalizeLegacyConfigJSON(tc.input)
+ assert.Equal(t, tc.want, got)
+ assert.Equal(t, got, normalizeLegacyConfigJSON(got))
+ })
+ }
+}
+
+func TestNormalizeLegacyConfigJSON(t *testing.T) {
+ type RepoUnit struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64
+ Config string `xorm:"TEXT"`
+ }
+ type LoginSource struct {
+ ID int64 `xorm:"pk autoincr"`
+ Name string
+ Cfg string `xorm:"TEXT"`
+ }
+ x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(RepoUnit), new(LoginSource))
+ defer cleanup()
+ if x == nil || t.Failed() {
+ return
+ }
+ defer test.MockVariableValue(&setting.Database.IterateBufferSize, 2)()
+
+ inputs := []string{
+ ` {"id":9007199254740993,"name":"例😀"} `,
+ "≻潈瑳㨢猢瑭硥浡汰潣≭⁽",
+ "\ufeff≻潈瑳㨢猢瑭硥浡汰潣≭⁽",
+ "",
+ "invalid",
+ }
+ want := []string{inputs[0], `{"Host":"smtp.example.com"}`, `{"Host":"smtp.example.com"}`, "", "invalid", ""}
+ for _, input := range inputs {
+ _, err := x.Insert(&RepoUnit{RepoID: 42, Config: input})
+ require.NoError(t, err)
+ _, err = x.Insert(&LoginSource{Name: "test", Cfg: input})
+ require.NoError(t, err)
+ }
+ _, err := x.Exec("INSERT INTO `repo_unit` (`repo_id`, `config`) VALUES (?, NULL)", 42)
+ require.NoError(t, err)
+ _, err = x.Exec("INSERT INTO `login_source` (`name`, `cfg`) VALUES (?, NULL)", "test")
+ require.NoError(t, err)
+
+ for range 2 {
+ require.NoError(t, NormalizeLegacyConfigJSON(t.Context(), x))
+ var units []RepoUnit
+ require.NoError(t, x.Asc("id").Find(&units))
+ require.Len(t, units, len(want))
+ var sources []LoginSource
+ require.NoError(t, x.Asc("id").Find(&sources))
+ require.Len(t, sources, len(want))
+ for i, config := range want {
+ assert.Equal(t, config, units[i].Config)
+ assert.EqualValues(t, 42, units[i].RepoID)
+ assert.Equal(t, config, sources[i].Cfg)
+ assert.Equal(t, "test", sources[i].Name)
+ }
+ }
+}
diff --git a/models/actions/config.go b/models/actions/config.go
index 6821669a81e..81cbb2b9454 100644
--- a/models/actions/config.go
+++ b/models/actions/config.go
@@ -6,10 +6,10 @@ package actions
import (
"context"
+ "gitea.dev/models/db"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
- "gitea.dev/modules/json"
"gitea.dev/modules/util"
"xorm.io/xorm/convert"
@@ -30,7 +30,7 @@ type OwnerActionsConfig struct {
var _ convert.ConversionFrom = (*OwnerActionsConfig)(nil)
func (cfg *OwnerActionsConfig) FromDB(bytes []byte) error {
- _ = json.Unmarshal(bytes, cfg)
+ db.UnmarshalFromDB(bytes, cfg)
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
return nil
}
diff --git a/models/auth/source.go b/models/auth/source.go
index 2533ab5040e..bfcd81b1bd1 100644
--- a/models/auth/source.go
+++ b/models/auth/source.go
@@ -10,6 +10,7 @@ import (
"reflect"
"gitea.dev/models/db"
+ "gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
@@ -60,14 +61,40 @@ var Names = map[Type]string{
type Config interface {
convert.Conversion
SetAuthSource(*Source)
+ GetAuthSource() *Source
}
type ConfigBase struct {
- AuthSource *Source
+ AuthSource *Source `json:"-"`
}
-func (p *ConfigBase) SetAuthSource(s *Source) {
- p.AuthSource = s
+func (c *ConfigBase) SetAuthSource(source *Source) {
+ c.AuthSource = source
+}
+
+func (c *ConfigBase) GetAuthSource() *Source {
+ return c.AuthSource
+}
+
+func UnmarshalAuthSourceConfig(bs []byte, v Config) {
+ if db.UnmarshalFromDB(bs, &v) {
+ return
+ }
+ // This is only a workaround because we don't know whether this unmarshal happens on sign-in page or admin panel.
+ // * Admin should have the chance to edit the auth source in admin panel, so FromDB should not return an error
+ // * The sign-in page should not use the auth source in the next login attempt, but we don't have a clear way to disable it immediately (acceptable)
+ // Actually, corrupted database value is a really rare case, just because AI thinks it should be handled.
+ authSourceID := v.GetAuthSource().ID
+ log.Error("Unable to unmarshal login source id=%d, will mark it as inactive to avoid unexpected sign-in issues", authSourceID)
+ go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
+ // A small time window to avoid race condition with admin's editing because there is no other way.
+ // The UpdateUnix field is not filled when this function is called.
+ _, err := db.GetEngine(ctx).ID(authSourceID).And("is_active = ? AND updated_unix < ?", true, timeutil.TimeStampNow()-5).
+ Cols("is_active").Update(&Source{IsActive: false})
+ if err != nil {
+ log.Error("Unable to set login source id=%d to inactive: %v", authSourceID, err)
+ }
+ })
}
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
diff --git a/models/auth/source_test.go b/models/auth/source_test.go
index b09fc7f4b06..4c915863978 100644
--- a/models/auth/source_test.go
+++ b/models/auth/source_test.go
@@ -6,10 +6,15 @@ package auth_test
import (
"strings"
"testing"
+ "time"
auth_model "gitea.dev/models/auth"
+ "gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/json"
+ "gitea.dev/modules/setting"
+ "gitea.dev/modules/test"
+ "gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,7 +29,8 @@ type TestSource struct {
// FromDB fills up a LDAPConfig from serialized format.
func (source *TestSource) FromDB(bs []byte) error {
- return json.Unmarshal(bs, &source)
+ auth_model.UnmarshalAuthSourceConfig(bs, source)
+ return nil
}
// ToDB exports a LDAPConfig to a serialized format.
@@ -53,3 +59,37 @@ func TestDumpAuthSource(t *testing.T) {
// INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784);
assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`)
}
+
+func TestUnmarshalAuthSourceConfig(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ defer test.MockVariableValue(&setting.IsProd, true)()
+ defer test.MockVariableValue(&setting.IsInTesting, false)()
+
+ cfg := &TestSource{}
+ source := &auth_model.Source{Type: auth_model.OAuth2, Name: t.Name(), IsActive: true, Cfg: cfg}
+ err := db.Insert(t.Context(), source)
+ require.NoError(t, err)
+ cfg.SetAuthSource(source)
+
+ t.Run("ValidConfig", func(t *testing.T) {
+ for _, data := range []string{`{"TestField":"TestValue"}`, "", "null"} {
+ auth_model.UnmarshalAuthSourceConfig([]byte(data), cfg)
+ assert.Equal(t, "TestValue", cfg.TestField)
+ assert.Same(t, source, cfg.GetAuthSource())
+ }
+ })
+
+ t.Run("InvalidConfigDeactivatesSource", func(t *testing.T) {
+ _, err = db.GetEngine(t.Context()).ID(source.ID).NoAutoTime().Cols("updated_unix").Update(&auth_model.Source{UpdatedUnix: timeutil.TimeStampNow() - 60})
+ require.NoError(t, err)
+ has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", true).Get(&auth_model.Source{})
+ assert.True(t, has)
+
+ auth_model.UnmarshalAuthSourceConfig([]byte(`{"TestField":false}`), cfg)
+ assert.Same(t, source, cfg.GetAuthSource())
+ require.Eventually(t, func() bool {
+ has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", false).Get(&auth_model.Source{})
+ return has
+ }, time.Second, time.Millisecond)
+ })
+}
diff --git a/models/db/convert.go b/models/db/convert.go
index 125449705c0..3d9eac47354 100644
--- a/models/db/convert.go
+++ b/models/db/convert.go
@@ -6,6 +6,7 @@ package db
import (
"fmt"
+ "gitea.dev/modules/json"
"gitea.dev/modules/setting"
"xorm.io/xorm"
@@ -84,3 +85,21 @@ func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err erro
}
return T(val), true, err
}
+
+func UnmarshalFromDB(data []byte, v any) bool {
+ if len(data) == 0 {
+ return true
+ }
+ err := json.Unmarshal(data, v)
+ if err != nil {
+ // Even if database value is corrupted, we should not let it cause unnecessary 500 error or block user's operation
+ // Corrupted database value is a really rare case, it should never happen unless site admin manually modified the database.
+ // Although some database values will be printed into logs, it isn't a real problem
+ // because such case should never really happen. If it happens, logging the value
+ // is the only way to tell site admin which record is wrong since there is no other context information,
+ // and then they need to re-create the config (with new secrets)
+ setting.PanicInDevOrTesting("UnmarshalFromDB %q failed: %v", string(data), err)
+ return false
+ }
+ return true
+}
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 4ea8f6d22d6..91d8073832a 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -56,12 +56,11 @@ func init() {
// UnitConfig describes common unit config
type UnitConfig struct{}
-// FromDB fills up a UnitConfig from serialized format.
func (cfg *UnitConfig) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+ db.UnmarshalFromDB(bs, cfg)
+ return nil
}
-// ToDB exports a UnitConfig to a serialized format.
func (cfg *UnitConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -71,12 +70,11 @@ type ExternalWikiConfig struct {
ExternalWikiURL string
}
-// FromDB fills up a ExternalWikiConfig from serialized format.
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+ db.UnmarshalFromDB(bs, cfg)
+ return nil
}
-// ToDB exports a ExternalWikiConfig to a serialized format.
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -89,12 +87,11 @@ type ExternalTrackerConfig struct {
ExternalTrackerRegexpPattern string
}
-// FromDB fills up a ExternalTrackerConfig from serialized format.
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+ db.UnmarshalFromDB(bs, cfg)
+ return nil
}
-// ToDB exports a ExternalTrackerConfig to a serialized format.
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -106,12 +103,11 @@ type IssuesConfig struct {
EnableDependencies bool
}
-// FromDB fills up a IssuesConfig from serialized format.
func (cfg *IssuesConfig) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+ db.UnmarshalFromDB(bs, cfg)
+ return nil
}
-// ToDB exports a IssuesConfig to a serialized format.
func (cfg *IssuesConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -155,16 +151,14 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
return cfg
}
-// FromDB fills up a PullRequestsConfig from serialized format.
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
// set default values for existing PullRequestConfig in DB
*cfg = *DefaultPullRequestsConfig()
- _ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error
+ db.UnmarshalFromDB(bs, cfg)
cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge)
return nil
}
-// ToDB exports a PullRequestsConfig to a serialized format.
func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -228,34 +222,18 @@ type ProjectsConfig struct {
ProjectsMode ProjectsMode
}
-// FromDB fills up a ProjectsConfig from serialized format.
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
- // TODO: remove GetProjectsMode, only use ProjectsMode
- cfg.ProjectsMode = ProjectsModeAll
- return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+ db.UnmarshalFromDB(bs, cfg)
+ cfg.ProjectsMode = util.IfZero(cfg.ProjectsMode, ProjectsModeAll)
+ return nil
}
-// ToDB exports a ProjectsConfig to a serialized format.
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
-func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
- if cfg.ProjectsMode != "" {
- return cfg.ProjectsMode
- }
-
- return ProjectsModeAll
-}
-
func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
- projectsMode := cfg.GetProjectsMode()
-
- if m == ProjectsModeNone {
- return true
- }
-
- return projectsMode == m || projectsMode == ProjectsModeAll
+ return m == ProjectsModeNone || cfg.ProjectsMode == m || cfg.ProjectsMode == ProjectsModeAll
}
// BeforeSet is invoked from XORM before setting the value of a field of this object.
diff --git a/models/repo/repo_unit_actions.go b/models/repo/repo_unit_actions.go
index 65f04b414b6..12f018bef08 100644
--- a/models/repo/repo_unit_actions.go
+++ b/models/repo/repo_unit_actions.go
@@ -6,6 +6,7 @@ package repo
import (
"slices"
+ "gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
"gitea.dev/modules/json"
@@ -173,14 +174,12 @@ func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) Action
return ClampActionsTokenPermissions(perms, maxPerms)
}
-// FromDB fills up a ActionsConfig from serialized format.
func (cfg *ActionsConfig) FromDB(bs []byte) error {
- _ = json.UnmarshalHandleDoubleEncode(bs, &cfg)
+ db.UnmarshalFromDB(bs, cfg)
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
return nil
}
-// ToDB exports a ActionsConfig to a serialized format.
func (cfg *ActionsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
diff --git a/modules/json/json.go b/modules/json/json.go
index eea9a1e6e7b..769ca1f3f43 100644
--- a/modules/json/json.go
+++ b/modules/json/json.go
@@ -5,7 +5,6 @@ package json
import (
"bytes"
- "encoding/binary"
"encoding/json" //nolint:depguard // this package wraps it
"io"
)
@@ -81,38 +80,3 @@ func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
func Valid(data []byte) bool {
return json.Valid(data)
}
-
-// UnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
-// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
-func UnmarshalHandleDoubleEncode(bs []byte, v any) error {
- if len(bs) == 0 {
- // json.Unmarshal will report errors if input is empty (nil or zero-length)
- // It seems that XORM ignores the nil but still passes zero-length string into this function
- // To be consistent, we should treat all empty inputs as success
- return nil
- }
- err := json.Unmarshal(bs, v)
- if err != nil {
- ok := true
- rs := []byte{}
- temp := make([]byte, 2)
- for _, rn := range string(bs) {
- if rn > 0xffff {
- ok = false
- break
- }
- binary.LittleEndian.PutUint16(temp, uint16(rn))
- rs = append(rs, temp...)
- }
- if ok {
- if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
- rs = rs[2:]
- }
- err = json.Unmarshal(rs, v)
- }
- }
- if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
- err = json.Unmarshal(bs[2:], v)
- }
- return err
-}
diff --git a/modules/json/json_test.go b/modules/json/json_test.go
index 2fa4da4cf79..ce3b02dc427 100644
--- a/modules/json/json_test.go
+++ b/modules/json/json_test.go
@@ -10,14 +10,6 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestGiteaDBJSONUnmarshal(t *testing.T) {
- var m map[any]any
- err := UnmarshalHandleDoubleEncode(nil, &m)
- assert.NoError(t, err)
- err = UnmarshalHandleDoubleEncode([]byte(""), &m)
- assert.NoError(t, err)
-}
-
func TestIndent(t *testing.T) {
buf := &bytes.Buffer{}
err := Indent(buf, []byte(`{"a":1}`), ">", " ")
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index 8e6f878bbe2..ddfb8136386 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -13,14 +13,7 @@ import (
"gitea.dev/modules/setting"
)
-// .____ ________ _____ __________
-// | | \______ \ / _ \\______ \
-// | | | | \ / /_\ \| ___/
-// | |___ | ` \/ | \ |
-// |_______ \/_______ /\____|__ /____|
-// \/ \/ \/
-
-// Package ldap provide functions & structure to query a LDAP ldap directory
+// Package ldap provide functions & structure to query an LDAP directory
// For now, it's mainly tested again an MS Active Directory service, see README.md for more information
// Source Basic LDAP authentication service
@@ -60,12 +53,8 @@ type Source struct {
UserUID string // User Attribute listed in Group
}
-// FromDB fills up a LDAPConfig from serialized format.
-func (source *Source) FromDB(bs []byte) error {
- err := json.UnmarshalHandleDoubleEncode(bs, &source)
- if err != nil {
- return err
- }
+func (source *Source) FromDB(bs []byte) (err error) {
+ auth.UnmarshalAuthSourceConfig(bs, source)
if source.BindPasswordEncrypt != "" {
source.BindPassword, err = secret.DecryptSecret(setting.SecretKey, source.BindPasswordEncrypt)
if err != nil {
@@ -76,7 +65,6 @@ func (source *Source) FromDB(bs []byte) error {
return nil
}
-// ToDB exports a LDAPConfig to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
var err error
source.BindPasswordEncrypt, err = secret.EncryptSecret(setting.SecretKey, source.BindPassword)
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index da7f61200c2..fed70ea23ef 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -33,12 +33,11 @@ type Source struct {
ExternalIDClaim string
}
-// FromDB fills up an OAuth2Config from serialized format.
func (source *Source) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &source)
+ auth.UnmarshalAuthSourceConfig(bs, source)
+ return nil
}
-// ToDB exports an OAuth2Config to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
index 3f782e73cff..dd92e99e155 100644
--- a/services/auth/source/pam/source.go
+++ b/services/auth/source/pam/source.go
@@ -8,13 +8,6 @@ import (
"gitea.dev/modules/json"
)
-// __________ _____ _____
-// \______ \/ _ \ / \
-// | ___/ /_\ \ / \ / \
-// | | / | \/ Y \
-// |____| \____|__ /\____|__ /
-// \/ \/
-
// Source holds configuration for the PAM login source.
type Source struct {
auth.ConfigBase `json:"-"`
@@ -23,12 +16,11 @@ type Source struct {
EmailDomain string
}
-// FromDB fills up a PAMConfig from serialized format.
func (source *Source) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &source)
+ auth.UnmarshalAuthSourceConfig(bs, source)
+ return nil
}
-// ToDB exports a PAMConfig to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index 13b4668e9df..5ecdedfbc39 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -8,13 +8,6 @@ import (
"gitea.dev/modules/json"
)
-// _________ __________________________
-// / _____/ / \__ ___/\______ \
-// \_____ \ / \ / \| | | ___/
-// / \/ Y \ | | |
-// /_______ /\____|__ /____| |____|
-// \/ \/
-
// Source holds configuration for the SMTP login source.
type Source struct {
auth.ConfigBase `json:"-"`
@@ -29,12 +22,11 @@ type Source struct {
DisableHelo bool
}
-// FromDB fills up an SMTPConfig from serialized format.
func (source *Source) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &source)
+ auth.UnmarshalAuthSourceConfig(bs, source)
+ return nil
}
-// ToDB exports an SMTPConfig to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go
index 267c269de60..9e51c61e5c9 100644
--- a/services/auth/source/sspi/source.go
+++ b/services/auth/source/sspi/source.go
@@ -8,13 +8,6 @@ import (
"gitea.dev/modules/json"
)
-// _________ ___________________.___
-// / _____// _____/\______ \ |
-// \_____ \ \_____ \ | ___/ |
-// / \/ \ | | | |
-// /_______ /_______ / |____| |___|
-// \/ \/
-
// Source holds configuration for SSPI single sign-on.
type Source struct {
auth.ConfigBase `json:"-"`
@@ -26,14 +19,13 @@ type Source struct {
DefaultLanguage string
}
-// FromDB fills up an SSPIConfig from serialized format.
-func (cfg *Source) FromDB(bs []byte) error {
- return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+func (source *Source) FromDB(bs []byte) error {
+ auth.UnmarshalAuthSourceConfig(bs, source)
+ return nil
}
-// ToDB exports an SSPIConfig to a serialized format.
-func (cfg *Source) ToDB() ([]byte, error) {
- return json.Marshal(cfg)
+func (source *Source) ToDB() ([]byte, error) {
+ return json.Marshal(source)
}
func init() {
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 787a73daf11..fe4c4865dc9 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -466,19 +466,19 @@
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
- {{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
+ {{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
{{end}}
- {{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
+ {{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
{{end}}
- {{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
+ {{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
{{end}}