mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Compare commits
5 Commits
7b43f84e1c
...
da5a20c8d2
Author | SHA1 | Date | |
---|---|---|---|
|
da5a20c8d2 | ||
|
21af8150b7 | ||
|
40faa6dc78 | ||
|
5bf5f388d1 | ||
|
b2db1a7fe0 |
@ -1767,6 +1767,12 @@ LEVEL = Info
|
||||
;;
|
||||
;; convert \r\n to \n for Sendmail
|
||||
;SENDMAIL_CONVERT_CRLF = true
|
||||
;;
|
||||
;; convert links of attached images to inline images. Only for images hosted in this gitea instance.
|
||||
;BASE64_EMBED_IMAGES = false
|
||||
;;
|
||||
;; The maximum size of sum of all images in a single email. Default is 9.5MB
|
||||
;BASE64_EMBED_IMAGES_MAX_SIZE_PER_EMAIL = 9961472
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -153,3 +153,16 @@
|
||||
download_count: 0
|
||||
size: 0
|
||||
created_unix: 946684800
|
||||
|
||||
-
|
||||
id: 13
|
||||
uuid: 1b267670-1793-4cd0-abc1-449269b7cff9
|
||||
repo_id: 1
|
||||
issue_id: 23
|
||||
release_id: 0
|
||||
uploader_id: 0
|
||||
comment_id: 2
|
||||
name: gitea.png
|
||||
download_count: 0
|
||||
size: 1458
|
||||
created_unix: 946684800
|
||||
|
@ -372,3 +372,20 @@
|
||||
created_unix: 1707270422
|
||||
updated_unix: 1707270422
|
||||
is_locked: false
|
||||
|
||||
-
|
||||
id: 23
|
||||
repo_id: 1
|
||||
index: 6
|
||||
poster_id: 1
|
||||
original_author_id: 0
|
||||
name: issue23
|
||||
content: 'content including this image: <image alt="gitea.png" src="attachments/1b267670-1793-4cd0-abc1-449269b7cff9" /> with some more content behind it'
|
||||
milestone_id: 0
|
||||
priority: 0
|
||||
is_closed: false
|
||||
is_pull: false
|
||||
num_comments: 0
|
||||
created_unix: 946684801
|
||||
updated_unix: 978307201
|
||||
is_locked: false
|
||||
|
@ -1,6 +1,6 @@
|
||||
-
|
||||
group_id: 1
|
||||
max_index: 5
|
||||
max_index: 6
|
||||
|
||||
-
|
||||
group_id: 2
|
||||
|
@ -9,7 +9,7 @@
|
||||
num_watches: 4
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 2
|
||||
num_issues: 3
|
||||
num_closed_issues: 1
|
||||
num_pulls: 3
|
||||
num_closed_pulls: 0
|
||||
|
@ -57,7 +57,7 @@ func Test_GetIssueIDsByRepoID(t *testing.T) {
|
||||
|
||||
ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, ids, 5)
|
||||
assert.Len(t, ids, 6)
|
||||
}
|
||||
|
||||
func TestIssueAPIURL(t *testing.T) {
|
||||
@ -170,7 +170,7 @@ func TestIssues(t *testing.T) {
|
||||
PageSize: 4,
|
||||
},
|
||||
},
|
||||
[]int64{1, 2, 3, 5},
|
||||
[]int64{1, 23, 2, 3},
|
||||
},
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
@ -249,11 +249,11 @@ func TestIssue_InsertIssue(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// there are 5 issues and max index is 5 on repository 1, so this one should 6
|
||||
issue := testInsertIssue(t, "my issue1", "special issue's comments?", 6)
|
||||
issue := testInsertIssue(t, "my issue1", "special issue's comments?", 7)
|
||||
_, err := db.DeleteByID[issues_model.Issue](db.DefaultContext, issue.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue = testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?", 7)
|
||||
issue = testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?", 8)
|
||||
_, err = db.DeleteByID[issues_model.Issue](db.DefaultContext, issue.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@ -380,7 +380,7 @@ func TestCountIssues(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 22, count)
|
||||
assert.EqualValues(t, 23, count)
|
||||
}
|
||||
|
||||
func TestIssueLoadAttributes(t *testing.T) {
|
||||
|
@ -22,7 +22,7 @@ func Test_NewIssueUsers(t *testing.T) {
|
||||
newIssue := &issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
PosterID: 4,
|
||||
Index: 6,
|
||||
Index: 7,
|
||||
Title: "newTestIssueTitle",
|
||||
Content: "newTestIssueContent",
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func TestDeleteAttachments(t *testing.T) {
|
||||
|
||||
count, err = repo_model.DeleteAttachmentsByComment(db.DefaultContext, 2, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Equal(t, 3, count)
|
||||
|
||||
err = repo_model.DeleteAttachment(db.DefaultContext, &repo_model.Attachment{ID: 8}, false)
|
||||
assert.NoError(t, err)
|
||||
|
@ -57,7 +57,7 @@ func searchIssueWithKeyword(t *testing.T) {
|
||||
Keyword: "issue2",
|
||||
RepoIDs: []int64{1},
|
||||
},
|
||||
[]int64{2},
|
||||
[]int64{2, 23},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
@ -106,7 +106,7 @@ func searchIssueByIndex(t *testing.T) {
|
||||
Keyword: "2",
|
||||
RepoIDs: []int64{1, 2, 3, 32},
|
||||
},
|
||||
[]int64{17, 12, 7, 2},
|
||||
[]int64{17, 12, 7, 2, 23},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
@ -133,7 +133,7 @@ func searchIssueInRepo(t *testing.T) {
|
||||
SearchOptions{
|
||||
RepoIDs: []int64{1},
|
||||
},
|
||||
[]int64{11, 5, 3, 2, 1},
|
||||
[]int64{11, 5, 3, 2, 23, 1},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
@ -177,7 +177,7 @@ func searchIssueByID(t *testing.T) {
|
||||
opts: SearchOptions{
|
||||
PosterID: optional.Some(int64(1)),
|
||||
},
|
||||
expectedIDs: []int64{11, 6, 3, 2, 1},
|
||||
expectedIDs: []int64{11, 6, 3, 2, 23, 1},
|
||||
},
|
||||
{
|
||||
opts: SearchOptions{
|
||||
@ -188,7 +188,7 @@ func searchIssueByID(t *testing.T) {
|
||||
{
|
||||
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1.
|
||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}),
|
||||
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
|
||||
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 23},
|
||||
},
|
||||
{
|
||||
opts: SearchOptions{
|
||||
@ -212,7 +212,7 @@ func searchIssueByID(t *testing.T) {
|
||||
opts: SearchOptions{
|
||||
SubscriberID: optional.Some(int64(1)),
|
||||
},
|
||||
expectedIDs: []int64{11, 6, 5, 3, 2, 1},
|
||||
expectedIDs: []int64{11, 6, 5, 3, 2, 23, 1},
|
||||
},
|
||||
{
|
||||
// issue 20 request user 15 and team 5 which user 15 belongs to
|
||||
@ -247,7 +247,7 @@ func searchIssueIsPull(t *testing.T) {
|
||||
SearchOptions{
|
||||
IsPull: optional.Some(false),
|
||||
},
|
||||
[]int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1},
|
||||
[]int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 23, 1},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
@ -272,7 +272,7 @@ func searchIssueIsClosed(t *testing.T) {
|
||||
SearchOptions{
|
||||
IsClosed: optional.Some(false),
|
||||
},
|
||||
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
|
||||
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 23, 1},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
@ -297,7 +297,7 @@ func searchIssueIsArchived(t *testing.T) {
|
||||
SearchOptions{
|
||||
IsArchived: optional.Some(false),
|
||||
},
|
||||
[]int64{22, 21, 17, 16, 15, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
|
||||
[]int64{22, 21, 17, 16, 15, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 23, 1},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
@ -359,7 +359,7 @@ func searchIssueByLabelID(t *testing.T) {
|
||||
SearchOptions{
|
||||
ExcludedLabelIDs: []int64{1},
|
||||
},
|
||||
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3},
|
||||
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 23},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
@ -378,7 +378,7 @@ func searchIssueByTime(t *testing.T) {
|
||||
SearchOptions{
|
||||
UpdatedAfterUnix: optional.Some(int64(0)),
|
||||
},
|
||||
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
|
||||
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 23, 1},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
@ -397,7 +397,7 @@ func searchIssueWithOrder(t *testing.T) {
|
||||
SearchOptions{
|
||||
SortBy: internal.SortByCreatedAsc,
|
||||
},
|
||||
[]int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17, 21, 22},
|
||||
[]int64{1, 23, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17, 21, 22},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
@ -451,7 +451,7 @@ func searchIssueWithPaginator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
[]int64{22, 21, 17, 16, 15},
|
||||
22,
|
||||
23,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
@ -13,21 +13,23 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
// Mailer represents mail service.
|
||||
type Mailer struct {
|
||||
// Mailer
|
||||
Name string `ini:"NAME"`
|
||||
From string `ini:"FROM"`
|
||||
EnvelopeFrom string `ini:"ENVELOPE_FROM"`
|
||||
OverrideEnvelopeFrom bool `ini:"-"`
|
||||
FromName string `ini:"-"`
|
||||
FromEmail string `ini:"-"`
|
||||
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
|
||||
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
|
||||
OverrideHeader map[string][]string `ini:"-"`
|
||||
Name string `ini:"NAME"`
|
||||
From string `ini:"FROM"`
|
||||
EnvelopeFrom string `ini:"ENVELOPE_FROM"`
|
||||
OverrideEnvelopeFrom bool `ini:"-"`
|
||||
FromName string `ini:"-"`
|
||||
FromEmail string `ini:"-"`
|
||||
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
|
||||
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
|
||||
OverrideHeader map[string][]string `ini:"-"`
|
||||
Base64EmbedImages bool `ini:"BASE64_EMBED_IMAGES"`
|
||||
Base64EmbedImagesMaxSizePerEmail int64 `ini:"BASE64_EMBED_IMAGES_MAX_SIZE_PER_EMAIL"`
|
||||
|
||||
// SMTP sender
|
||||
Protocol string `ini:"PROTOCOL"`
|
||||
@ -150,6 +152,8 @@ func loadMailerFrom(rootCfg ConfigProvider) {
|
||||
sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute)
|
||||
sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true)
|
||||
sec.Key("FROM").MustString(sec.Key("USER").String())
|
||||
sec.Key("BASE64_EMBED_IMAGES").MustBool(false)
|
||||
sec.Key("BASE64_EMBED_IMAGES_MAX_SIZE_PER_EMAIL").MustInt64(9.5 * 1024 * 1024)
|
||||
|
||||
// Now map the values on to the MailService
|
||||
MailService = &Mailer{}
|
||||
|
@ -1701,7 +1701,9 @@ issues.time_estimate_invalid=O formato da estimativa de tempo é inválido
|
||||
issues.start_tracking_history=começou a trabalhar %s
|
||||
issues.tracker_auto_close=O cronómetro será parado automaticamente quando esta questão for fechada
|
||||
issues.tracking_already_started=`Você já iniciou a contagem de tempo <a href="%s">noutra questão</a>!`
|
||||
issues.stop_tracking=Parar cronómetro
|
||||
issues.stop_tracking_history=trabalhou durante <b>%[1]s</b> %[2]s
|
||||
issues.cancel_tracking=Descartar
|
||||
issues.cancel_tracking_history=`cancelou a contagem de tempo %s`
|
||||
issues.del_time=Eliminar este registo de tempo
|
||||
issues.add_time_history=adicionou <b>%[1]s</b> de tempo gasto %[2]s
|
||||
|
@ -37,7 +37,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
|
||||
|
||||
issueIDs, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issueIDs, 5)
|
||||
assert.Len(t, issueIDs, 6)
|
||||
|
||||
issue := &issues_model.Issue{
|
||||
RepoID: 1,
|
||||
@ -48,7 +48,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issueIDs, 4)
|
||||
assert.Len(t, issueIDs, 5)
|
||||
|
||||
// check attachment removal
|
||||
attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 4)
|
||||
|
@ -26,7 +26,7 @@ func Test_Suggestion(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
keyword: "",
|
||||
expectedIndexes: []int64{5, 1, 4, 2, 3},
|
||||
expectedIndexes: []int64{5, 6, 1, 4, 2},
|
||||
},
|
||||
{
|
||||
keyword: "1",
|
||||
@ -34,7 +34,7 @@ func Test_Suggestion(t *testing.T) {
|
||||
},
|
||||
{
|
||||
keyword: "issue",
|
||||
expectedIndexes: []int64{4, 1, 2, 3},
|
||||
expectedIndexes: []int64{6, 4, 1, 2, 3},
|
||||
},
|
||||
{
|
||||
keyword: "pull",
|
||||
|
@ -7,9 +7,12 @@ package mailer
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -18,19 +21,24 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"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"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -228,6 +236,15 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if setting.MailService.Base64EmbedImages {
|
||||
bodyStr := string(body)
|
||||
bodyStr, err = Base64InlineImages(bodyStr, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = template.HTML(bodyStr)
|
||||
}
|
||||
|
||||
actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType)
|
||||
|
||||
if actName != "new" {
|
||||
@ -359,6 +376,110 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
func Base64InlineImages(body string, ctx *mailCommentContext) (string, error) {
|
||||
doc, err := html.Parse(strings.NewReader(body))
|
||||
if err != nil {
|
||||
log.Error("Failed to parse HTML body: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
var totalEmbeddedImagesSize int64
|
||||
|
||||
var processNode func(*html.Node)
|
||||
processNode = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode {
|
||||
if n.Data == "img" {
|
||||
for i, attr := range n.Attr {
|
||||
if attr.Key == "src" {
|
||||
attachmentPath := attr.Val
|
||||
dataURI, err := AttachmentSrcToBase64DataURI(attachmentPath, ctx, &totalEmbeddedImagesSize)
|
||||
if err != nil {
|
||||
log.Trace("attachmentSrcToDataURI not possible: %v", err) // Not an error, just skip. This is probably an image from outside the gitea instance.
|
||||
continue
|
||||
}
|
||||
log.Trace("Old value of src attribute: %s, new value (first 100 characters): %s", attr.Val, dataURI[:100])
|
||||
n.Attr[i].Val = dataURI
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
processNode(c)
|
||||
}
|
||||
}
|
||||
|
||||
processNode(doc)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = html.Render(&buf, doc)
|
||||
if err != nil {
|
||||
log.Error("Failed to render modified HTML: %v", err)
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func AttachmentSrcToBase64DataURI(attachmentPath string, ctx *mailCommentContext, totalEmbeddedImagesSize *int64) (string, error) {
|
||||
if !strings.HasPrefix(attachmentPath, setting.AppURL) { // external image
|
||||
return "", fmt.Errorf("external image")
|
||||
}
|
||||
parts := strings.Split(attachmentPath, "/attachments/")
|
||||
if len(parts) <= 1 {
|
||||
return "", fmt.Errorf("invalid attachment path: %s", attachmentPath)
|
||||
}
|
||||
|
||||
attachmentUUID := parts[len(parts)-1]
|
||||
attachment, err := repo_model.GetAttachmentByUUID(ctx, attachmentUUID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// "Doer" is theoretically not the correct permission check (as Doer created the action on which to send), but as this is batch processed the receipants can't be accessed.
|
||||
// Therefore we check the Doer, with which we counter leaking information as a Doer brute force attack on attachments would be possible.
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, ctx.Issue.Repo, ctx.Doer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !perm.CanRead(unit.TypeIssues) {
|
||||
return "", fmt.Errorf("no permission")
|
||||
}
|
||||
|
||||
fr, err := storage.Attachments.Open(attachment.RelativePath())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
maxSize := setting.MailService.Base64EmbedImagesMaxSizePerEmail // at maximum read the whole available combined email size, to prevent maliciously large file reads
|
||||
|
||||
lr := &io.LimitedReader{R: fr, N: maxSize + 1}
|
||||
content, err := io.ReadAll(lr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(content) > int(maxSize) {
|
||||
return "", fmt.Errorf("file size exceeds the embedded image max limit \\(%d bytes\\)", maxSize)
|
||||
}
|
||||
|
||||
if *totalEmbeddedImagesSize+int64(len(content)) > setting.MailService.Base64EmbedImagesMaxSizePerEmail {
|
||||
return "", fmt.Errorf("total embedded images exceed max limit: %d > %d", *totalEmbeddedImagesSize+int64(len(content)), setting.MailService.Base64EmbedImagesMaxSizePerEmail)
|
||||
}
|
||||
*totalEmbeddedImagesSize += int64(len(content))
|
||||
|
||||
mimeType := http.DetectContentType(content)
|
||||
|
||||
if !strings.HasPrefix(mimeType, "image/") {
|
||||
return "", fmt.Errorf("not an image")
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(content)
|
||||
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encoded)
|
||||
|
||||
return dataURI, nil
|
||||
}
|
||||
|
||||
func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
|
||||
var path string
|
||||
if issue.IsPull {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"mime/quotedprintable"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -23,6 +24,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
sender_service "code.gitea.io/gitea/services/mailer/sender"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -59,6 +61,7 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
|
||||
|
||||
setting.MailService = &mailService
|
||||
setting.Domain = "localhost"
|
||||
setting.AppURL = "https://try.gitea.io/"
|
||||
|
||||
doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer})
|
||||
@ -450,3 +453,132 @@ func TestFromDisplayName(t *testing.T) {
|
||||
assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
|
||||
})
|
||||
}
|
||||
|
||||
func PrepareAttachmentsStorage(t testing.TB) { // same as in test_utils.go
|
||||
// prepare attachments directory and files
|
||||
assert.NoError(t, storage.Clean(storage.Attachments))
|
||||
|
||||
s, err := storage.NewStorage(setting.LocalStorageType, &setting.Storage{
|
||||
Path: filepath.Join(filepath.Dir(setting.AppPath), "tests", "testdata", "data", "attachments"),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, s.IterateObjects("", func(p string, obj storage.Object) error {
|
||||
_, err = storage.Copy(storage.Attachments, p, s, p)
|
||||
return err
|
||||
}))
|
||||
}
|
||||
|
||||
func TestEmbedBase64ImagesInEmail(t *testing.T) {
|
||||
// Fake context setup
|
||||
doer, repo, _, _ := prepareMailerTest(t)
|
||||
PrepareAttachmentsStorage(t)
|
||||
setting.MailService.Base64EmbedImages = true
|
||||
setting.MailService.Base64EmbedImagesMaxSizePerEmail = 10 * 1024 * 1024
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 23, Repo: repo, Poster: doer})
|
||||
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
|
||||
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl))
|
||||
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
|
||||
msgs, err := composeIssueCommentMessages(&mailCommentContext{
|
||||
Context: context.TODO(), // TODO: use a correct context
|
||||
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
|
||||
Content: strings.ReplaceAll(issue.Content, `src="`, `src="`+setting.AppURL),
|
||||
}, "en-US", recipients, false, "issue create")
|
||||
|
||||
mailBody := msgs[0].Body
|
||||
re := regexp.MustCompile(`(?s)<body>(.*?)</body>`)
|
||||
matches := re.FindStringSubmatch(mailBody)
|
||||
if len(matches) > 1 {
|
||||
mailBody = matches[1]
|
||||
}
|
||||
// check if the mail body was correctly generated
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, mailBody, "content including this image")
|
||||
|
||||
// check if an image was embedded
|
||||
assert.Contains(t, mailBody, "data:image/png;base64,")
|
||||
|
||||
// check if the image was embedded only once
|
||||
assert.Equal(t, 1, strings.Count(mailBody, "data:image/png;base64,"))
|
||||
|
||||
img2InternalBase64 := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAxAQMAAAB3d7wRAAAABlBMVEVgmyF6qkqITHmkAAAAAXRSTlMBN+Ho8AAAAJhJREFUKM+V0DsOwyAQBNCxXLjkCFwk0t7McDQfhS4tpQuEzWc/iaUU2eo1zC4DUMWYF3DxVKzGTXjBGb2RsjJEo6ZhN1Zj+cEgi/9hBQl3YflkkIsbo5IO5glKTuhPpavM3Hp4C7WdjEWYrL5GMkp/R+s4GPlh/CZn4MEwv9aHHiyD3ujm5X22eaMyDa5yAm+O0B1TPa1l3W2qZWMg+KgtAAAAAElFTkSuQmCC"
|
||||
|
||||
// check if the image was embedded correctly
|
||||
assert.Contains(t, mailBody, img2InternalBase64)
|
||||
}
|
||||
|
||||
func TestEmbedBase64Images(t *testing.T) {
|
||||
user, repo, _, _ := prepareMailerTest(t)
|
||||
PrepareAttachmentsStorage(t)
|
||||
setting.MailService.Base64EmbedImages = true
|
||||
setting.MailService.Base64EmbedImagesMaxSizePerEmail = 10 * 1024 * 1024
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 23, Repo: repo, Poster: user})
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 13, IssueID: issue.ID, RepoID: repo.ID})
|
||||
ctx0 := context.Background()
|
||||
|
||||
ctx := &mailCommentContext{Context: ctx0 /* TODO: use a correct context */, Issue: issue, Doer: user}
|
||||
|
||||
img1ExternalURL := "https://via.placeholder.com/10"
|
||||
img1ExternalImg := "<img src=\"" + img1ExternalURL + "\"/>"
|
||||
|
||||
img2InternalURL := setting.AppURL + repo.Owner.Name + "/" + repo.Name + "/attachments/" + attachment.UUID
|
||||
img2InternalImg := "<img src=\"" + img2InternalURL + "\"/>"
|
||||
img2InternalBase64 := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAxAQMAAAB3d7wRAAAABlBMVEVgmyF6qkqITHmkAAAAAXRSTlMBN+Ho8AAAAJhJREFUKM+V0DsOwyAQBNCxXLjkCFwk0t7McDQfhS4tpQuEzWc/iaUU2eo1zC4DUMWYF3DxVKzGTXjBGb2RsjJEo6ZhN1Zj+cEgi/9hBQl3YflkkIsbo5IO5glKTuhPpavM3Hp4C7WdjEWYrL5GMkp/R+s4GPlh/CZn4MEwv9aHHiyD3ujm5X22eaMyDa5yAm+O0B1TPa1l3W2qZWMg+KgtAAAAAElFTkSuQmCC"
|
||||
img2InternalBase64Img := "<img src=\"" + img2InternalBase64 + "\"/>"
|
||||
|
||||
// 1st Test: convert internal image to base64
|
||||
t.Run("replaceSpecifiedBase64ImagesInternal", func(t *testing.T) {
|
||||
totalEmbeddedImagesSize := int64(0)
|
||||
|
||||
resultImg1Internal, err := AttachmentSrcToBase64DataURI(img2InternalURL, ctx, &totalEmbeddedImagesSize)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, img2InternalBase64, resultImg1Internal) // replace cause internal image
|
||||
})
|
||||
|
||||
// 2nd Test: convert external image to base64 -> abort cause external image
|
||||
t.Run("replaceSpecifiedBase64ImagesExternal", func(t *testing.T) {
|
||||
totalEmbeddedImagesSize := int64(0)
|
||||
|
||||
resultImg1External, err := AttachmentSrcToBase64DataURI(img1ExternalURL, ctx, &totalEmbeddedImagesSize)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", resultImg1External) // don't replace cause external image
|
||||
})
|
||||
|
||||
// 3rd Test: generate email body with 1 internal and 1 external image, expect the result to have the internal image replaced with base64 data and the external not replaced
|
||||
t.Run("generateEmailBody", func(t *testing.T) {
|
||||
mailBody := "<html><head></head><body><p>Test1</p>" + img1ExternalImg + "<p>Test2</p>" + img2InternalImg + "<p>Test3</p></body></html>"
|
||||
expectedMailBody := "<html><head></head><body><p>Test1</p>" + img1ExternalImg + "<p>Test2</p>" + img2InternalBase64Img + "<p>Test3</p></body></html>"
|
||||
resultMailBody, err := Base64InlineImages(mailBody, ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedMailBody, resultMailBody)
|
||||
})
|
||||
|
||||
// 4th Test, generate email body with 2 internal images, but set Mailer.Base64EmbedImagesMaxSizePerEmail to the size of the first image (+1), expect the first image to be replaced and the second not
|
||||
t.Run("generateEmailBodyWithMaxSize", func(t *testing.T) {
|
||||
setting.MailService.Base64EmbedImagesMaxSizePerEmail = int64(len(img2InternalBase64) + 1)
|
||||
|
||||
mailBody := "<html><head></head><body><p>Test1</p>" + img2InternalImg + "<p>Test2</p>" + img2InternalImg + "<p>Test3</p></body></html>"
|
||||
expectedMailBody := "<html><head></head><body><p>Test1</p>" + img2InternalBase64Img + "<p>Test2</p>" + img2InternalImg + "<p>Test3</p></body></html>"
|
||||
resultMailBody, err := Base64InlineImages(mailBody, ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedMailBody, resultMailBody)
|
||||
})
|
||||
|
||||
// 5th Test, generate email body with 3 internal images, but set Mailer.Base64EmbedImagesMaxSizePerEmail to the size of all 3 images (+1), expect all images to be replaced
|
||||
t.Run("generateEmailBodyWith3Images", func(t *testing.T) {
|
||||
setting.MailService.Base64EmbedImagesMaxSizePerEmail = int64(len(img2InternalBase64)*3 + 1)
|
||||
|
||||
mailBody := "<html><head></head><body><p>Test1</p>" + img2InternalImg + "<p>Test2</p>" + img2InternalImg + "<p>Test3</p>" + img2InternalImg + "</body></html>"
|
||||
expectedMailBody := "<html><head></head><body><p>Test1</p>" + img2InternalBase64Img + "<p>Test2</p>" + img2InternalBase64Img + "<p>Test3</p>" + img2InternalBase64Img + "</body></html>"
|
||||
resultMailBody, err := Base64InlineImages(mailBody, ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedMailBody, resultMailBody)
|
||||
})
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo
|
||||
}
|
||||
|
||||
if len(branches) == 0 {
|
||||
graphCmd.AddArguments("--all")
|
||||
graphCmd.AddArguments("--tags", "--branches")
|
||||
}
|
||||
|
||||
graphCmd.AddArguments("-C", "-M", "--date=iso-strict").
|
||||
|
@ -287,7 +287,7 @@ func TestAPISearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 15) // 15 public issues
|
||||
assert.Len(t, apiIssues, 16) // 16 public issues
|
||||
|
||||
since := "2000-01-01T00:50:01+00:00" // 946687801
|
||||
before := time.Unix(999307200, 0).Format(time.RFC3339)
|
||||
@ -297,7 +297,7 @@ func TestAPISearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 11)
|
||||
assert.Len(t, apiIssues, 12)
|
||||
query.Del("since")
|
||||
query.Del("before")
|
||||
|
||||
@ -313,7 +313,7 @@ func TestAPISearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
|
||||
assert.EqualValues(t, "23", resp.Header().Get("X-Total-Count"))
|
||||
assert.Len(t, apiIssues, 20)
|
||||
|
||||
query.Add("limit", "10")
|
||||
@ -321,7 +321,7 @@ func TestAPISearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
|
||||
assert.EqualValues(t, "23", resp.Header().Get("X-Total-Count"))
|
||||
assert.Len(t, apiIssues, 10)
|
||||
|
||||
query = url.Values{"assigned": {"true"}, "state": {"all"}}
|
||||
@ -350,7 +350,7 @@ func TestAPISearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 8)
|
||||
assert.Len(t, apiIssues, 9)
|
||||
|
||||
query = url.Values{"owner": {"org3"}} // organization
|
||||
link.RawQuery = query.Encode()
|
||||
|
@ -33,7 +33,7 @@ func TestNodeinfo(t *testing.T) {
|
||||
assert.True(t, nodeinfo.OpenRegistrations)
|
||||
assert.Equal(t, "gitea", nodeinfo.Software.Name)
|
||||
assert.Equal(t, 29, nodeinfo.Usage.Users.Total)
|
||||
assert.Equal(t, 22, nodeinfo.Usage.LocalPosts)
|
||||
assert.Equal(t, 23, nodeinfo.Usage.LocalPosts)
|
||||
assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
|
||||
})
|
||||
}
|
||||
|
@ -268,7 +268,7 @@ func TestAPIViewRepo(t *testing.T) {
|
||||
assert.EqualValues(t, 1, repo.ID)
|
||||
assert.EqualValues(t, "repo1", repo.Name)
|
||||
assert.EqualValues(t, 2, repo.Releases)
|
||||
assert.EqualValues(t, 1, repo.OpenIssues)
|
||||
assert.EqualValues(t, 2, repo.OpenIssues)
|
||||
assert.EqualValues(t, 3, repo.OpenPulls)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user12/repo10")
|
||||
|
@ -495,7 +495,7 @@ func TestSearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String())
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 11)
|
||||
assert.Len(t, apiIssues, 12)
|
||||
query.Del("since")
|
||||
query.Del("before")
|
||||
|
||||
@ -511,7 +511,7 @@ func TestSearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String())
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
|
||||
assert.EqualValues(t, "23", resp.Header().Get("X-Total-Count"))
|
||||
assert.Len(t, apiIssues, 20)
|
||||
|
||||
query.Add("limit", "5")
|
||||
@ -519,7 +519,7 @@ func TestSearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String())
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
|
||||
assert.EqualValues(t, "23", resp.Header().Get("X-Total-Count"))
|
||||
assert.Len(t, apiIssues, 5)
|
||||
|
||||
query = url.Values{"assigned": {"true"}, "state": {"all"}}
|
||||
@ -548,7 +548,7 @@ func TestSearchIssues(t *testing.T) {
|
||||
req = NewRequest(t, "GET", link.String())
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 8)
|
||||
assert.Len(t, apiIssues, 9)
|
||||
|
||||
query = url.Values{"owner": {"org3"}} // organization
|
||||
link.RawQuery = query.Encode()
|
||||
|
@ -924,7 +924,7 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.
|
||||
Run(&git.RunOpts{Dir: dstPath, Stderr: stderrBuf})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, stderrBuf.String(), setting.AppURL+"user2/repo1/pulls/6")
|
||||
assert.Contains(t, stderrBuf.String(), setting.AppURL+"user2/repo1/pulls/7")
|
||||
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
||||
@ -1044,7 +1044,7 @@ func TestPullNonMergeForAdminWithBranchProtection(t *testing.T) {
|
||||
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
mergeReq := NewRequestWithValues(t, "POST", "/api/v1/repos/user2/repo1/pulls/6/merge", map[string]string{
|
||||
mergeReq := NewRequestWithValues(t, "POST", "/api/v1/repos/user2/repo1/pulls/7/merge", map[string]string{
|
||||
"_csrf": csrf,
|
||||
"head_commit_id": "",
|
||||
"merge_when_checks_succeed": "false",
|
||||
|
BIN
tests/testdata/data/attachments/1/b/1b267670-1793-4cd0-abc1-449269b7cff9
vendored
Normal file
BIN
tests/testdata/data/attachments/1/b/1b267670-1793-4cd0-abc1-449269b7cff9
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 240 B |
Loading…
x
Reference in New Issue
Block a user