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}}