diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c3b78e60bb..5c23f70d7c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -784,6 +784,10 @@ LEVEL = Info ;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token ;ENABLE_BASIC_AUTHENTICATION = true ;; +;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 login methods. +;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication. +;ENABLE_PASSWORD_SIGNIN_FORM = true +;; ;; More detail: https://github.com/gogits/gogs/issues/165 ;ENABLE_REVERSE_PROXY_AUTHENTICATION = false ; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible. @@ -1944,13 +1948,13 @@ LEVEL = Info ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;MINIO_SECRET_ACCESS_KEY = ;; -;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. -;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables -;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, -;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), +;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. +;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables +;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, +;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), ;; or the DefaultIAMRoleEndpoint if not provided otherwise. ;MINIO_IAM_ENDPOINT = -;; +;; ;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio` ;MINIO_BUCKET = gitea ;; @@ -2695,10 +2699,10 @@ LEVEL = Info ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;MINIO_SECRET_ACCESS_KEY = ;; -;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. -;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables -;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, -;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), +;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. +;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables +;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, +;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), ;; or the DefaultIAMRoleEndpoint if not provided otherwise. ;MINIO_IAM_ENDPOINT = ;; diff --git a/models/activities/action.go b/models/activities/action.go index 546d4340ae..65d95fbe66 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -448,65 +448,13 @@ type GetFeedsOptions struct { Date string // the day we want activity for: YYYY-MM-DD } -// GetFeeds returns actions according to the provided options -func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) { - if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { - return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") - } - - cond, err := activityQueryCondition(ctx, opts) - if err != nil { - return nil, 0, err - } - - actions := make([]*Action, 0, opts.PageSize) - var count int64 - opts.SetDefaultValues() - - if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value. - sess := db.GetEngine(ctx).Where(cond) - sess = db.SetSessionPagination(sess, &opts) - - count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) - if err != nil { - return nil, 0, fmt.Errorf("FindAndCount: %w", err) - } - } else { - // First, only query which IDs are necessary, and only then query all actions to speed up the overall query - sess := db.GetEngine(ctx).Where(cond).Select("`action`.id") - sess = db.SetSessionPagination(sess, &opts) - - actionIDs := make([]int64, 0, opts.PageSize) - if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil { - return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) - } - - count, err = db.GetEngine(ctx).Where(cond). - Table("action"). - Cols("`action`.id").Count() - if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) - } - - if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil { - return nil, 0, fmt.Errorf("Find: %w", err) - } - } - - if err := ActionList(actions).LoadAttributes(ctx); err != nil { - return nil, 0, fmt.Errorf("LoadAttributes: %w", err) - } - - return actions, count, nil -} - // ActivityReadable return whether doer can read activities of user func ActivityReadable(user, doer *user_model.User) bool { return !user.KeepActivityPrivate || doer != nil && (doer.IsAdmin || user.ID == doer.ID) } -func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) { +func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) { cond := builder.NewCond() if opts.RequestedTeam != nil && opts.RequestedUser == nil { diff --git a/models/activities/action_list.go b/models/activities/action_list.go index aafb7f8a26..5f9acb8f2a 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -201,3 +201,55 @@ func (actions ActionList) LoadIssues(ctx context.Context) error { } return nil } + +// GetFeeds returns actions according to the provided options +func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) { + if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { + return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") + } + + cond, err := ActivityQueryCondition(ctx, opts) + if err != nil { + return nil, 0, err + } + + actions := make([]*Action, 0, opts.PageSize) + var count int64 + opts.SetDefaultValues() + + if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value. + sess := db.GetEngine(ctx).Where(cond) + sess = db.SetSessionPagination(sess, &opts) + + count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) + if err != nil { + return nil, 0, fmt.Errorf("FindAndCount: %w", err) + } + } else { + // First, only query which IDs are necessary, and only then query all actions to speed up the overall query + sess := db.GetEngine(ctx).Where(cond).Select("`action`.id") + sess = db.SetSessionPagination(sess, &opts) + + actionIDs := make([]int64, 0, opts.PageSize) + if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil { + return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) + } + + count, err = db.GetEngine(ctx).Where(cond). + Table("action"). + Cols("`action`.id").Count() + if err != nil { + return nil, 0, fmt.Errorf("Count: %w", err) + } + + if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil { + return nil, 0, fmt.Errorf("Find: %w", err) + } + } + + if err := ActionList(actions).LoadAttributes(ctx); err != nil { + return nil, 0, fmt.Errorf("LoadAttributes: %w", err) + } + + return actions, count, nil +} diff --git a/models/activities/action_test.go b/models/activities/action_test.go index 64330ebbb3..9cfe981656 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -42,114 +42,6 @@ func TestAction_GetRepoLink(t *testing.T) { assert.Equal(t, comment.HTMLURL(db.DefaultContext), action.GetCommentHTMLURL(db.DefaultContext)) } -func TestGetFeeds(t *testing.T) { - // test with an individual user - assert.NoError(t, unittest.PrepareTestDatabase()) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedUser: user, - Actor: user, - IncludePrivate: true, - OnlyPerformedBy: false, - IncludeDeleted: true, - }) - assert.NoError(t, err) - if assert.Len(t, actions, 1) { - assert.EqualValues(t, 1, actions[0].ID) - assert.EqualValues(t, user.ID, actions[0].UserID) - } - assert.Equal(t, int64(1), count) - - actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedUser: user, - Actor: user, - IncludePrivate: false, - OnlyPerformedBy: false, - }) - assert.NoError(t, err) - assert.Len(t, actions, 0) - assert.Equal(t, int64(0), count) -} - -func TestGetFeedsForRepos(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - privRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}) - - // private repo & no login - actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedRepo: privRepo, - IncludePrivate: true, - }) - assert.NoError(t, err) - assert.Len(t, actions, 0) - assert.Equal(t, int64(0), count) - - // public repo & no login - actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedRepo: pubRepo, - IncludePrivate: true, - }) - assert.NoError(t, err) - assert.Len(t, actions, 1) - assert.Equal(t, int64(1), count) - - // private repo and login - actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedRepo: privRepo, - IncludePrivate: true, - Actor: user, - }) - assert.NoError(t, err) - assert.Len(t, actions, 1) - assert.Equal(t, int64(1), count) - - // public repo & login - actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedRepo: pubRepo, - IncludePrivate: true, - Actor: user, - }) - assert.NoError(t, err) - assert.Len(t, actions, 1) - assert.Equal(t, int64(1), count) -} - -func TestGetFeeds2(t *testing.T) { - // test with an organization user - assert.NoError(t, unittest.PrepareTestDatabase()) - org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedUser: org, - Actor: user, - IncludePrivate: true, - OnlyPerformedBy: false, - IncludeDeleted: true, - }) - assert.NoError(t, err) - assert.Len(t, actions, 1) - if assert.Len(t, actions, 1) { - assert.EqualValues(t, 2, actions[0].ID) - assert.EqualValues(t, org.ID, actions[0].UserID) - } - assert.Equal(t, int64(1), count) - - actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedUser: org, - Actor: user, - IncludePrivate: false, - OnlyPerformedBy: false, - IncludeDeleted: true, - }) - assert.NoError(t, err) - assert.Len(t, actions, 0) - assert.Equal(t, int64(0), count) -} - func TestActivityReadable(t *testing.T) { tt := []struct { desc string @@ -227,26 +119,6 @@ func TestNotifyWatchers(t *testing.T) { }) } -func TestGetFeedsCorrupted(t *testing.T) { - // Now we will not check for corrupted data in the feeds - // users should run doctor to fix their data - assert.NoError(t, unittest.PrepareTestDatabase()) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ID: 8, - RepoID: 1700, - }) - - actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedUser: user, - Actor: user, - IncludePrivate: true, - }) - assert.NoError(t, err) - assert.Len(t, actions, 1) - assert.Equal(t, int64(1), count) -} - func TestConsistencyUpdateAction(t *testing.T) { if !setting.Database.Type.IsSQLite3() { t.Skip("Test is only for SQLite database.") @@ -322,24 +194,3 @@ func TestDeleteIssueActions(t *testing.T) { assert.NoError(t, activities_model.DeleteIssueActions(db.DefaultContext, issue.RepoID, issue.ID, issue.Index)) unittest.AssertCount(t, &activities_model.Action{}, 0) } - -func TestRepoActions(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - _ = db.TruncateBeans(db.DefaultContext, &activities_model.Action{}) - for i := 0; i < 3; i++ { - _ = db.Insert(db.DefaultContext, &activities_model.Action{ - UserID: 2 + int64(i), - ActUserID: 2, - RepoID: repo.ID, - OpType: activities_model.ActionCommentIssue, - }) - } - count, _ := db.Count[activities_model.Action](db.DefaultContext, &db.ListOptions{}) - assert.EqualValues(t, 3, count) - actions, _, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedRepo: repo, - }) - assert.NoError(t, err) - assert.Len(t, actions, 1) -} diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index 78fcd76d43..1f8f0f590e 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -47,7 +47,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi groupByName = groupBy } - cond, err := activityQueryCondition(ctx, GetFeedsOptions{ + cond, err := ActivityQueryCondition(ctx, GetFeedsOptions{ RequestedUser: user, RequestedTeam: team, Actor: doer, diff --git a/models/renderhelper/repo_file_test.go b/models/renderhelper/repo_file_test.go index 40027ec76f..959648b660 100644 --- a/models/renderhelper/repo_file_test.go +++ b/models/renderhelper/repo_file_test.go @@ -12,6 +12,8 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + _ "code.gitea.io/gitea/modules/markup/orgmode" + "github.com/stretchr/testify/assert" ) @@ -81,3 +83,40 @@ func TestRepoFile(t *testing.T) { `, rendered) }) } + +func TestRepoFileOrgMode(t *testing.T) { + unittest.PrepareTestEnv(t) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + t.Run("Links", func(t *testing.T) { + rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{ + CurrentRefPath: "/commit/1234", + CurrentTreePath: "my-dir", + }).WithRelativePath("my-dir/a.org") + + rendered, err := markup.RenderString(rctx, ` +[[https://google.com/]] +[[ImageLink.svg][The Image Desc]] +`) + assert.NoError(t, err) + assert.Equal(t, `

+https://google.com/ +The Image Desc

+`, rendered) + }) + + t.Run("CodeHighlight", func(t *testing.T) { + rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{}).WithRelativePath("my-dir/a.org") + + rendered, err := markup.RenderString(rctx, ` +#+begin_src c +int a = 1; +#+end_src +`) + assert.NoError(t, err) + assert.Equal(t, `
+
int a = 1;
+
+`, rendered) + }) +} diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go index e0b82f86f2..d0352947a8 100644 --- a/modules/cache/cache_test.go +++ b/modules/cache/cache_test.go @@ -43,7 +43,7 @@ func TestTest(t *testing.T) { elapsed, err := Test() assert.NoError(t, err) // mem cache should take from 300ns up to 1ms on modern hardware ... - assert.Less(t, elapsed, SlowCacheThreshold) + assert.Less(t, elapsed, time.Millisecond) } func TestGetCache(t *testing.T) { diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index 31257351ae..c6cc334000 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -6,10 +6,12 @@ package markup import ( "fmt" "html" + "html/template" "io" "strings" "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -20,33 +22,36 @@ import ( ) func init() { - markup.RegisterRenderer(Renderer{}) + markup.RegisterRenderer(renderer{}) } // Renderer implements markup.Renderer for orgmode -type Renderer struct{} +type renderer struct{} -var _ markup.PostProcessRenderer = (*Renderer)(nil) +var ( + _ markup.Renderer = (*renderer)(nil) + _ markup.PostProcessRenderer = (*renderer)(nil) +) // Name implements markup.Renderer -func (Renderer) Name() string { +func (renderer) Name() string { return "orgmode" } // NeedPostProcess implements markup.PostProcessRenderer -func (Renderer) NeedPostProcess() bool { return true } +func (renderer) NeedPostProcess() bool { return true } // Extensions implements markup.Renderer -func (Renderer) Extensions() []string { +func (renderer) Extensions() []string { return []string{".org"} } // SanitizerRules implements markup.Renderer -func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { +func (renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{} } -// Render renders orgmode rawbytes to HTML +// Render renders orgmode raw bytes to HTML func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { htmlWriter := org.NewHTMLWriter() htmlWriter.HighlightCodeBlock = func(source, lang string, inline bool, params map[string]string) string { @@ -56,10 +61,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error panic(err) } }() - var w strings.Builder - if _, err := w.WriteString(`
`); err != nil {
-			return ""
-		}
+		w := &strings.Builder{}
 
 		lexer := lexers.Get(lang)
 		if lexer == nil && lang == "" {
@@ -70,26 +72,20 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 			lang = strings.ToLower(lexer.Config().Name)
 		}
 
+		// include language-x class as part of commonmark spec
+		if err := ctx.RenderInternal.FormatWithSafeAttrs(w, `
`, lang); err != nil {
+			return ""
+		}
 		if lexer == nil {
-			// include language-x class as part of commonmark spec
-			if _, err := w.WriteString(``); err != nil {
-				return ""
-			}
 			if _, err := w.WriteString(html.EscapeString(source)); err != nil {
 				return ""
 			}
 		} else {
-			// include language-x class as part of commonmark spec
-			if _, err := w.WriteString(``); err != nil {
-				return ""
-			}
 			lexer = chroma.Coalesce(lexer)
-
 			if _, err := w.WriteString(string(highlight.CodeFromLexer(lexer, source))); err != nil {
 				return ""
 			}
 		}
-
 		if _, err := w.WriteString("
"); err != nil { return "" } @@ -97,11 +93,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error return w.String() } - w := &Writer{ - HTMLWriter: htmlWriter, - Ctx: ctx, - } - + w := &orgWriter{rctx: ctx, HTMLWriter: htmlWriter} htmlWriter.ExtendingWriter = w res, err := org.New().Silent().Parse(input, "").Write(w) @@ -122,17 +114,18 @@ func RenderString(ctx *markup.RenderContext, content string) (string, error) { } // Render renders orgmode string to HTML string -func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { +func (renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { return Render(ctx, input, output) } -// Writer implements org.Writer -type Writer struct { +type orgWriter struct { *org.HTMLWriter - Ctx *markup.RenderContext + rctx *markup.RenderContext } -func (r *Writer) resolveLink(kind, link string) string { +var _ org.Writer = (*orgWriter)(nil) + +func (r *orgWriter) resolveLink(kind, link string) string { link = strings.TrimPrefix(link, "file:") if !strings.HasPrefix(link, "#") && // not a URL fragment !markup.IsFullURLString(link) { @@ -142,39 +135,42 @@ func (r *Writer) resolveLink(kind, link string) string { kind = org.RegularLink{URL: link}.Kind() } if kind == "image" || kind == "video" { - link = r.Ctx.RenderHelper.ResolveLink(link, markup.LinkTypeMedia) + link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeMedia) } else { - link = r.Ctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault) + link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault) } } return link } // WriteRegularLink renders images, links or videos -func (r *Writer) WriteRegularLink(l org.RegularLink) { +func (r *orgWriter) WriteRegularLink(l org.RegularLink) { link := r.resolveLink(l.Kind(), l.URL) + printHTML := func(html string, a ...any) { + _, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...)) + } // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 switch l.Kind() { case "image": if l.Description == nil { - _, _ = fmt.Fprintf(r, `%s`, link, link) + printHTML(`%s`, link, link) } else { imageSrc := r.resolveLink(l.Kind(), org.String(l.Description...)) - _, _ = fmt.Fprintf(r, `%s`, link, imageSrc, imageSrc) + printHTML(`%s`, link, imageSrc, imageSrc) } case "video": if l.Description == nil { - _, _ = fmt.Fprintf(r, ``, link, link) + printHTML(``, link, link) } else { videoSrc := r.resolveLink(l.Kind(), org.String(l.Description...)) - _, _ = fmt.Fprintf(r, ``, link, videoSrc, videoSrc) + printHTML(``, link, videoSrc, videoSrc) } default: - description := link + var description any = link if l.Description != nil { - description = r.WriteNodesAsString(l.Description...) + description = template.HTML(r.WriteNodesAsString(l.Description...)) // orgmode HTMLWriter outputs HTML content } - _, _ = fmt.Fprintf(r, `%s`, link, description) + printHTML(`%s`, link, description) } } diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index d30df3b188..e3cc05b4f0 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -58,15 +58,15 @@ func TestRender_Media(t *testing.T) { } test("[[file:../../.images/src/02/train.jpg]]", - `

.images/src/02/train.jpg

`) + `

.images/src/02/train.jpg

`) test("[[file:train.jpg]]", - `

relative-path/train.jpg

`) + `

relative-path/train.jpg

`) // With description. test("[[https://example.com][https://example.com/example.svg]]", - `

https://example.com/example.svg

`) + `

https://example.com/example.svg

`) test("[[https://example.com][pre https://example.com/example.svg post]]", - `

pre https://example.com/example.svg post

`) + `

pre https://example.com/example.svg post

`) test("[[https://example.com][https://example.com/example.mp4]]", `

`) test("[[https://example.com][pre https://example.com/example.mp4 post]]", @@ -74,19 +74,19 @@ func TestRender_Media(t *testing.T) { // Without description. test("[[https://example.com/example.svg]]", - `

https://example.com/example.svg

`) + `

https://example.com/example.svg

`) test("[[https://example.com/example.mp4]]", `

`) // test [[LINK][DESCRIPTION]] syntax with "file:" prefix test(`[[https://example.com/][file:https://example.com/foo%20bar.svg]]`, - `

https://example.com/foo%20bar.svg

`) + `

https://example.com/foo%20bar.svg

`) test(`[[file:https://example.com/foo%20bar.svg][Goto Image]]`, `

Goto Image

`) test(`[[file:https://example.com/link][https://example.com/image.jpg]]`, - `

https://example.com/image.jpg

`) + `

https://example.com/image.jpg

`) test(`[[file:https://example.com/link][file:https://example.com/image.jpg]]`, - `

https://example.com/image.jpg

`) + `

https://example.com/image.jpg

`) } func TestRender_Source(t *testing.T) { diff --git a/modules/setting/service.go b/modules/setting/service.go index c858f80354..526ad64eb4 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -41,6 +41,7 @@ var Service = struct { AllowOnlyInternalRegistration bool AllowOnlyExternalRegistration bool ShowRegistrationButton bool + EnablePasswordSignInForm bool ShowMilestonesDashboardPage bool RequireSignInView bool EnableNotifyMail bool @@ -159,6 +160,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true) Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) + Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true) Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b7766e4dc0..1a2db341e2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1032,6 +1032,8 @@ fork_to_different_account = Fork to a different account fork_visibility_helper = The visibility of a forked repository cannot be changed. fork_branch = Branch to be cloned to the fork all_branches = All branches +view_all_branches = View all branches +view_all_tags = View all tags fork_no_valid_owners = This repository can not be forked because there are no valid owners. fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner. use_template = Use this template @@ -1461,8 +1463,6 @@ issues.new.no_items = No items issues.new.milestone = Milestone issues.new.no_milestone = No Milestone issues.new.clear_milestone = Clear milestone -issues.new.open_milestone = Open Milestones -issues.new.closed_milestone = Closed Milestones issues.new.assignees = Assignees issues.new.clear_assignees = Clear assignees issues.new.no_assignees = No Assignees diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 9e58746272..3fb653bcb6 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + feed_service "code.gitea.io/gitea/services/feed" "code.gitea.io/gitea/services/org" user_service "code.gitea.io/gitea/services/user" ) @@ -447,7 +448,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) { ListOptions: listOptions, } - feeds, count, err := activities_model.GetFeeds(ctx, opts) + feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetFeeds", err) return diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 20226b4d6b..bc50960b61 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + feed_service "code.gitea.io/gitea/services/feed" org_service "code.gitea.io/gitea/services/org" repo_service "code.gitea.io/gitea/services/repository" ) @@ -882,7 +883,7 @@ func ListTeamActivityFeeds(ctx *context.APIContext) { ListOptions: listOptions, } - feeds, count, err := activities_model.GetFeeds(ctx, opts) + feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetFeeds", err) return diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 69a95dd5a5..40990a28cb 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -34,6 +34,7 @@ import ( actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + feed_service "code.gitea.io/gitea/services/feed" "code.gitea.io/gitea/services/issue" repo_service "code.gitea.io/gitea/services/repository" ) @@ -1313,7 +1314,7 @@ func ListRepoActivityFeeds(ctx *context.APIContext) { ListOptions: listOptions, } - feeds, count, err := activities_model.GetFeeds(ctx, opts) + feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetFeeds", err) return diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index a9011427fb..e668326861 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + feed_service "code.gitea.io/gitea/services/feed" ) // Search search users @@ -214,7 +215,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) { ListOptions: listOptions, } - feeds, count, err := activities_model.GetFeeds(ctx, opts) + feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetFeeds", err) return diff --git a/routers/private/mail.go b/routers/private/mail.go index cf3abb31c6..6c33467af7 100644 --- a/routers/private/mail.go +++ b/routers/private/mail.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/mailer" + sender_service "code.gitea.io/gitea/services/mailer/sender" ) // SendEmail pushes messages to mail queue @@ -81,7 +82,7 @@ func SendEmail(ctx *context.PrivateContext) { func sendEmail(ctx *context.PrivateContext, subject, message string, to []string) { for _, email := range to { - msg := mailer.NewMessage(email, subject, message) + msg := sender_service.NewMessage(email, subject, message) mailer.SendAsync(msg) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index c9ef9193f1..3f16da3cdd 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -160,54 +160,42 @@ func CheckAutoLogin(ctx *context.Context) bool { return false } -// SignIn render sign in page -func SignIn(ctx *context.Context) { +func prepareSignInPageData(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("sign_in") - - if CheckAutoLogin(ctx) { - return - } - - if ctx.IsSigned { - RedirectAfterLogin(ctx) - return - } - - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) - if err != nil { - ctx.ServerError("UserSignIn", err) - return - } - ctx.Data["OAuth2Providers"] = oauth2Providers + ctx.Data["OAuth2Providers"], _ = oauth2.GetOAuth2Providers(ctx, optional.Some(true)) ctx.Data["Title"] = ctx.Tr("sign_in") ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsLogin"] = true ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { context.SetCaptchaData(ctx) } +} +// SignIn render sign in page +func SignIn(ctx *context.Context) { + if CheckAutoLogin(ctx) { + return + } + if ctx.IsSigned { + RedirectAfterLogin(ctx) + return + } + prepareSignInPageData(ctx) ctx.HTML(http.StatusOK, tplSignIn) } // SignInPost response for sign in request func SignInPost(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("sign_in") - - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) - if err != nil { - ctx.ServerError("UserSignIn", err) + if !setting.Service.EnablePasswordSignInForm { + ctx.Error(http.StatusForbidden) return } - ctx.Data["OAuth2Providers"] = oauth2Providers - ctx.Data["Title"] = ctx.Tr("sign_in") - ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" - ctx.Data["PageIsSignIn"] = true - ctx.Data["PageIsLogin"] = true - ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) + prepareSignInPageData(ctx) if ctx.HasError() { ctx.HTML(http.StatusOK, tplSignIn) return @@ -216,8 +204,6 @@ func SignInPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.SignInForm) if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { - context.SetCaptchaData(ctx) - context.VerifyCaptcha(ctx, tplSignIn, form) if ctx.Written() { return diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 47de7c089d..4ec46e302a 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/renderhelper" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/services/context" + feed_service "code.gitea.io/gitea/services/feed" "github.com/gorilla/feeds" ) @@ -28,7 +29,7 @@ func ShowUserFeedAtom(ctx *context.Context) { func showUserFeed(ctx *context.Context, formatType string) { includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) - actions, _, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ + actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.Doer, IncludePrivate: includePrivate, diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go index bfcc3a37d6..2e69fac758 100644 --- a/routers/web/feed/repo.go +++ b/routers/web/feed/repo.go @@ -9,13 +9,14 @@ import ( activities_model "code.gitea.io/gitea/models/activities" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/services/context" + feed_service "code.gitea.io/gitea/services/feed" "github.com/gorilla/feeds" ) // ShowRepoFeed shows user activity on the repo as RSS / Atom feed func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) { - actions, _, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ + actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedRepo: repo, Actor: ctx.Doer, IncludePrivate: true, diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 6149ccb08d..0cf932ac03 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/web/feed" "code.gitea.io/gitea/services/context" + feed_service "code.gitea.io/gitea/services/feed" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" @@ -113,7 +114,7 @@ func Dashboard(ctx *context.Context) { ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) } - feeds, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ + feeds, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctxUser, RequestedTeam: ctx.Org.Team, Actor: ctx.Doer, diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 931af0a283..c41030a5e2 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/routers/web/org" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" + feed_service "code.gitea.io/gitea/services/feed" ) const ( @@ -167,7 +168,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb case "activity": date := ctx.FormString("date") pagingNum = setting.UI.FeedPagingNum - items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ + items, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.Doer, IncludePrivate: showPrivate, diff --git a/services/feed/feed.go b/services/feed/feed.go new file mode 100644 index 0000000000..93bf875fd0 --- /dev/null +++ b/services/feed/feed.go @@ -0,0 +1,15 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package feed + +import ( + "context" + + activities_model "code.gitea.io/gitea/models/activities" +) + +// GetFeeds returns actions according to the provided options +func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) { + return activities_model.GetFeeds(ctx, opts) +} diff --git a/services/feed/feed_test.go b/services/feed/feed_test.go new file mode 100644 index 0000000000..6f1cb9a969 --- /dev/null +++ b/services/feed/feed_test.go @@ -0,0 +1,165 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package feed + +import ( + "testing" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestGetFeeds(t *testing.T) { + // test with an individual user + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedUser: user, + Actor: user, + IncludePrivate: true, + OnlyPerformedBy: false, + IncludeDeleted: true, + }) + assert.NoError(t, err) + if assert.Len(t, actions, 1) { + assert.EqualValues(t, 1, actions[0].ID) + assert.EqualValues(t, user.ID, actions[0].UserID) + } + assert.Equal(t, int64(1), count) + + actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedUser: user, + Actor: user, + IncludePrivate: false, + OnlyPerformedBy: false, + }) + assert.NoError(t, err) + assert.Len(t, actions, 0) + assert.Equal(t, int64(0), count) +} + +func TestGetFeedsForRepos(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + privRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}) + + // private repo & no login + actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedRepo: privRepo, + IncludePrivate: true, + }) + assert.NoError(t, err) + assert.Len(t, actions, 0) + assert.Equal(t, int64(0), count) + + // public repo & no login + actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedRepo: pubRepo, + IncludePrivate: true, + }) + assert.NoError(t, err) + assert.Len(t, actions, 1) + assert.Equal(t, int64(1), count) + + // private repo and login + actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedRepo: privRepo, + IncludePrivate: true, + Actor: user, + }) + assert.NoError(t, err) + assert.Len(t, actions, 1) + assert.Equal(t, int64(1), count) + + // public repo & login + actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedRepo: pubRepo, + IncludePrivate: true, + Actor: user, + }) + assert.NoError(t, err) + assert.Len(t, actions, 1) + assert.Equal(t, int64(1), count) +} + +func TestGetFeeds2(t *testing.T) { + // test with an organization user + assert.NoError(t, unittest.PrepareTestDatabase()) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedUser: org, + Actor: user, + IncludePrivate: true, + OnlyPerformedBy: false, + IncludeDeleted: true, + }) + assert.NoError(t, err) + assert.Len(t, actions, 1) + if assert.Len(t, actions, 1) { + assert.EqualValues(t, 2, actions[0].ID) + assert.EqualValues(t, org.ID, actions[0].UserID) + } + assert.Equal(t, int64(1), count) + + actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedUser: org, + Actor: user, + IncludePrivate: false, + OnlyPerformedBy: false, + IncludeDeleted: true, + }) + assert.NoError(t, err) + assert.Len(t, actions, 0) + assert.Equal(t, int64(0), count) +} + +func TestGetFeedsCorrupted(t *testing.T) { + // Now we will not check for corrupted data in the feeds + // users should run doctor to fix their data + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ + ID: 8, + RepoID: 1700, + }) + + actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedUser: user, + Actor: user, + IncludePrivate: true, + }) + assert.NoError(t, err) + assert.Len(t, actions, 1) + assert.Equal(t, int64(1), count) +} + +func TestRepoActions(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + _ = db.TruncateBeans(db.DefaultContext, &activities_model.Action{}) + for i := 0; i < 3; i++ { + _ = db.Insert(db.DefaultContext, &activities_model.Action{ + UserID: 2 + int64(i), + ActUserID: 2, + RepoID: repo.ID, + OpType: activities_model.ActionCommentIssue, + }) + } + count, _ := db.Count[activities_model.Action](db.DefaultContext, &db.ListOptions{}) + assert.EqualValues(t, 3, count) + actions, _, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + RequestedRepo: repo, + }) + assert.NoError(t, err) + assert.Len(t, actions, 1) +} diff --git a/services/feed/action.go b/services/feed/notifier.go similarity index 100% rename from services/feed/action.go rename to services/feed/notifier.go diff --git a/services/feed/action_test.go b/services/feed/notifier_test.go similarity index 100% rename from services/feed/action_test.go rename to services/feed/notifier_test.go diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 8eee32a8c6..ee2c8c0963 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -29,9 +29,8 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload" + sender_service "code.gitea.io/gitea/services/mailer/sender" "code.gitea.io/gitea/services/mailer/token" - - "gopkg.in/gomail.v2" ) const ( @@ -60,7 +59,7 @@ func SendTestMail(email string) error { // No mail service configured return nil } - return gomail.Send(Sender, NewMessage(email, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) + return sender_service.Send(sender, sender_service.NewMessage(email, "Gitea Test Email!", "Gitea Test Email!")) } // sendUserMail sends a mail to the user @@ -82,7 +81,7 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s return } - msg := NewMessage(u.EmailTo(), subject, content.String()) + msg := sender_service.NewMessage(u.EmailTo(), subject, content.String()) msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info) SendAsync(msg) @@ -130,7 +129,7 @@ func SendActivateEmailMail(u *user_model.User, email string) { return } - msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String()) + msg := sender_service.NewMessage(email, locale.TrString("mail.activate_email"), content.String()) msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID) SendAsync(msg) @@ -158,7 +157,7 @@ func SendRegisterNotifyMail(u *user_model.User) { return } - msg := NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String()) + msg := sender_service.NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String()) msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID) SendAsync(msg) @@ -189,13 +188,13 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository) return } - msg := NewMessage(u.EmailTo(), subject, content.String()) + msg := sender_service.NewMessage(u.EmailTo(), subject, content.String()) msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID) SendAsync(msg) } -func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipients []*user_model.User, fromMention bool, info string) ([]*Message, error) { +func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipients []*user_model.User, fromMention bool, info string) ([]*sender_service.Message, error) { var ( subject string link string @@ -304,9 +303,9 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient return nil, err } - msgs := make([]*Message, 0, len(recipients)) + msgs := make([]*sender_service.Message, 0, len(recipients)) for _, recipient := range recipients { - msg := NewMessageFrom( + msg := sender_service.NewMessageFrom( recipient.Email, fromDisplayName(ctx.Doer), setting.MailService.FromEmail, diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index af1a7a2662..1d73d77612 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" + sender_service "code.gitea.io/gitea/services/mailer/sender" ) const ( @@ -80,11 +81,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re return } - msgs := make([]*Message, 0, len(tos)) + msgs := make([]*sender_service.Message, 0, len(tos)) publisherName := fromDisplayName(rel.Publisher) msgID := generateMessageIDForRelease(rel) for _, to := range tos { - msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String()) + msg := sender_service.NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String()) msg.Info = subject msg.SetHeader("Message-ID", msgID) msgs = append(msgs, msg) diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go index 7003584786..5f80654bcd 100644 --- a/services/mailer/mail_repo.go +++ b/services/mailer/mail_repo.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" + sender_service "code.gitea.io/gitea/services/mailer/sender" ) // SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created @@ -79,7 +80,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U } for _, to := range emailTos { - msg := NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String()) + msg := sender_service.NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String()) msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID) SendAsync(msg) diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index ceecefa50f..4f2d5e4ca7 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" + sender_service "code.gitea.io/gitea/services/mailer/sender" ) const ( @@ -67,7 +68,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod return err } - msg := NewMessage(invite.Email, subject, mailBody.String()) + msg := sender_service.NewMessage(invite.Email, subject, mailBody.String()) msg.Info = subject SendAsync(msg) diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 663ffa85ef..42de7599eb 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -23,6 +23,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + sender_service "code.gitea.io/gitea/services/mailer/sender" "github.com/stretchr/testify/assert" ) @@ -167,7 +168,7 @@ func TestTemplateSelection(t *testing.T) { template.Must(bodyTemplates.New("pull/comment").Parse("pull/comment/body")) template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body")) - expect := func(t *testing.T, msg *Message, expSubject, expBody string) { + expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) { subject := msg.ToMessage().GetHeader("Subject") msgbuf := new(bytes.Buffer) _, _ = msg.ToMessage().WriteTo(msgbuf) @@ -252,7 +253,7 @@ func TestTemplateServices(t *testing.T) { "//Re: //") } -func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recipients []*user_model.User, fromMention bool, info string) *Message { +func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recipients []*user_model.User, fromMention bool, info string) *sender_service.Message { msgs, err := composeIssueCommentMessages(ctx, "en-US", recipients, fromMention, info) assert.NoError(t, err) assert.Len(t, msgs, 1) diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index 5cb6d03521..bf4b5a43cb 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -5,391 +5,21 @@ package mailer import ( - "bytes" "context" - "crypto/tls" - "fmt" - "hash/fnv" - "io" - "net" - "net/smtp" - "os" - "os/exec" - "strings" - "time" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + sender_service "code.gitea.io/gitea/services/mailer/sender" notify_service "code.gitea.io/gitea/services/notify" - - ntlmssp "github.com/Azure/go-ntlmssp" - "github.com/jaytaylor/html2text" - "gopkg.in/gomail.v2" ) -// Message mail body and log info -type Message struct { - Info string // Message information for log purpose. - FromAddress string - FromDisplayName string - To string // Use only one recipient to prevent leaking of addresses - ReplyTo string - Subject string - Date time.Time - Body string - Headers map[string][]string -} +var mailQueue *queue.WorkerPoolQueue[*sender_service.Message] -// ToMessage converts a Message to gomail.Message -func (m *Message) ToMessage() *gomail.Message { - msg := gomail.NewMessage() - msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) - msg.SetHeader("To", m.To) - if m.ReplyTo != "" { - msg.SetHeader("Reply-To", m.ReplyTo) - } - for header := range m.Headers { - msg.SetHeader(header, m.Headers[header]...) - } - - if setting.MailService.SubjectPrefix != "" { - msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) - } else { - msg.SetHeader("Subject", m.Subject) - } - msg.SetDateHeader("Date", m.Date) - msg.SetHeader("X-Auto-Response-Suppress", "All") - - plainBody, err := html2text.FromString(m.Body) - if err != nil || setting.MailService.SendAsPlainText { - if strings.Contains(base.TruncateString(m.Body, 100), "") { - log.Warn("Mail contains HTML but configured to send as plain text.") - } - msg.SetBody("text/plain", plainBody) - } else { - msg.SetBody("text/plain", plainBody) - msg.AddAlternative("text/html", m.Body) - } - - if len(msg.GetHeader("Message-ID")) == 0 { - msg.SetHeader("Message-ID", m.generateAutoMessageID()) - } - - for k, v := range setting.MailService.OverrideHeader { - if len(msg.GetHeader(k)) != 0 { - log.Debug("Mailer override header '%s' as per config", k) - } - msg.SetHeader(k, v...) - } - - return msg -} - -// SetHeader adds additional headers to a message -func (m *Message) SetHeader(field string, value ...string) { - m.Headers[field] = value -} - -func (m *Message) generateAutoMessageID() string { - dateMs := m.Date.UnixNano() / 1e6 - h := fnv.New64() - if len(m.To) > 0 { - _, _ = h.Write([]byte(m.To)) - } - _, _ = h.Write([]byte(m.Subject)) - _, _ = h.Write([]byte(m.Body)) - return fmt.Sprintf("", dateMs, h.Sum64(), setting.Domain) -} - -// NewMessageFrom creates new mail message object with custom From header. -func NewMessageFrom(to, fromDisplayName, fromAddress, subject, body string) *Message { - log.Trace("NewMessageFrom (body):\n%s", body) - - return &Message{ - FromAddress: fromAddress, - FromDisplayName: fromDisplayName, - To: to, - Subject: subject, - Date: time.Now(), - Body: body, - Headers: map[string][]string{}, - } -} - -// NewMessage creates new mail message object with default From header. -func NewMessage(to, subject, body string) *Message { - return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body) -} - -type loginAuth struct { - username, password string -} - -// LoginAuth SMTP AUTH LOGIN Auth Handler -func LoginAuth(username, password string) smtp.Auth { - return &loginAuth{username, password} -} - -// Start start SMTP login auth -func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { - return "LOGIN", []byte{}, nil -} - -// Next next step of SMTP login auth -func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { - if more { - switch string(fromServer) { - case "Username:": - return []byte(a.username), nil - case "Password:": - return []byte(a.password), nil - default: - return nil, fmt.Errorf("unknown fromServer: %s", string(fromServer)) - } - } - return nil, nil -} - -type ntlmAuth struct { - username, password, domain string - domainNeeded bool -} - -// NtlmAuth SMTP AUTH NTLM Auth Handler -func NtlmAuth(username, password string) smtp.Auth { - user, domain, domainNeeded := ntlmssp.GetDomain(username) - return &ntlmAuth{user, password, domain, domainNeeded} -} - -// Start starts SMTP NTLM Auth -func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { - negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, "") - return "NTLM", negotiateMessage, err -} - -// Next next step of SMTP ntlm auth -func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) { - if more { - if len(fromServer) == 0 { - return nil, fmt.Errorf("ntlm ChallengeMessage is empty") - } - authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded) - return authenticateMessage, err - } - return nil, nil -} - -// Sender SMTP mail sender -type smtpSender struct{} - -// Send send email -func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { - opts := setting.MailService - - var network string - var address string - if opts.Protocol == "smtp+unix" { - network = "unix" - address = opts.SMTPAddr - } else { - network = "tcp" - address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort) - } - - conn, err := net.Dial(network, address) - if err != nil { - return fmt.Errorf("failed to establish network connection to SMTP server: %w", err) - } - defer conn.Close() - - var tlsconfig *tls.Config - if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" { - tlsconfig = &tls.Config{ - InsecureSkipVerify: opts.ForceTrustServerCert, - ServerName: opts.SMTPAddr, - } - - if opts.UseClientCert { - cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile) - if err != nil { - return fmt.Errorf("could not load SMTP client certificate: %w", err) - } - tlsconfig.Certificates = []tls.Certificate{cert} - } - } - - if opts.Protocol == "smtps" { - conn = tls.Client(conn, tlsconfig) - } - - host := "localhost" - if opts.Protocol == "smtp+unix" { - host = opts.SMTPAddr - } - client, err := smtp.NewClient(conn, host) - if err != nil { - return fmt.Errorf("could not initiate SMTP session: %w", err) - } - - if opts.EnableHelo { - hostname := opts.HeloHostname - if len(hostname) == 0 { - hostname, err = os.Hostname() - if err != nil { - return fmt.Errorf("could not retrieve system hostname: %w", err) - } - } - - if err = client.Hello(hostname); err != nil { - return fmt.Errorf("failed to issue HELO command: %w", err) - } - } - - if opts.Protocol == "smtp+starttls" { - hasStartTLS, _ := client.Extension("STARTTLS") - if hasStartTLS { - if err = client.StartTLS(tlsconfig); err != nil { - return fmt.Errorf("failed to start TLS connection: %w", err) - } - } else { - log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP") - } - } - - canAuth, options := client.Extension("AUTH") - if len(opts.User) > 0 { - if !canAuth { - return fmt.Errorf("SMTP server does not support AUTH, but credentials provided") - } - - var auth smtp.Auth - - if strings.Contains(options, "CRAM-MD5") { - auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd) - } else if strings.Contains(options, "PLAIN") { - auth = smtp.PlainAuth("", opts.User, opts.Passwd, host) - } else if strings.Contains(options, "LOGIN") { - // Patch for AUTH LOGIN - auth = LoginAuth(opts.User, opts.Passwd) - } else if strings.Contains(options, "NTLM") { - auth = NtlmAuth(opts.User, opts.Passwd) - } - - if auth != nil { - if err = client.Auth(auth); err != nil { - return fmt.Errorf("failed to authenticate SMTP: %w", err) - } - } - } - - if opts.OverrideEnvelopeFrom { - if err = client.Mail(opts.EnvelopeFrom); err != nil { - return fmt.Errorf("failed to issue MAIL command: %w", err) - } - } else { - if err = client.Mail(from); err != nil { - return fmt.Errorf("failed to issue MAIL command: %w", err) - } - } - - for _, rec := range to { - if err = client.Rcpt(rec); err != nil { - return fmt.Errorf("failed to issue RCPT command: %w", err) - } - } - - w, err := client.Data() - if err != nil { - return fmt.Errorf("failed to issue DATA command: %w", err) - } else if _, err = msg.WriteTo(w); err != nil { - return fmt.Errorf("SMTP write failed: %w", err) - } else if err = w.Close(); err != nil { - return fmt.Errorf("SMTP close failed: %w", err) - } - - return client.Quit() -} - -// Sender sendmail mail sender -type sendmailSender struct{} - -// Send send email -func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error { - var err error - var closeError error - var waitError error - - envelopeFrom := from - if setting.MailService.OverrideEnvelopeFrom { - envelopeFrom = setting.MailService.EnvelopeFrom - } - - args := []string{"-f", envelopeFrom, "-i"} - args = append(args, setting.MailService.SendmailArgs...) - args = append(args, to...) - log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args) - - desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args) - - ctx, _, finished := process.GetManager().AddContextTimeout(graceful.GetManager().HammerContext(), setting.MailService.SendmailTimeout, desc) - defer finished() - - cmd := exec.CommandContext(ctx, setting.MailService.SendmailPath, args...) - pipe, err := cmd.StdinPipe() - if err != nil { - return err - } - process.SetSysProcAttribute(cmd) - - if err = cmd.Start(); err != nil { - _ = pipe.Close() - return err - } - - if setting.MailService.SendmailConvertCRLF { - buf := &strings.Builder{} - _, err = msg.WriteTo(buf) - if err == nil { - _, err = strings.NewReplacer("\r\n", "\n").WriteString(pipe, buf.String()) - } - } else { - _, err = msg.WriteTo(pipe) - } - - // we MUST close the pipe or sendmail will hang waiting for more of the message - // Also we should wait on our sendmail command even if something fails - closeError = pipe.Close() - waitError = cmd.Wait() - if err != nil { - return err - } else if closeError != nil { - return closeError - } - return waitError -} - -// Sender sendmail mail sender -type dummySender struct{} - -// Send send email -func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error { - buf := bytes.Buffer{} - if _, err := msg.WriteTo(&buf); err != nil { - return err - } - log.Debug("Mail From: %s To: %v Body: %s", from, to, buf.String()) - return nil -} - -var mailQueue *queue.WorkerPoolQueue[*Message] - -// Sender sender for sending mail synchronously -var Sender gomail.Sender +// sender sender for sending mail synchronously +var sender sender_service.Sender // NewContext start mail queue service func NewContext(ctx context.Context) { @@ -406,20 +36,20 @@ func NewContext(ctx context.Context) { switch setting.MailService.Protocol { case "sendmail": - Sender = &sendmailSender{} + sender = &sender_service.SendmailSender{} case "dummy": - Sender = &dummySender{} + sender = &sender_service.DummySender{} default: - Sender = &smtpSender{} + sender = &sender_service.SMTPSender{} } subjectTemplates, bodyTemplates = templates.Mailer(ctx) - mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*Message) []*Message { + mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*sender_service.Message) []*sender_service.Message { for _, msg := range items { gomailMsg := msg.ToMessage() log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info) - if err := gomail.Send(Sender, gomailMsg); err != nil { + if err := sender_service.Send(sender, msg); err != nil { log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err) } else { log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info) @@ -436,7 +66,7 @@ func NewContext(ctx context.Context) { // SendAsync send emails asynchronously (make it mockable) var SendAsync = sendAsync -func sendAsync(msgs ...*Message) { +func sendAsync(msgs ...*sender_service.Message) { if setting.MailService == nil { log.Error("Mailer: SendAsync is being invoked but mail service hasn't been initialized") return diff --git a/services/mailer/sender/dummy.go b/services/mailer/sender/dummy.go new file mode 100644 index 0000000000..dd5f14abec --- /dev/null +++ b/services/mailer/sender/dummy.go @@ -0,0 +1,26 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "bytes" + "io" + + "code.gitea.io/gitea/modules/log" +) + +// DummySender Sender sendmail mail sender +type DummySender struct{} + +var _ Sender = &DummySender{} + +// Send send email +func (s *DummySender) Send(from string, to []string, msg io.WriterTo) error { + buf := bytes.Buffer{} + if _, err := msg.WriteTo(&buf); err != nil { + return err + } + log.Debug("Mail From: %s To: %v Body: %s", from, to, buf.String()) + return nil +} diff --git a/services/mailer/sender/message.go b/services/mailer/sender/message.go new file mode 100644 index 0000000000..a3255692f0 --- /dev/null +++ b/services/mailer/sender/message.go @@ -0,0 +1,112 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "fmt" + "hash/fnv" + "strings" + "time" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/jaytaylor/html2text" + "gopkg.in/gomail.v2" +) + +// Message mail body and log info +type Message struct { + Info string // Message information for log purpose. + FromAddress string + FromDisplayName string + To string // Use only one recipient to prevent leaking of addresses + ReplyTo string + Subject string + Date time.Time + Body string + Headers map[string][]string +} + +// ToMessage converts a Message to gomail.Message +func (m *Message) ToMessage() *gomail.Message { + msg := gomail.NewMessage() + msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) + msg.SetHeader("To", m.To) + if m.ReplyTo != "" { + msg.SetHeader("Reply-To", m.ReplyTo) + } + for header := range m.Headers { + msg.SetHeader(header, m.Headers[header]...) + } + + if setting.MailService.SubjectPrefix != "" { + msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) + } else { + msg.SetHeader("Subject", m.Subject) + } + msg.SetDateHeader("Date", m.Date) + msg.SetHeader("X-Auto-Response-Suppress", "All") + + plainBody, err := html2text.FromString(m.Body) + if err != nil || setting.MailService.SendAsPlainText { + if strings.Contains(base.TruncateString(m.Body, 100), "") { + log.Warn("Mail contains HTML but configured to send as plain text.") + } + msg.SetBody("text/plain", plainBody) + } else { + msg.SetBody("text/plain", plainBody) + msg.AddAlternative("text/html", m.Body) + } + + if len(msg.GetHeader("Message-ID")) == 0 { + msg.SetHeader("Message-ID", m.generateAutoMessageID()) + } + + for k, v := range setting.MailService.OverrideHeader { + if len(msg.GetHeader(k)) != 0 { + log.Debug("Mailer override header '%s' as per config", k) + } + msg.SetHeader(k, v...) + } + + return msg +} + +// SetHeader adds additional headers to a message +func (m *Message) SetHeader(field string, value ...string) { + m.Headers[field] = value +} + +func (m *Message) generateAutoMessageID() string { + dateMs := m.Date.UnixNano() / 1e6 + h := fnv.New64() + if len(m.To) > 0 { + _, _ = h.Write([]byte(m.To)) + } + _, _ = h.Write([]byte(m.Subject)) + _, _ = h.Write([]byte(m.Body)) + return fmt.Sprintf("", dateMs, h.Sum64(), setting.Domain) +} + +// NewMessageFrom creates new mail message object with custom From header. +func NewMessageFrom(to, fromDisplayName, fromAddress, subject, body string) *Message { + log.Trace("NewMessageFrom (body):\n%s", body) + + return &Message{ + FromAddress: fromAddress, + FromDisplayName: fromDisplayName, + To: to, + Subject: subject, + Date: time.Now(), + Body: body, + Headers: map[string][]string{}, + } +} + +// NewMessage creates new mail message object with default From header. +func NewMessage(to, subject, body string) *Message { + return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body) +} diff --git a/services/mailer/mailer_test.go b/services/mailer/sender/message_test.go similarity index 97% rename from services/mailer/mailer_test.go rename to services/mailer/sender/message_test.go index 6d7c44f40c..d47052685e 100644 --- a/services/mailer/mailer_test.go +++ b/services/mailer/sender/message_test.go @@ -1,7 +1,7 @@ -// Copyright 2021 The Gogs Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package mailer +package sender import ( "strings" diff --git a/services/mailer/sender/sender.go b/services/mailer/sender/sender.go new file mode 100644 index 0000000000..bf317aa846 --- /dev/null +++ b/services/mailer/sender/sender.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "gopkg.in/gomail.v2" +) + +type Sender gomail.Sender + +var Send = send + +func send(sender Sender, msgs ...*Message) error { + if setting.MailService == nil { + log.Error("Mailer: Send is being invoked but mail service hasn't been initialized") + return nil + } + goMsgs := []*gomail.Message{} + for _, msg := range msgs { + goMsgs = append(goMsgs, msg.ToMessage()) + } + return gomail.Send(sender, goMsgs...) +} diff --git a/services/mailer/sender/sendmail.go b/services/mailer/sender/sendmail.go new file mode 100644 index 0000000000..64c7f8f081 --- /dev/null +++ b/services/mailer/sender/sendmail.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "fmt" + "io" + "os/exec" + "strings" + + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" +) + +// SendmailSender Sender sendmail mail sender +type SendmailSender struct{} + +var _ Sender = &SendmailSender{} + +// Send send email +func (s *SendmailSender) Send(from string, to []string, msg io.WriterTo) error { + var err error + var closeError error + var waitError error + + envelopeFrom := from + if setting.MailService.OverrideEnvelopeFrom { + envelopeFrom = setting.MailService.EnvelopeFrom + } + + args := []string{"-f", envelopeFrom, "-i"} + args = append(args, setting.MailService.SendmailArgs...) + args = append(args, to...) + log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args) + + desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args) + + ctx, _, finished := process.GetManager().AddContextTimeout(graceful.GetManager().HammerContext(), setting.MailService.SendmailTimeout, desc) + defer finished() + + cmd := exec.CommandContext(ctx, setting.MailService.SendmailPath, args...) + pipe, err := cmd.StdinPipe() + if err != nil { + return err + } + process.SetSysProcAttribute(cmd) + + if err = cmd.Start(); err != nil { + _ = pipe.Close() + return err + } + + if setting.MailService.SendmailConvertCRLF { + buf := &strings.Builder{} + _, err = msg.WriteTo(buf) + if err == nil { + _, err = strings.NewReplacer("\r\n", "\n").WriteString(pipe, buf.String()) + } + } else { + _, err = msg.WriteTo(pipe) + } + + // we MUST close the pipe or sendmail will hang waiting for more of the message + // Also we should wait on our sendmail command even if something fails + closeError = pipe.Close() + waitError = cmd.Wait() + if err != nil { + return err + } else if closeError != nil { + return closeError + } + return waitError +} diff --git a/services/mailer/sender/smtp.go b/services/mailer/sender/smtp.go new file mode 100644 index 0000000000..ab49b7e5f8 --- /dev/null +++ b/services/mailer/sender/smtp.go @@ -0,0 +1,150 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "crypto/tls" + "fmt" + "io" + "net" + "net/smtp" + "os" + "strings" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +// SMTPSender Sender SMTP mail sender +type SMTPSender struct{} + +var _ Sender = &SMTPSender{} + +// Send send email +func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error { + opts := setting.MailService + + var network string + var address string + if opts.Protocol == "smtp+unix" { + network = "unix" + address = opts.SMTPAddr + } else { + network = "tcp" + address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort) + } + + conn, err := net.Dial(network, address) + if err != nil { + return fmt.Errorf("failed to establish network connection to SMTP server: %w", err) + } + defer conn.Close() + + var tlsconfig *tls.Config + if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" { + tlsconfig = &tls.Config{ + InsecureSkipVerify: opts.ForceTrustServerCert, + ServerName: opts.SMTPAddr, + } + + if opts.UseClientCert { + cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile) + if err != nil { + return fmt.Errorf("could not load SMTP client certificate: %w", err) + } + tlsconfig.Certificates = []tls.Certificate{cert} + } + } + + if opts.Protocol == "smtps" { + conn = tls.Client(conn, tlsconfig) + } + + host := "localhost" + if opts.Protocol == "smtp+unix" { + host = opts.SMTPAddr + } + client, err := smtp.NewClient(conn, host) + if err != nil { + return fmt.Errorf("could not initiate SMTP session: %w", err) + } + + if opts.EnableHelo { + hostname := opts.HeloHostname + if len(hostname) == 0 { + hostname, err = os.Hostname() + if err != nil { + return fmt.Errorf("could not retrieve system hostname: %w", err) + } + } + + if err = client.Hello(hostname); err != nil { + return fmt.Errorf("failed to issue HELO command: %w", err) + } + } + + if opts.Protocol == "smtp+starttls" { + hasStartTLS, _ := client.Extension("STARTTLS") + if hasStartTLS { + if err = client.StartTLS(tlsconfig); err != nil { + return fmt.Errorf("failed to start TLS connection: %w", err) + } + } else { + log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP") + } + } + + canAuth, options := client.Extension("AUTH") + if len(opts.User) > 0 { + if !canAuth { + return fmt.Errorf("SMTP server does not support AUTH, but credentials provided") + } + + var auth smtp.Auth + + if strings.Contains(options, "CRAM-MD5") { + auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd) + } else if strings.Contains(options, "PLAIN") { + auth = smtp.PlainAuth("", opts.User, opts.Passwd, host) + } else if strings.Contains(options, "LOGIN") { + // Patch for AUTH LOGIN + auth = LoginAuth(opts.User, opts.Passwd) + } else if strings.Contains(options, "NTLM") { + auth = NtlmAuth(opts.User, opts.Passwd) + } + + if auth != nil { + if err = client.Auth(auth); err != nil { + return fmt.Errorf("failed to authenticate SMTP: %w", err) + } + } + } + + if opts.OverrideEnvelopeFrom { + if err = client.Mail(opts.EnvelopeFrom); err != nil { + return fmt.Errorf("failed to issue MAIL command: %w", err) + } + } else { + if err = client.Mail(from); err != nil { + return fmt.Errorf("failed to issue MAIL command: %w", err) + } + } + + for _, rec := range to { + if err = client.Rcpt(rec); err != nil { + return fmt.Errorf("failed to issue RCPT command: %w", err) + } + } + + w, err := client.Data() + if err != nil { + return fmt.Errorf("failed to issue DATA command: %w", err) + } else if _, err = msg.WriteTo(w); err != nil { + return fmt.Errorf("SMTP write failed: %w", err) + } else if err = w.Close(); err != nil { + return fmt.Errorf("SMTP close failed: %w", err) + } + + return client.Quit() +} diff --git a/services/mailer/sender/smtp_auth.go b/services/mailer/sender/smtp_auth.go new file mode 100644 index 0000000000..df65498a5a --- /dev/null +++ b/services/mailer/sender/smtp_auth.go @@ -0,0 +1,69 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "fmt" + "net/smtp" + + "github.com/Azure/go-ntlmssp" +) + +type loginAuth struct { + username, password string +} + +// LoginAuth SMTP AUTH LOGIN Auth Handler +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +// Start start SMTP login auth +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +// Next next step of SMTP login auth +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + return nil, fmt.Errorf("unknown fromServer: %s", string(fromServer)) + } + } + return nil, nil +} + +type ntlmAuth struct { + username, password, domain string + domainNeeded bool +} + +// NtlmAuth SMTP AUTH NTLM Auth Handler +func NtlmAuth(username, password string) smtp.Auth { + user, domain, domainNeeded := ntlmssp.GetDomain(username) + return &ntlmAuth{user, password, domain, domainNeeded} +} + +// Start starts SMTP NTLM Auth +func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, "") + return "NTLM", negotiateMessage, err +} + +// Next next step of SMTP ntlm auth +func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + if len(fromServer) == 0 { + return nil, fmt.Errorf("ntlm ChallengeMessage is empty") + } + authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded) + return authenticateMessage, err + } + return nil, nil +} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index b6c26c8527..7e4c8854f5 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -7,10 +7,12 @@
-
- + +
+ {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}} +
- - - {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index c4f73875f2..b68c34a02a 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -1,87 +1,61 @@ {{/* Attributes: -* root * ContainerClasses -* (TODO: search "branch_dropdown" in the template directory) +* Repository +* CurrentRefType: eg. "branch", "tag" +* CurrentRefShortName: eg. "master", "v1.0" +* CurrentTreePath +* RefLinkTemplate: redirect to the link when a branch/tag is selected +* RefFormActionTemplate: change the parent form's action when a branch/tag is selected +* DropdownFixedText: the text to show in the dropdown (mainly used by "release page"), if empty, the text will be the branch/tag name +* ShowTabBranches +* ShowTabTagsTab +* AllowCreateNewRef +* ShowViewAllRefsEntry + +Search "repo/branch_dropdown" in the template directory to find all occurrences. */}} -{{$defaultSelectedRefName := $.root.BranchName}} -{{if and .root.IsViewTag (not .noTag)}} - {{$defaultSelectedRefName = .root.TagName}} -{{end}} -{{if eq $defaultSelectedRefName ""}} - {{$defaultSelectedRefName = $.root.Repository.DefaultBranch}} -{{end}} +
- const data = { - 'textReleaseCompare': {{ctx.Locale.Tr "repo.release.compare"}}, - 'textCreateTag': {{ctx.Locale.Tr "repo.tag.create_tag"}}, - 'textCreateBranch': {{ctx.Locale.Tr "repo.branch.create_branch"}}, - 'textCreateBranchFrom': {{ctx.Locale.Tr "repo.branch.create_from"}}, - 'textBranches': {{ctx.Locale.Tr "repo.branches"}}, - 'textTags': {{ctx.Locale.Tr "repo.tags"}}, - 'textDefaultBranchLabel': {{ctx.Locale.Tr "repo.default_branch_label"}}, - - 'mode': '{{if or .root.IsViewTag .isTag}}tags{{else}}branches{{end}}', - 'showBranchesInDropdown': {{$showBranchesInDropdown}}, - 'searchFieldPlaceholder': '{{if $.noTag}}{{ctx.Locale.Tr "repo.pulls.filter_branch"}}{{else if $showBranchesInDropdown}}{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}{{else}}{{ctx.Locale.Tr "repo.find_tag"}}{{end}}...', - 'branchForm': {{$.branchForm}}, - 'disableCreateBranch': {{if .disableCreateBranch}}{{.disableCreateBranch}}{{else}}{{not .root.CanCreateBranch}}{{end}}, - 'setAction': {{.setAction}}, - 'submitForm': {{.submitForm}}, - 'viewType': {{$type}}, - 'refName': {{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}, - 'commitIdShort': {{ShortSha .root.CommitID}}, - 'tagName': {{.root.TagName}}, - 'branchName': {{.root.BranchName}}, - 'noTag': {{.noTag}}, - 'defaultSelectedRefName': {{$defaultSelectedRefName}}, - 'repoDefaultBranch': {{.root.Repository.DefaultBranch}}, - 'enableFeed': {{.root.EnableFeed}}, - 'rssURLPrefix': '{{$.root.RepoLink}}/rss/branch/', - 'branchURLPrefix': '{{if .branchURLPrefix}}{{.branchURLPrefix}}{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/branch/{{end}}', - 'branchURLSuffix': '{{if .branchURLSuffix}}{{.branchURLSuffix}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}', - 'tagURLPrefix': '{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if .release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}', - 'tagURLSuffix': '{{if .tagURLSuffix}}{{.tagURLSuffix}}{{else if .release}}...{{if .release.IsDraft}}{{PathEscapeSegments .release.Target}}{{else}}{{if .release.TagName}}{{PathEscapeSegments .release.TagName}}{{else}}{{PathEscapeSegments .release.Sha1}}{{end}}{{end}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}', - 'repoLink': {{.root.RepoLink}}, - 'treePath': {{.root.TreePath}}, - 'branchNameSubURL': {{.root.BranchNameSubURL}}, - 'noResults': {{ctx.Locale.Tr "no_results_found"}}, - }; - {{if .release}} - data.release = { - 'tagName': {{.release.TagName}}, - }; - {{end}} - window.config.pageData.branchDropdownDataList = window.config.pageData.branchDropdownDataList || []; - window.config.pageData.branchDropdownDataList.push(data); - - -
+ data-enable-feed="{{ctx.RootData.EnableFeed}}" +> {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}

- {{template "repo/branch_dropdown" dict "root" . - "noTag" true "disableCreateBranch" true - "branchForm" "branch-dropdown-form" - "branchURLPrefix" (printf "%s/_cherrypick/%s/" $.RepoLink .CommitID) "branchURLSuffix" "" - "setAction" true "submitForm" true}} -
- - + + + {{template "repo/branch_dropdown" dict + "Repository" .Repository + "ShowTabBranches" true + "CurrentRefType" "branch" + "CurrentRefShortName" (Iif $.BranchName $.Repository.DefaultBranch) + "RefFormActionTemplate" (print "{RepoLink}/_cherrypick/" .CommitID "/{RefShortName}") + }}
diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl index 6bce585774..7065bf33f4 100644 --- a/templates/repo/commits.tmpl +++ b/templates/repo/commits.tmpl @@ -5,7 +5,24 @@ {{template "repo/sub_menu" .}}
- {{template "repo/branch_dropdown" dict "root" .}} + + {{$branchDropdownCurrentRefType := "branch"}} + {{$branchDropdownCurrentRefShortName := .BranchName}} + {{if .IsViewTag}} + {{$branchDropdownCurrentRefType = "tag"}} + {{$branchDropdownCurrentRefShortName = .TagName}} + {{end}} + {{template "repo/branch_dropdown" dict + "Repository" .Repository + "ShowTabBranches" true + "ShowTabTags" true + "CurrentRefType" $branchDropdownCurrentRefType + "CurrentRefShortName" $branchDropdownCurrentRefShortName + "CurrentTreePath" .TreePath + "RefLinkTemplate" "{RepoLink}/commits/{RefType}/{RefShortName}/{TreePath}" + "AllowCreateNewRef" .CanCreateBranch + }} + {{svg "octicon-git-branch"}} {{ctx.Locale.Tr "repo.commit_graph"}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 12c4a17234..4b25915c27 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -47,7 +47,23 @@ {{$isHomepage := (eq $n 0)}}
- {{template "repo/branch_dropdown" dict "root" .}} + {{$branchDropdownCurrentRefType := "branch"}} + {{$branchDropdownCurrentRefShortName := .BranchName}} + {{if .IsViewTag}} + {{$branchDropdownCurrentRefType = "tag"}} + {{$branchDropdownCurrentRefShortName = .TagName}} + {{end}} + {{template "repo/branch_dropdown" dict + "Repository" .Repository + "ShowTabBranches" true + "ShowTabTags" true + "CurrentRefType" $branchDropdownCurrentRefType + "CurrentRefShortName" $branchDropdownCurrentRefShortName + "CurrentTreePath" .TreePath + "RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}" + "AllowCreateNewRef" .CanCreateBranch + "ShowViewAllRefsEntry" true + }} {{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} {{$cmpBranch := ""}} {{if ne .Repository.ID .BaseRepo.ID}} diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index 4cdad7a748..8e2410393d 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -58,7 +58,7 @@ {{end}} {{if .ClosedMilestones}}
-
{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}
+
{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}
{{range .ClosedMilestones}}
{{.Name}} diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl index 736a75d73a..96a3879b80 100644 --- a/templates/repo/issue/milestone_new.tmpl +++ b/templates/repo/issue/milestone_new.tmpl @@ -5,7 +5,7 @@
{{template "repo/issue/navbar" .}} {{if and (or .CanWriteIssues .CanWritePulls) .PageIsEditMilestone}} - diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index 08507e545d..6969003968 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -48,7 +48,10 @@
{{end}} - {{template "user/auth/oauth_container" .}} + {{if and .OAuth2Providers .EnableOpenIDSignIn}} +
{{ctx.Locale.Tr "sign_in_or"}}
+ {{template "user/auth/oauth_container" .}} + {{end}}
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index a7b2464069..27b0ef10c7 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -40,7 +40,7 @@ {{ctx.Locale.Tr "settings.manage_emails"}}
-
+
{{if $.EnableNotifyMail}}
{{ctx.Locale.Tr "settings.email_desc"}}
@@ -65,27 +65,34 @@
{{end}} {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}} - {{range .Emails}} -
- {{if not .IsPrimary}} -
+ {{range .Emails}} +
+
+ {{.Email}} + {{if .IsPrimary}} +
{{ctx.Locale.Tr "settings.primary"}}
+ {{end}} + {{if .IsActivated}} +
{{ctx.Locale.Tr "settings.activated"}}
+ {{else}} +
{{ctx.Locale.Tr "settings.requires_activation"}}
+ {{end}} +
+
+ {{if not .IsPrimary}} -
- {{if .CanBePrimary}} -
+ {{if .CanBePrimary}}
{{$.CsrfTokenHtml}}
-
+ {{end}} {{end}} - {{end}} - {{if not .IsActivated}} -
+ {{if not .IsActivated}}
{{$.CsrfTokenHtml}} @@ -96,22 +103,11 @@ {{end}}
+ {{end}}
- {{end}} -
- {{.Email}} - {{if .IsPrimary}} -
{{ctx.Locale.Tr "settings.primary"}}
- {{end}} - {{if .IsActivated}} -
{{ctx.Locale.Tr "settings.activated"}}
- {{else}} -
{{ctx.Locale.Tr "settings.requires_activation"}}
- {{end}}
-
- {{end}} - {{end}} + {{end}}{{/* range Emails */}} + {{end}}{{/* if manage_credentials */}}
{{end}} diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go index de5797f289..29e9930538 100644 --- a/tests/integration/api_actions_artifact_test.go +++ b/tests/integration/api_actions_artifact_test.go @@ -144,12 +144,12 @@ func TestActionsArtifactDownload(t *testing.T) { var downloadResp downloadArtifactResponse DecodeJSON(t, resp, &downloadResp) assert.Len(t, downloadResp.Value, 1) - assert.Equal(t, "artifact-download/abc.txt", downloadResp.Value[artifactIdx].Path) - assert.Equal(t, "file", downloadResp.Value[artifactIdx].ItemType) - assert.Contains(t, downloadResp.Value[artifactIdx].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") + assert.Equal(t, "artifact-download/abc.txt", downloadResp.Value[0].Path) + assert.Equal(t, "file", downloadResp.Value[0].ItemType) + assert.Contains(t, downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") - idx = strings.Index(downloadResp.Value[artifactIdx].ContentLocation, "/api/actions_pipeline/_apis/pipelines/") - url = downloadResp.Value[artifactIdx].ContentLocation[idx:] + idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/") + url = downloadResp.Value[0].ContentLocation[idx:] req = NewRequest(t, "GET", url). AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") resp = MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/incoming_email_test.go b/tests/integration/incoming_email_test.go index 88571303ac..e968a2956e 100644 --- a/tests/integration/incoming_email_test.go +++ b/tests/integration/incoming_email_test.go @@ -19,11 +19,11 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/mailer/incoming" incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload" + sender_service "code.gitea.io/gitea/services/mailer/sender" token_service "code.gitea.io/gitea/services/mailer/token" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" - "gopkg.in/gomail.v2" ) func TestIncomingEmail(t *testing.T) { @@ -189,11 +189,15 @@ func TestIncomingEmail(t *testing.T) { token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload) assert.NoError(t, err) - msg := gomail.NewMessage() - msg.SetHeader("To", strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)) - msg.SetHeader("From", user.Email) - msg.SetBody("text/plain", token) - err = gomail.Send(&smtpTestSender{}, msg) + msg := sender_service.NewMessageFrom( + strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1), + "", + user.Email, + "", + token, + ) + + err = sender_service.Send(&smtpTestSender{}, msg) assert.NoError(t, err) assert.Eventually(t, func() bool { diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 886d4a8259..abad9eb5e5 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" @@ -91,3 +92,31 @@ func TestSigninWithRememberMe(t *testing.T) { req = NewRequest(t, "GET", "/user/settings") session.MakeRequest(t, req, http.StatusOK) } + +func TestEnablePasswordSignInForm(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("EnablePasswordSignInForm=false", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, false)() + + req := NewRequest(t, "GET", "/user/login") + resp := MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/login']", false) + + req = NewRequest(t, "POST", "/user/login") + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("EnablePasswordSignInForm=true", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)() + + req := NewRequest(t, "GET", "/user/login") + resp := MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/login']", true) + + req = NewRequest(t, "POST", "/user/login") + MakeRequest(t, req, http.StatusOK) + }) +} diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css index 32c71e802b..73760390de 100644 --- a/web_src/css/modules/list.css +++ b/web_src/css/modules/list.css @@ -126,12 +126,6 @@ cursor: pointer; } -.ui.list .list > .item [class*="right floated"], -.ui.list > .item [class*="right floated"] { - float: right; - margin: 0 0 0 1em; -} - .ui.menu .ui.list > .item, .ui.menu .ui.list .list > .item { display: list-item; diff --git a/web_src/css/modules/menu.css b/web_src/css/modules/menu.css index 43679a3317..a5efd23053 100644 --- a/web_src/css/modules/menu.css +++ b/web_src/css/modules/menu.css @@ -633,18 +633,6 @@ } } -.ui.floated.menu { - float: left; - margin: 0 0.5rem 0 0; -} -.ui.floated.menu .item:last-child::before { - display: none; -} -.ui.right.floated.menu { - float: right; - margin: 0 0 0 0.5rem; -} - .ui.borderless.menu .item::before, .ui.borderless.menu .item .menu .item::before, .ui.menu .borderless.item::before { diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 7aecb1e351..2f66336a66 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -1,244 +1,220 @@ diff --git a/web_src/js/features/comp/ReactionSelector.ts b/web_src/js/features/comp/ReactionSelector.ts index f6381c2563..1e955c7ab4 100644 --- a/web_src/js/features/comp/ReactionSelector.ts +++ b/web_src/js/features/comp/ReactionSelector.ts @@ -1,8 +1,8 @@ import {POST} from '../../modules/fetch.ts'; import {fomanticQuery} from '../../modules/fomantic/base.ts'; -export function initCompReactionSelector() { - for (const container of document.querySelectorAll('.issue-content, .diff-file-body')) { +export function initCompReactionSelector(parent: ParentNode = document) { + for (const container of parent.querySelectorAll('.issue-content, .diff-file-body')) { container.addEventListener('click', async (e) => { // there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment const target = e.target.closest('.comment-reaction-button'); diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index f39de96f5b..58e0d88092 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -38,7 +38,7 @@ function initRepoDiffFileViewToggle() { } function initRepoDiffConversationForm() { - addDelegatedEventListener(document, 'submit', '.conversation-holder form', async (form, e) => { + addDelegatedEventListener(document, 'submit', '.conversation-holder form', async (form, e) => { e.preventDefault(); const textArea = form.querySelector('textarea'); if (!validateTextareaNonEmpty(textArea)) return; @@ -55,7 +55,9 @@ function initRepoDiffConversationForm() { formData.append(submitter.name, submitter.value); } - const trLineType = form.closest('tr').getAttribute('data-line-type'); + // on the diff page, the form is inside a "tr" and need to get the line-type ahead + // but on the conversation page, there is no parent "tr" + const trLineType = form.closest('tr')?.getAttribute('data-line-type'); const response = await POST(form.getAttribute('action'), {data: formData}); const newConversationHolder = createElementFromHTML(await response.text()); const path = newConversationHolder.getAttribute('data-path'); @@ -65,14 +67,18 @@ function initRepoDiffConversationForm() { form.closest('.conversation-holder').replaceWith(newConversationHolder); form = null; // prevent further usage of the form because it should have been replaced - let selector; - if (trLineType === 'same') { - selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`; - } else { - selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`; - } - for (const el of document.querySelectorAll(selector)) { - el.classList.add('tw-invisible'); // TODO need to figure out why + if (trLineType) { + // if there is a line-type for the "tr", it means the form is on the diff page + // then hide the "add-code-comment" [+] button for current code line by adding "tw-invisible" because the conversation has been added + let selector; + if (trLineType === 'same') { + selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`; + } else { + selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`; + } + for (const el of document.querySelectorAll(selector)) { + el.classList.add('tw-invisible'); + } } fomanticQuery(newConversationHolder.querySelectorAll('.ui.dropdown')).dropdown(); @@ -109,7 +115,7 @@ function initRepoDiffConversationForm() { const $conversation = $(data); $(this).closest('.conversation-holder').replaceWith($conversation); $conversation.find('.dropdown').dropdown(); - initCompReactionSelector($conversation); + initCompReactionSelector($conversation[0]); } else { window.location.reload(); } diff --git a/web_src/js/types.ts b/web_src/js/types.ts index 29279bbded..e7c9ac0df4 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -59,3 +59,5 @@ export type FomanticInitFunction = { settings?: Record, (...args: any[]): any, } + +export type GitRefType = 'branch' | 'tag';