diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index d58309f141bd5..d1dd0a141b963 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1529,6 +1529,10 @@ LEVEL = Info
;; userid = use the userid / sub attribute
;; nickname = use the nickname attribute
;; email = use the username part of the email attribute
+;; Note: `nickname` and `email` options will normalize input strings using the following criteria:
+;; - diacritics are removed
+;; - the characters in the set `['´\x60]` are removed
+;; - the characters in the set `[\s~+]` are replaced with `-`
;USERNAME = nickname
;;
;; Update avatar if available from oauth2 provider.
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index e111ff6db62e0..c11b4012a5514 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -596,9 +596,13 @@ And the following unique queues:
- `OPENID_CONNECT_SCOPES`: **_empty_**: List of additional openid connect scopes. (`openid` is implicitly added)
- `ENABLE_AUTO_REGISTRATION`: **false**: Automatically create user accounts for new oauth2 users.
- `USERNAME`: **nickname**: The source of the username for new oauth2 accounts:
- - userid - use the userid / sub attribute
- - nickname - use the nickname attribute
- - email - use the username part of the email attribute
+ - `userid` - use the userid / sub attribute
+ - `nickname` - use the nickname attribute
+ - `email` - use the username part of the email attribute
+ - Note: `nickname` and `email` options will normalize input strings using the following criteria:
+ - diacritics are removed
+ - the characters in the set `['´\x60]` are removed
+ - the characters in the set `[\s~+]` are replaced with `-`
- `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login.
- `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists:
- disabled - show an error
diff --git a/models/user/user.go b/models/user/user.go
index b02d4eaddf565..b541590cd425d 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -10,8 +10,10 @@ import (
"fmt"
"net/url"
"path/filepath"
+ "regexp"
"strings"
"time"
+ "unicode"
_ "image/jpeg" // Needed for jpeg support
@@ -29,6 +31,9 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
+ "golang.org/x/text/runes"
+ "golang.org/x/text/transform"
+ "golang.org/x/text/unicode/norm"
"xorm.io/builder"
)
@@ -515,6 +520,26 @@ func GetUserSalt() (string, error) {
return hex.EncodeToString(rBytes), nil
}
+// Note: The set of characters here can safely expand without a breaking change,
+// but characters removed from this set can cause user account linking to break
+var (
+ customCharsReplacement = strings.NewReplacer("Æ", "AE")
+ removeCharsRE = regexp.MustCompile(`['´\x60]`)
+ removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
+ replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
+)
+
+// normalizeUserName returns a string with single-quotes and diacritics
+// removed, and any other non-supported username characters replaced with
+// a `-` character
+func NormalizeUserName(s string) (string, error) {
+ strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s))
+ if err != nil {
+ return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s)
+ }
+ return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
+}
+
var (
reservedUsernames = []string{
".",
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 971117482c2d4..65aebea43a957 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -544,3 +544,31 @@ func Test_ValidateUser(t *testing.T) {
assert.EqualValues(t, expected, err == nil, fmt.Sprintf("case: %+v", kase))
}
}
+
+func Test_NormalizeUserFromEmail(t *testing.T) {
+ testCases := []struct {
+ Input string
+ Expected string
+ IsNormalizedValid bool
+ }{
+ {"test", "test", true},
+ {"Sinéad.O'Connor", "Sinead.OConnor", true},
+ {"Æsir", "AEsir", true},
+ // \u00e9\u0065\u0301
+ {"éé", "ee", true},
+ {"Awareness Hub", "Awareness-Hub", true},
+ {"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
+ {".bad.", ".bad.", false},
+ {"new😀user", "new😀user", false}, // No plans to support
+ }
+ for _, testCase := range testCases {
+ normalizedName, err := user_model.NormalizeUserName(testCase.Input)
+ assert.NoError(t, err)
+ assert.EqualValues(t, testCase.Expected, normalizedName)
+ if testCase.IsNormalizedValid {
+ assert.NoError(t, user_model.IsUsableUsername(normalizedName))
+ } else {
+ assert.Error(t, user_model.IsUsableUsername(normalizedName))
+ }
+ }
+}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 03168b6946814..05b1c3ef72a70 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -852,11 +852,14 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
}
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
- // FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
- // The "mode" approach should be refactored to some other more clear&reliable way.
- if ctx.Metas == nil || (ctx.Metas["mode"] == "document" && !ctx.IsWiki) {
+ if ctx.Metas == nil {
return
}
+
+ // FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
+ // The "mode" approach should be refactored to some other more clear&reliable way.
+ crossLinkOnly := (ctx.Metas["mode"] == "document" && !ctx.IsWiki)
+
var (
found bool
ref *references.RenderizableReference
@@ -870,7 +873,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// Repos with external issue trackers might still need to reference local PRs
// We need to concern with the first one that shows up in the text, whichever it is
isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
- foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle)
+ foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
switch ctx.Metas["style"] {
case "", IssueNameStyleNumeric:
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index d1f4e9e8a35b6..62fd0f5a85883 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -561,11 +561,16 @@ func TestPostProcess_RenderDocument(t *testing.T) {
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
}
- // Issue index shouldn't be post processing in an document.
+ // Issue index shouldn't be post processing in a document.
test(
"#1",
"#1")
+ // But cross-referenced issue index should work.
+ test(
+ "go-gitea/gitea#12345",
+ `go-gitea/gitea#12345`)
+
// Test that other post processing still works.
test(
":gitea:",
diff --git a/modules/references/references.go b/modules/references/references.go
index 68662425cccf1..64a67d7da7c3d 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -331,8 +331,11 @@ func FindAllIssueReferences(content string) []IssueReference {
}
// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string.
-func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *RenderizableReference) {
- match := issueNumericPattern.FindStringSubmatchIndex(content)
+func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) (bool, *RenderizableReference) {
+ var match []int
+ if !crossLinkOnly {
+ match = issueNumericPattern.FindStringSubmatchIndex(content)
+ }
if match == nil {
if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil {
return false, nil
diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index aea76b989ce6a..10cadf03dd32b 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -21,7 +21,7 @@ const (
OAuth2UsernameUserid OAuth2UsernameType = "userid"
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name
OAuth2UsernameNickname OAuth2UsernameType = "nickname"
- // OAuth2UsernameEmail username of oauth2 email filed will be used as gitea name
+ // OAuth2UsernameEmail username of oauth2 email field will be used as gitea name
OAuth2UsernameEmail OAuth2UsernameType = "email"
)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 53f0402e2dfaa..3c564f518e5db 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3532,8 +3532,8 @@ runs.actors_no_select = All actors
runs.status_no_select = All status
runs.no_results = No results matched.
runs.no_workflows = There are no workflows yet.
-runs.no_workflows.quick_start = Don't know how to start with Gitea Action? See the quick start guide.
-runs.no_workflows.documentation = For more information on the Gitea Action, see the documentation.
+runs.no_workflows.quick_start = Don't know how to start with Gitea Actions? See the quick start guide.
+runs.no_workflows.documentation = For more information on Gitea Actions, see the documentation.
runs.no_runs = The workflow has no runs yet.
runs.empty_commit_message = (empty commit message)
@@ -3552,7 +3552,7 @@ variables.none = There are no variables yet.
variables.deletion = Remove variable
variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue?
variables.description = Variables will be passed to certain actions and cannot be read otherwise.
-variables.id_not_exist = Variable with id %d not exists.
+variables.id_not_exist = Variable with ID %d does not exist.
variables.edit = Edit Variable
variables.deletion.failed = Failed to remove variable.
variables.deletion.success = The variable has been removed.
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 21a72a9521533..474bae98e4a45 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -368,14 +368,14 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/"
}
-func getUserName(gothUser *goth.User) string {
+func getUserName(gothUser *goth.User) (string, error) {
switch setting.OAuth2Client.Username {
case setting.OAuth2UsernameEmail:
- return strings.Split(gothUser.Email, "@")[0]
+ return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0])
case setting.OAuth2UsernameNickname:
- return gothUser.NickName
+ return user_model.NormalizeUserName(gothUser.NickName)
default: // OAuth2UsernameUserid
- return gothUser.UserID
+ return gothUser.UserID, nil
}
}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index f41590dc13587..1d94e52fe3e47 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -55,7 +55,11 @@ func LinkAccount(ctx *context.Context) {
}
gu, _ := gothUser.(goth.User)
- uname := getUserName(&gu)
+ uname, err := getUserName(&gu)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
email := gu.Email
ctx.Data["user_name"] = uname
ctx.Data["email"] = email
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 21d82cea45039..00305a36ee2b5 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -970,8 +970,13 @@ func SignInOAuthCallback(ctx *context.Context) {
ctx.ServerError("CreateUser", err)
return
}
+ uname, err := getUserName(&gothUser)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
u = &user_model.User{
- Name: getUserName(&gothUser),
+ Name: uname,
FullName: gothUser.Name,
Email: gothUser.Email,
LoginType: auth.OAuth2,
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index 790e270417cff..fc5cd966fcf4c 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -33,7 +33,7 @@
{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}