1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 09:37:02 +00:00

Reorganize packages, closes #79

- All packages go into either cmd or pkg directories
- Most packages renamed
- Server packages moved into pkg/server
- sanitize moved into webui, as that's the only place it's used
- filestore moved into pkg/storage/file
- Makefile updated, and PKG variable use fixed
This commit is contained in:
James Hillyerd
2018-03-09 19:32:45 -08:00
parent f00b9ddef0
commit f8c30a678a
55 changed files with 225 additions and 220 deletions

View File

@@ -0,0 +1,318 @@
package webui
import (
"fmt"
"html/template"
"io"
"net/http"
"strconv"
"github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/pkg/storage"
"github.com/jhillyerd/inbucket/pkg/stringutil"
"github.com/jhillyerd/inbucket/pkg/webui/sanitize"
)
// MailboxIndex renders the index page for a particular mailbox
func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Form values must be validated manually
name := req.FormValue("name")
selected := req.FormValue("id")
if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
name, err = stringutil.ParseMailboxName(name)
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
// Remember this mailbox was visited
RememberMailbox(ctx, name)
// Get flash messages, save session
errorFlash := ctx.Session.Flashes("errors")
if err = ctx.Session.Save(req, w); err != nil {
return err
}
// Render template
return web.RenderTemplate("mailbox/index.html", w, map[string]interface{}{
"ctx": ctx,
"errorFlash": errorFlash,
"name": name,
"selected": selected,
})
}
// MailboxLink handles pretty links to a particular message. Renders a redirect
func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
// Build redirect
uri := fmt.Sprintf("%s?name=%s&id=%s", web.Reverse("MailboxIndex"), name, id)
http.Redirect(w, req, uri, http.StatusSeeOther)
return nil
}
// MailboxList renders a list of messages in a mailbox. Renders a partial
func MailboxList(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
// This doesn't indicate not found, likely an IO error
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
messages, err := mb.GetMessages()
if err != nil {
// This doesn't indicate empty, likely an IO error
return fmt.Errorf("Failed to get messages for %v: %v", name, err)
}
log.Tracef("Got %v messsages", len(messages))
// Render partial template
return web.RenderPartial("mailbox/_list.html", w, map[string]interface{}{
"ctx": ctx,
"name": name,
"messages": messages,
})
}
// MailboxShow renders a particular message from a mailbox. Renders an HTML partial
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
// This doesn't indicate not found, likely an IO error
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
msg, err := mb.GetMessage(id)
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate empty, likely an IO error
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
mime, err := msg.ReadBody()
if err != nil {
return fmt.Errorf("ReadBody(%q) failed: %v", id, err)
}
body := template.HTML(web.TextToHTML(mime.Text))
htmlAvailable := mime.HTML != ""
var htmlBody template.HTML
if htmlAvailable {
if str, err := sanitize.HTML(mime.HTML); err == nil {
htmlBody = template.HTML(str)
} else {
log.Warnf("HTML sanitizer failed: %s", err)
}
}
// Render partial template
return web.RenderPartial("mailbox/_show.html", w, map[string]interface{}{
"ctx": ctx,
"name": name,
"message": msg,
"body": body,
"htmlAvailable": htmlAvailable,
"htmlBody": htmlBody,
"mimeErrors": mime.Errors,
"attachments": mime.Attachments,
})
}
// MailboxHTML displays the HTML content of a message. Renders a partial
func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
// This doesn't indicate not found, likely an IO error
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate missing, likely an IO error
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
mime, err := message.ReadBody()
if err != nil {
return fmt.Errorf("ReadBody(%q) failed: %v", id, err)
}
// Render partial template
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
return web.RenderPartial("mailbox/_html.html", w, map[string]interface{}{
"ctx": ctx,
"name": name,
"message": message,
// TODO It is not really safe to render, need to sanitize, issue #5
"body": template.HTML(mime.HTML),
})
}
// MailboxSource displays the raw source of a message, including headers. Renders text/plain
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
// This doesn't indicate not found, likely an IO error
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate missing, likely an IO error
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
raw, err := message.ReadRaw()
if err != nil {
return fmt.Errorf("ReadRaw(%q) failed: %v", id, err)
}
// Output message source
w.Header().Set("Content-Type", "text/plain")
if _, err := io.WriteString(w, *raw); err != nil {
return err
}
return nil
}
// MailboxDownloadAttach sends the attachment to the client; disposition:
// attachment, type: application/octet-stream
func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
numStr := ctx.Vars["num"]
num, err := strconv.ParseUint(numStr, 10, 32)
if err != nil {
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
// This doesn't indicate not found, likely an IO error
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate missing, likely an IO error
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
body, err := message.ReadBody()
if err != nil {
return err
}
if int(num) >= len(body.Attachments) {
ctx.Session.AddFlash("Attachment number too high", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
part := body.Attachments[num]
// Output attachment
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment")
if _, err := io.Copy(w, part); err != nil {
return err
}
return nil
}
// MailboxViewAttach sends the attachment to the client for online viewing
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
id := ctx.Vars["id"]
numStr := ctx.Vars["num"]
num, err := strconv.ParseUint(numStr, 10, 32)
if err != nil {
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
// This doesn't indicate not found, likely an IO error
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate missing, likely an IO error
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
body, err := message.ReadBody()
if err != nil {
return err
}
if int(num) >= len(body.Attachments) {
ctx.Session.AddFlash("Attachment number too high", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
part := body.Attachments[num]
// Output attachment
w.Header().Set("Content-Type", part.ContentType)
if _, err := io.Copy(w, part); err != nil {
return err
}
return nil
}

35
pkg/webui/recent.go Normal file
View File

@@ -0,0 +1,35 @@
package webui
import (
"github.com/jhillyerd/inbucket/pkg/server/web"
)
const (
// maximum mailboxes to remember
maxRemembered = 8
// session value key; referenced in templates, do not change
mailboxKey = "recentMailboxes"
)
// RememberMailbox manages the list of recently accessed mailboxes stored in the session
func RememberMailbox(ctx *web.Context, mailbox string) {
recent := RecentMailboxes(ctx)
newRecent := make([]string, 1, maxRemembered)
newRecent[0] = mailbox
for _, recBox := range recent {
// Insert until newRecent is full, but don't repeat the new mailbox
if len(newRecent) < maxRemembered && mailbox != recBox {
newRecent = append(newRecent, recBox)
}
}
ctx.Session.Values[mailboxKey] = newRecent
}
// RecentMailboxes returns a slice of the most recently accessed mailboxes
func RecentMailboxes(ctx *web.Context) []string {
val := ctx.Session.Values[mailboxKey]
recent, _ := val.([]string)
return recent
}

View File

@@ -0,0 +1,106 @@
package webui
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/pkg/stringutil"
)
// RootIndex serves the Inbucket landing page
func RootIndex(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
greeting, err := ioutil.ReadFile(config.GetWebConfig().GreetingFile)
if err != nil {
return fmt.Errorf("Failed to load greeting: %v", err)
}
// Get flash messages, save session
errorFlash := ctx.Session.Flashes("errors")
if err = ctx.Session.Save(req, w); err != nil {
return err
}
// Render template
return web.RenderTemplate("root/index.html", w, map[string]interface{}{
"ctx": ctx,
"errorFlash": errorFlash,
"greeting": template.HTML(string(greeting)),
})
}
// RootMonitor serves the Inbucket monitor page
func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
if !config.GetWebConfig().MonitorVisible {
ctx.Session.AddFlash("Monitor is disabled in configuration", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
// Get flash messages, save session
errorFlash := ctx.Session.Flashes("errors")
if err = ctx.Session.Save(req, w); err != nil {
return err
}
// Render template
return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{
"ctx": ctx,
"errorFlash": errorFlash,
})
}
// RootMonitorMailbox serves the Inbucket monitor page for a particular mailbox
func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
if !config.GetWebConfig().MonitorVisible {
ctx.Session.AddFlash("Monitor is disabled in configuration", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
// Get flash messages, save session
errorFlash := ctx.Session.Flashes("errors")
if err = ctx.Session.Save(req, w); err != nil {
return err
}
// Render template
return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{
"ctx": ctx,
"errorFlash": errorFlash,
"name": name,
})
}
// RootStatus serves the Inbucket status page
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
smtpListener := fmt.Sprintf("%s:%d", config.GetSMTPConfig().IP4address.String(),
config.GetSMTPConfig().IP4port)
pop3Listener := fmt.Sprintf("%s:%d", config.GetPOP3Config().IP4address.String(),
config.GetPOP3Config().IP4port)
webListener := fmt.Sprintf("%s:%d", config.GetWebConfig().IP4address.String(),
config.GetWebConfig().IP4port)
// Get flash messages, save session
errorFlash := ctx.Session.Flashes("errors")
if err = ctx.Session.Save(req, w); err != nil {
return err
}
// Render template
return web.RenderTemplate("root/status.html", w, map[string]interface{}{
"ctx": ctx,
"errorFlash": errorFlash,
"version": config.Version,
"buildDate": config.BuildDate,
"smtpListener": smtpListener,
"pop3Listener": pop3Listener,
"webListener": webListener,
"smtpConfig": config.GetSMTPConfig(),
"dataStoreConfig": config.GetDataStoreConfig(),
})
}

35
pkg/webui/routes.go Normal file
View File

@@ -0,0 +1,35 @@
// Package webui powers Inbucket's web GUI
package webui
import (
"github.com/gorilla/mux"
"github.com/jhillyerd/inbucket/pkg/server/web"
)
// SetupRoutes populates routes for the webui into the provided Router
func SetupRoutes(r *mux.Router) {
r.Path("/").Handler(
web.Handler(RootIndex)).Name("RootIndex").Methods("GET")
r.Path("/monitor").Handler(
web.Handler(RootMonitor)).Name("RootMonitor").Methods("GET")
r.Path("/monitor/{name}").Handler(
web.Handler(RootMonitorMailbox)).Name("RootMonitorMailbox").Methods("GET")
r.Path("/status").Handler(
web.Handler(RootStatus)).Name("RootStatus").Methods("GET")
r.Path("/link/{name}/{id}").Handler(
web.Handler(MailboxLink)).Name("MailboxLink").Methods("GET")
r.Path("/mailbox").Handler(
web.Handler(MailboxIndex)).Name("MailboxIndex").Methods("GET")
r.Path("/mailbox/{name}").Handler(
web.Handler(MailboxList)).Name("MailboxList").Methods("GET")
r.Path("/mailbox/{name}/{id}").Handler(
web.Handler(MailboxShow)).Name("MailboxShow").Methods("GET")
r.Path("/mailbox/{name}/{id}/html").Handler(
web.Handler(MailboxHTML)).Name("MailboxHtml").Methods("GET")
r.Path("/mailbox/{name}/{id}/source").Handler(
web.Handler(MailboxSource)).Name("MailboxSource").Methods("GET")
r.Path("/mailbox/dattach/{name}/{id}/{num}/{file}").Handler(
web.Handler(MailboxDownloadAttach)).Name("MailboxDownloadAttach").Methods("GET")
r.Path("/mailbox/vattach/{name}/{id}/{num}/{file}").Handler(
web.Handler(MailboxViewAttach)).Name("MailboxViewAttach").Methods("GET")
}

110
pkg/webui/sanitize/css.go Normal file
View File

@@ -0,0 +1,110 @@
package sanitize
import (
"bytes"
"strings"
"github.com/gorilla/css/scanner"
)
// propertyRule may someday allow control of what values are valid for a particular property.
type propertyRule struct{}
var allowedProperties = map[string]propertyRule{
"align": {},
"background-color": {},
"border": {},
"border-bottom": {},
"border-left": {},
"border-radius": {},
"border-right": {},
"border-top": {},
"box-sizing": {},
"clear": {},
"color": {},
"content": {},
"display": {},
"font-family": {},
"font-size": {},
"font-weight": {},
"height": {},
"line-height": {},
"margin": {},
"margin-bottom": {},
"margin-left": {},
"margin-right": {},
"margin-top": {},
"max-height": {},
"max-width": {},
"overflow": {},
"padding": {},
"padding-bottom": {},
"padding-left": {},
"padding-right": {},
"padding-top": {},
"table-layout": {},
"text-align": {},
"text-decoration": {},
"text-shadow": {},
"vertical-align": {},
"width": {},
"word-break": {},
}
// Handler Token, return next state.
type stateHandler func(b *bytes.Buffer, t *scanner.Token) stateHandler
func sanitizeStyle(input string) string {
b := &bytes.Buffer{}
scan := scanner.New(input)
state := stateStart
for {
t := scan.Next()
if t.Type == scanner.TokenEOF {
return b.String()
}
if t.Type == scanner.TokenError {
return ""
}
state = state(b, t)
if state == nil {
return ""
}
}
}
func stateStart(b *bytes.Buffer, t *scanner.Token) stateHandler {
switch t.Type {
case scanner.TokenIdent:
_, ok := allowedProperties[strings.ToLower(t.Value)]
if !ok {
return stateEat
}
b.WriteString(t.Value)
return stateValid
case scanner.TokenS:
return stateStart
}
// Unexpected type.
b.WriteString("/*" + t.Type.String() + "*/")
return stateEat
}
func stateEat(b *bytes.Buffer, t *scanner.Token) stateHandler {
if t.Type == scanner.TokenChar && t.Value == ";" {
// Done eating.
return stateStart
}
// Throw away this token.
return stateEat
}
func stateValid(b *bytes.Buffer, t *scanner.Token) stateHandler {
state := stateValid
if t.Type == scanner.TokenChar && t.Value == ";" {
// End of property.
state = stateStart
}
b.WriteString(t.Value)
return state
}

View File

@@ -0,0 +1,34 @@
package sanitize
import (
"testing"
)
func TestSanitizeStyle(t *testing.T) {
testCases := []struct {
input, want string
}{
{"", ""},
{
"color: red;",
"color: red;",
},
{
"background-color: black; color: white",
"background-color: black;color: white",
},
{
"background-color: black; invalid: true; color: white",
"background-color: black;color: white",
},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
got := sanitizeStyle(tc.input)
if got != tc.want {
t.Errorf("got: %q, want: %q, input: %q", got, tc.want, tc.input)
}
})
}
}

View File

@@ -0,0 +1,88 @@
package sanitize
import (
"bufio"
"bytes"
"io"
"regexp"
"strings"
"github.com/microcosm-cc/bluemonday"
"golang.org/x/net/html"
)
var (
cssSafe = regexp.MustCompile(".*")
policy = bluemonday.UGCPolicy().
AllowElements("center").
AllowAttrs("style").Matching(cssSafe).Globally()
)
func HTML(html string) (output string, err error) {
output, err = sanitizeStyleTags(html)
if err != nil {
return "", err
}
output = policy.Sanitize(output)
return
}
func sanitizeStyleTags(input string) (string, error) {
r := strings.NewReader(input)
b := &bytes.Buffer{}
if err := styleTagFilter(b, r); err != nil {
return "", err
}
return b.String(), nil
}
func styleTagFilter(w io.Writer, r io.Reader) error {
bw := bufio.NewWriter(w)
b := make([]byte, 256)
z := html.NewTokenizer(r)
for {
b = b[:0]
tt := z.Next()
switch tt {
case html.ErrorToken:
err := z.Err()
if err == io.EOF {
return bw.Flush()
}
return err
case html.StartTagToken, html.SelfClosingTagToken:
name, hasAttr := z.TagName()
if !hasAttr {
bw.Write(z.Raw())
continue
}
b = append(b, '<')
b = append(b, name...)
for {
key, val, more := z.TagAttr()
strval := string(val)
style := false
if strings.ToLower(string(key)) == "style" {
style = true
strval = sanitizeStyle(strval)
}
if !style || strval != "" {
b = append(b, ' ')
b = append(b, key...)
b = append(b, '=', '"')
b = append(b, []byte(html.EscapeString(strval))...)
b = append(b, '"')
}
if !more {
break
}
}
if tt == html.SelfClosingTagToken {
b = append(b, '/')
}
bw.Write(append(b, '>'))
default:
bw.Write(z.Raw())
}
}
}

View File

@@ -0,0 +1,171 @@
package sanitize_test
import (
"testing"
"github.com/jhillyerd/inbucket/pkg/webui/sanitize"
)
// TestHTMLPlainStrings test plain text passthrough
func TestHTMLPlainStrings(t *testing.T) {
testStrings := []string{
"",
"plain string",
"one &lt; two",
}
for _, ts := range testStrings {
t.Run(ts, func(t *testing.T) {
got, err := sanitize.HTML(ts)
if err != nil {
t.Fatal(err)
}
if got != ts {
t.Errorf("Got: %q, want: %q", got, ts)
}
})
}
}
// TestHTMLSimpleFormatting tests basic tags we should allow
func TestHTMLSimpleFormatting(t *testing.T) {
testStrings := []string{
"<p>paragraph</p>",
"<b>bold</b>",
"<i>italic</b>",
"<em>emphasis</em>",
"<strong>strong</strong>",
"<div><span>text</span></div>",
"<center>text</center>",
}
for _, ts := range testStrings {
t.Run(ts, func(t *testing.T) {
got, err := sanitize.HTML(ts)
if err != nil {
t.Fatal(err)
}
if got != ts {
t.Errorf("Got: %q, want: %q", got, ts)
}
})
}
}
// TestHTMLScriptTags tests some strings with JavaScript
func TestHTMLScriptTags(t *testing.T) {
testCases := []struct {
input, want string
}{
{
`safe<script>nope</script>`,
`safe`,
},
{
`<a onblur="alert(something)" href="http://mysite.com">mysite</a>`,
`<a href="http://mysite.com" rel="nofollow">mysite</a>`,
},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
got, err := sanitize.HTML(tc.input)
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("Got: %q, want: %q", got, tc.want)
}
})
}
}
func TestSanitizeStyleTags(t *testing.T) {
testCases := []struct {
name, input, want string
}{
{
"empty",
``,
``,
},
{
"open",
`<div>`,
`<div>`,
},
{
"open close",
`<div></div>`,
`<div></div>`,
},
{
"inner text",
`<div>foo bar</div>`,
`<div>foo bar</div>`,
},
{
"self close",
`<br/>`,
`<br/>`,
},
{
"open params",
`<div id="me">`,
`<div id="me">`,
},
{
"open params squote",
`<div id="me" title='best'>`,
`<div id="me" title="best">`,
},
{
"open style",
`<div id="me" style="color: red;">`,
`<div id="me" style="color: red;">`,
},
{
"open style squote",
`<div id="me" style='color: red;'>`,
`<div id="me" style="color: red;">`,
},
{
"open style mixed case",
`<div id="me" StYlE="color: red;">`,
`<div id="me" style="color: red;">`,
},
{
"closed style",
`<br style="border: 1px solid red;"/>`,
`<br style="border: 1px solid red;"/>`,
},
{
"mixed case style",
`<br StYlE="border: 1px solid red;"/>`,
`<br style="border: 1px solid red;"/>`,
},
{
"mixed case invalid style",
`<br StYlE="position: fixed;"/>`,
`<br/>`,
},
{
"mixed",
`<p id='i' title="cla'zz" style="font-size: 25px;"><b>some text</b></p>`,
`<p id="i" title="cla&#39;zz" style="font-size: 25px;"><b>some text</b></p>`,
},
{
"invalid styles",
`<div id="me" style='position: absolute;'>`,
`<div id="me">`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := sanitize.HTML(tc.input)
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("input: %s\ngot : %s\nwant: %s", tc.input, got, tc.want)
}
})
}
}