diff --git a/app/store/comment.go b/app/store/comment.go index 0d2f5e51..6bfc603a 100644 --- a/app/store/comment.go +++ b/app/store/comment.go @@ -2,9 +2,12 @@ package store import ( "html/template" + "net/url" "regexp" + "strings" "time" + "github.com/PuerkitoBio/goquery" "github.com/microcosm-cc/bluemonday" ) @@ -61,6 +64,9 @@ const ( HardDelete DeleteMode = 1 ) +// Maximum length for URL text shortening. +const shortURLLen = 32 + // PrepareUntrusted pre-processes a comment received from untrusted source by clearing all // autogen fields and reset everything users not supposed to provide func (c *Comment) PrepareUntrusted() { @@ -91,13 +97,47 @@ func (c *Comment) SetDeleted(mode DeleteMode) { } } -// Sanitize clean dangerous html/js from the comment +// Sanitize clean dangerous html/js from the comment, shorten autolinks. func (c *Comment) Sanitize() { p := bluemonday.UGCPolicy() p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") - c.Text = p.Sanitize(c.Text) + c.Text = shortenAutoLinks(p.Sanitize(c.Text), shortURLLen) c.Orig = p.Sanitize(c.Orig) c.User.ID = template.HTMLEscapeString(c.User.ID) c.User.Name = template.HTMLEscapeString(c.User.Name) c.User.Picture = p.Sanitize(c.User.Picture) } + +// Shortens all the automatic links in HTML: auto link has equal "href" and "text" attributes. +func shortenAutoLinks(commentHTML string, max int) string { + doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML)) + if err != nil { + return commentHTML + } + doc.Find("a").Each(func(i int, s *goquery.Selection) { + if href, ok := s.Attr("href"); ok { + if href != s.Text() || len(href) < max+3 || max < 3 { + return + } + url, err := url.Parse(href) + if err != nil { + return + } + url.Path, url.RawQuery, url.Fragment = "", "", "" + host := url.String() + if host == "" { + return + } + short := href[:max-3] + if len(short) < len(host) { + short = host + } + s.SetText(short + "...") + } + }) + if html, err := doc.Find("body").Html(); err == nil { + return html + } + + return commentHTML +} diff --git a/app/store/comment_test.go b/app/store/comment_test.go index 5723f7c0..9597cba6 100644 --- a/app/store/comment_test.go +++ b/app/store/comment_test.go @@ -16,7 +16,7 @@ func TestComment_Sanitize(t *testing.T) { {inp: Comment{}, out: Comment{}}, { inp: Comment{ - Text: `blah XSS` + "\n\t", + Text: `blah XSS` + "\n\t", User: User{ID: `username`}, }, out: Comment{ @@ -24,6 +24,16 @@ func TestComment_Sanitize(t *testing.T) { User: User{ID: `<a href="http://blah.com">username</a>`}, }, }, + { + inp: Comment{ + Text: `blah https://www.reddit.com/r/golang/comments/8jdo2l/remark42_is_a_selfhosted_lightweight_and_simple/` + "\n\t", + User: User{ID: `username`}, + }, + out: Comment{ + Text: `blah https://www.reddit.com/r/gola...` + "\n\t", + User: User{ID: `<a href="http://blah.com">username</a>`}, + }, + }, } for n, tt := range tbl { @@ -110,3 +120,46 @@ func TestComment_SetDeletedHard(t *testing.T) { assert.False(t, comment.Pin) assert.Equal(t, User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comment.User) } + +func TestComment_ShortenAutoLinks(t *testing.T) { + tbl := []struct { + max int + in, out string + }{ + {32, "", ""}, + {32, "text", "text"}, + {32, "

asd

", "

asd

"}, + {5, `incorrect-url`, `incorrect-url`}, + {32, `some text, not href`, `some text, not href`}, + { + 32, + `https://blah.com/a/b/c/d?g=123#anc`, + `https://blah.com/a/b/c/d?g=123#anc`, + }, + { + 31, + `https://blah.com/a/b/c/d?g=123#anc`, + `https://blah.com/a/b/c/d?g=1...`, + }, + { + 15, + `https://blah.com/a/b/c/d?g=123#anc`, + `https://blah.com...`, + }, + { + 3, + `https://blah.com/a/b/c/d?g=123#anc`, + `https://blah.com...`, + }, + { + -1, + `https://blah.com/a/b/c/d?g=123#anc`, + `https://blah.com/a/b/c/d?g=123#anc`, + }, + } + + for n, tt := range tbl { + got := shortenAutoLinks(tt.in, tt.max) + assert.Equalf(t, tt.out, got, "check #%d", n) + } +}