Commit 35d16226b9
Verified · cmc
Layout: unified · split
app/escape_test.go added +150
| @@ -0,0 +1,150 @@ | |||
| 1 | package app | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "html/template" | ||
| 5 | "net/http/httptest" | ||
| 6 | "skunkyart/static" | ||
| 7 | "strings" | ||
| 8 | "sync" | ||
| 9 | "testing" | ||
| 10 | |||
| 11 | "github.com/krazywarez/devianter" | ||
| 12 | ) | ||
| 13 | |||
| 14 | var loadTemplatesOnce sync.Once | ||
| 15 | |||
| 16 | // loadTemplates makes static.Templates usable from a test. The non-embed build | ||
| 17 | // reads the repository's static/ directory; the embed build already has it. | ||
| 18 | func loadTemplates() { | ||
| 19 | loadTemplatesOnce.Do(func() { | ||
| 20 | static.StaticPath = "../static" | ||
| 21 | static.CopyTemplatesToMemory() | ||
| 22 | }) | ||
| 23 | } | ||
| 24 | |||
| 25 | // markup is the payload every escaping test injects. It closes an attribute, | ||
| 26 | // closes a tag and opens a new element, which is what an injection needs to do. | ||
| 27 | const markup = `"><b id=injected>x</b>` | ||
| 28 | |||
| 29 | // TestEveryPageTemplateRenders pins down that the switch to html/template | ||
| 30 | // parses and executes every page: html/template rejects some constructs | ||
| 31 | // text/template accepts, and a failure here would be a 500 on every request. | ||
| 32 | func TestEveryPageTemplateRenders(t *testing.T) { | ||
| 33 | loadTemplates() | ||
| 34 | for _, page := range []string{"about.htm", "daily.htm", "deviantion.htm", "gruser.htm", "search.htm"} { | ||
| 35 | rec := httptest.NewRecorder() | ||
| 36 | s := skunkyart{Writer: rec, Host: "http://localhost", BasePath: "/"} | ||
| 37 | s.ExecuteTemplate(page, "html", &s) | ||
| 38 | if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") { | ||
| 39 | t.Errorf("%s: status %d, body %q", page, rec.Code, rec.Body.String()) | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | rec := httptest.NewRecorder() | ||
| 44 | uri := "/" | ||
| 45 | skunkyart{Writer: rec}.ExecuteTemplate("index.htm", "html", &uri) | ||
| 46 | if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") { | ||
| 47 | t.Errorf("index.htm: status %d, body %q", rec.Code, rec.Body.String()) | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | // TestSearchPageEscapesTheQuery is the regression test for the reflected | ||
| 52 | // query: it appears in the search box's value attribute and in the results | ||
| 53 | // heading, and both must show it as text. | ||
| 54 | func TestSearchPageEscapesTheQuery(t *testing.T) { | ||
| 55 | loadTemplates() | ||
| 56 | rec := httptest.NewRecorder() | ||
| 57 | s := skunkyart{Writer: rec, Host: "http://localhost", BasePath: "/", Endpoint: "search", QueryRaw: markup} | ||
| 58 | s.Templates.Search.List = template.HTML("<div></div>") | ||
| 59 | s.Templates.Search.Content.Total = 1 | ||
| 60 | s.ExecuteTemplate("search.htm", "html", &s) | ||
| 61 | |||
| 62 | body := rec.Body.String() | ||
| 63 | if strings.Contains(body, "<b id=injected>") { | ||
| 64 | t.Fatalf("query rendered as markup:\n%s", body) | ||
| 65 | } | ||
| 66 | if n := strings.Count(body, "<b id=injected>"); n != 3 { | ||
| 67 | t.Errorf("escaped query appears %d times, want 3 (title, value attribute, heading):\n%s", n, body) | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | // TestDeviationListEscapesTitles covers the Go-built listing, which | ||
| 72 | // html/template cannot escape because it arrives as template.HTML. | ||
| 73 | func TestDeviationListEscapesTitles(t *testing.T) { | ||
| 74 | nsfw := CFG.Nsfw | ||
| 75 | CFG.Nsfw = true | ||
| 76 | defer func() { CFG.Nsfw = nsfw }() | ||
| 77 | |||
| 78 | d := devianter.Deviation{Title: markup} | ||
| 79 | d.Author.Username = markup | ||
| 80 | devs := []devianter.Deviation{d} | ||
| 81 | |||
| 82 | out := skunkyart{Host: "http://localhost"}.DeviationList(devs, false) | ||
| 83 | if strings.Contains(out, "<b id=injected>") || !strings.Contains(out, "<b id=injected>") { | ||
| 84 | t.Errorf("HTML listing did not escape the title:\n%s", out) | ||
| 85 | } | ||
| 86 | |||
| 87 | rec := httptest.NewRecorder() | ||
| 88 | skunkyart{Host: "http://localhost", Writer: rec, Atom: true}.DeviationList(devs, true) | ||
| 89 | if feed := rec.Body.String(); strings.Contains(feed, "<b id=injected>") || !strings.Contains(feed, "<b id=injected>") { | ||
| 90 | t.Errorf("Atom feed did not escape the title:\n%s", feed) | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | // TestParseCommentsEscapesUsernames covers the comment thread, where the name | ||
| 95 | // is written as link text and as the "In reply to" target. | ||
| 96 | func TestParseCommentsEscapesUsernames(t *testing.T) { | ||
| 97 | var c devianter.Comments | ||
| 98 | var parent, reply devianter.Thread | ||
| 99 | parent.ID = 1 | ||
| 100 | parent.User.Username = markup | ||
| 101 | reply.ID = 2 | ||
| 102 | reply.Parent = 1 | ||
| 103 | reply.User.Username = "bob" | ||
| 104 | c.Thread = []devianter.Thread{parent, reply} | ||
| 105 | |||
| 106 | out := skunkyart{Host: "http://localhost", _pth: "/post/x/y"}.ParseComments(c, devianter.Error{}) | ||
| 107 | if strings.Contains(out, "<b id=injected>") { | ||
| 108 | t.Fatalf("username rendered as markup:\n%s", out) | ||
| 109 | } | ||
| 110 | if n := strings.Count(out, "<b id=injected>"); n != 4 { | ||
| 111 | t.Errorf("escaped username appears %d times, want 4 (avatar, link, author, reply target):\n%s", n, out) | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | // TestParseDescriptionEscapesMarkupText covers the plain-HTML branch: text is | ||
| 116 | // escaped, whitelisted tags are kept bare, and anything else is dropped. | ||
| 117 | func TestParseDescriptionEscapesMarkupText(t *testing.T) { | ||
| 118 | var d devianter.Text | ||
| 119 | d.Html.Markup = `a <b class="z">b</b> <script>alert(1)</script> <i>` | ||
| 120 | |||
| 121 | out := ParseDescription("http://localhost", d) | ||
| 122 | for _, bad := range []string{"<script>", `class="z"`, "<i>"} { | ||
| 123 | if strings.Contains(out, bad) { | ||
| 124 | t.Errorf("output contains %q:\n%s", bad, out) | ||
| 125 | } | ||
| 126 | } | ||
| 127 | for _, want := range []string{"<b>b</b>", "<i>"} { | ||
| 128 | if !strings.Contains(out, want) { | ||
| 129 | t.Errorf("output lacks %q:\n%s", want, out) | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | // TestErrorPageShowsOneEscapedLine covers the 502 page: a WAF block arrives | ||
| 135 | // as a whole HTML document, and only its first line is echoed, as text. | ||
| 136 | func TestErrorPageShowsOneEscapedLine(t *testing.T) { | ||
| 137 | rec := httptest.NewRecorder() | ||
| 138 | skunkyart{Writer: rec, Host: "http://localhost"}.Error(devianter.Error{Error: "blocked <!DOCTYPE html>\n<html>second line"}) | ||
| 139 | |||
| 140 | body := rec.Body.String() | ||
| 141 | if rec.Code != 502 { | ||
| 142 | t.Errorf("status %d, want 502", rec.Code) | ||
| 143 | } | ||
| 144 | if strings.Contains(body, "second line") { | ||
| 145 | t.Errorf("error page carries lines past the first:\n%s", body) | ||
| 146 | } | ||
| 147 | if strings.Contains(body, "<!DOCTYPE html>") || !strings.Contains(body, "<!DOCTYPE html>") { | ||
| 148 | t.Errorf("upstream error not escaped:\n%s", body) | ||
| 149 | } | ||
| 150 | } | ||
app/parsers.go +43 −31
| @@ -32,9 +32,9 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) | |||
| 32 | cmmts.WriteString(`"><p id="`) | 32 | cmmts.WriteString(`"><p id="`) |
| 33 | cmmts.WriteString(strconv.Itoa(x.ID)) | 33 | cmmts.WriteString(strconv.Itoa(x.ID)) |
| 34 | cmmts.WriteString(`"><img src="`) | 34 | cmmts.WriteString(`"><img src="`) |
| 35 | cmmts.WriteString(URLBuilder(s.Host, "media", "emojitar", x.User.Username, "?type=a")) | 35 | cmmts.WriteString(esc(URLBuilder(s.Host, "media", "emojitar", x.User.Username, "?type=a"))) |
| 36 | cmmts.WriteString(`" width="30px" height="30px"><a href="`) | 36 | cmmts.WriteString(`" width="30px" height="30px"><a href="`) |
| 37 | cmmts.WriteString(URLBuilder(s.Host, "group_user", "?q=", x.User.Username, "&type=a")) | 37 | cmmts.WriteString(esc(URLBuilder(s.Host, "group_user", "?q=", x.User.Username, "&type=a"))) |
| 38 | cmmts.WriteString(`"><b`) | 38 | cmmts.WriteString(`"><b`) |
| 39 | cmmts.WriteString(` class="`) | 39 | cmmts.WriteString(` class="`) |
| 40 | if x.User.Banned { | 40 | if x.User.Banned { |
| @@ -44,19 +44,19 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) | |||
| 44 | cmmts.WriteString(`author`) | 44 | cmmts.WriteString(`author`) |
| 45 | } | 45 | } |
| 46 | cmmts.WriteString(`">`) | 46 | cmmts.WriteString(`">`) |
| 47 | cmmts.WriteString(x.User.Username) | 47 | cmmts.WriteString(esc(x.User.Username)) |
| 48 | cmmts.WriteString("</b></a> ") | 48 | cmmts.WriteString("</b></a> ") |
| 49 | 49 | ||
| 50 | if x.Parent > 0 { | 50 | if x.Parent > 0 { |
| 51 | cmmts.WriteString(` In reply to <a href="`) | 51 | cmmts.WriteString(` In reply to <a href="`) |
| 52 | cmmts.WriteString(s._pth) | 52 | cmmts.WriteString(esc(s._pth)) |
| 53 | cmmts.WriteString("#") | 53 | cmmts.WriteString("#") |
| 54 | cmmts.WriteString(strconv.Itoa(x.Parent)) | 54 | cmmts.WriteString(strconv.Itoa(x.Parent)) |
| 55 | cmmts.WriteString(`">`) | 55 | cmmts.WriteString(`">`) |
| 56 | if replied[x.Parent] == "" { | 56 | if replied[x.Parent] == "" { |
| 57 | cmmts.WriteString("???") | 57 | cmmts.WriteString("???") |
| 58 | } else { | 58 | } else { |
| 59 | cmmts.WriteString(replied[x.Parent]) | 59 | cmmts.WriteString(esc(replied[x.Parent])) |
| 60 | } | 60 | } |
| 61 | cmmts.WriteString("</a>") | 61 | cmmts.WriteString("</a>") |
| 62 | } | 62 | } |
| @@ -108,27 +108,31 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con | |||
| 108 | if !VisibleDeviation(data) { | 108 | if !VisibleDeviation(data) { |
| 109 | continue | 109 | continue |
| 110 | } | 110 | } |
| 111 | if preview, fullview := ParseMedia(s.Host, data.Media, 320), ParseMedia(s.Host, data.Media); true { | 111 | // Escaped once here: the same values go into both the HTML grid and the |
| 112 | // Atom feed, and html.EscapeString produces entities XML accepts too. | ||
| 113 | author, title := esc(data.Author.Username), esc(data.Title) | ||
| 114 | postURL := esc(ConvertDeviantArtURLToSkunkyArt(s.Host, data.Url)) | ||
| 115 | if preview, fullview := esc(ParseMedia(s.Host, data.Media, 320)), esc(ParseMedia(s.Host, data.Media)); true { | ||
| 112 | if allowAtom && s.Atom { | 116 | if allowAtom && s.Atom { |
| 113 | s.Writer.Header().Add("Content-Type", "application/atom+xml") | 117 | s.Writer.Header().Add("Content-Type", "application/atom+xml") |
| 114 | id := strconv.Itoa(data.ID) | 118 | id := strconv.Itoa(data.ID) |
| 115 | listContent.WriteString(`<entry><author><name>`) | 119 | listContent.WriteString(`<entry><author><name>`) |
| 116 | listContent.WriteString(data.Author.Username) | 120 | listContent.WriteString(author) |
| 117 | listContent.WriteString(`</name></author><title>`) | 121 | listContent.WriteString(`</name></author><title>`) |
| 118 | listContent.WriteString(data.Title) | 122 | listContent.WriteString(title) |
| 119 | listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`) | 123 | listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`) |
| 120 | listContent.WriteString(URLBuilder(s.Host, "post", data.Author.Username, "atom-"+id)) | 124 | listContent.WriteString(esc(URLBuilder(s.Host, "post", data.Author.Username, "atom-"+id))) |
| 121 | listContent.WriteString(`"/><id>`) | 125 | listContent.WriteString(`"/><id>`) |
| 122 | listContent.WriteString(id) | 126 | listContent.WriteString(id) |
| 123 | listContent.WriteString(`</id><published>`) | 127 | listContent.WriteString(`</id><published>`) |
| 124 | listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")) | 128 | listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")) |
| 125 | listContent.WriteString(`</published>`) | 129 | listContent.WriteString(`</published>`) |
| 126 | listContent.WriteString(`<media:group><media:title>`) | 130 | listContent.WriteString(`<media:group><media:title>`) |
| 127 | listContent.WriteString(data.Title) | 131 | listContent.WriteString(title) |
| 128 | listContent.WriteString(`</media:title><media:thumbinal url="`) | 132 | listContent.WriteString(`</media:title><media:thumbinal url="`) |
| 129 | listContent.WriteString(preview) | 133 | listContent.WriteString(preview) |
| 130 | listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`) | 134 | listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`) |
| 131 | listContent.WriteString(ConvertDeviantArtURLToSkunkyArt(s.Host, data.Url)) | 135 | listContent.WriteString(postURL) |
| 132 | listContent.WriteString(`"><img src="`) | 136 | listContent.WriteString(`"><img src="`) |
| 133 | listContent.WriteString(fullview) | 137 | listContent.WriteString(fullview) |
| 134 | listContent.WriteString(`"/></a><p>`) | 138 | listContent.WriteString(`"/></a><p>`) |
| @@ -146,11 +150,11 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con | |||
| 146 | listContent.WriteString(`<h1>[ TEXT ]</h1>`) | 150 | listContent.WriteString(`<h1>[ TEXT ]</h1>`) |
| 147 | } | 151 | } |
| 148 | listContent.WriteString(`<br><a href="`) | 152 | listContent.WriteString(`<br><a href="`) |
| 149 | listContent.WriteString(ConvertDeviantArtURLToSkunkyArt(s.Host, data.Url)) | 153 | listContent.WriteString(postURL) |
| 150 | listContent.WriteString(`">`) | 154 | listContent.WriteString(`">`) |
| 151 | listContent.WriteString(data.Author.Username) | 155 | listContent.WriteString(author) |
| 152 | listContent.WriteString(" - ") | 156 | listContent.WriteString(" - ") |
| 153 | listContent.WriteString(data.Title) | 157 | listContent.WriteString(title) |
| 154 | 158 | ||
| 155 | if data.NSFW { | 159 | if data.NSFW { |
| 156 | listContent.WriteString(` [<span class="nsfw">NSFW</span>]`) | 160 | listContent.WriteString(` [<span class="nsfw">NSFW</span>]`) |
| @@ -175,14 +179,14 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con | |||
| 175 | case s.Type == 0: | 179 | case s.Type == 0: |
| 176 | list.WriteString("Daily Deviations") | 180 | list.WriteString("Daily Deviations") |
| 177 | case s.Type == 'g' && len(devs) != 0: | 181 | case s.Type == 'g' && len(devs) != 0: |
| 178 | list.WriteString(devs[0].Author.Username) | 182 | list.WriteString(esc(devs[0].Author.Username)) |
| 179 | default: | 183 | default: |
| 180 | list.WriteString("SkunkyArt") | 184 | list.WriteString("SkunkyArt") |
| 181 | } | 185 | } |
| 182 | list.WriteString(`</title>`) | 186 | list.WriteString(`</title>`) |
| 183 | 187 | ||
| 184 | list.WriteString(`<link rel="alternate" href="`) | 188 | list.WriteString(`<link rel="alternate" href="`) |
| 185 | list.WriteString(s.Host) | 189 | list.WriteString(esc(s.Host)) |
| 186 | list.WriteString(`"/>`) | 190 | list.WriteString(`"/>`) |
| 187 | 191 | ||
| 188 | list.WriteString(listContent.String()) | 192 | list.WriteString(listContent.String()) |
| @@ -316,7 +320,7 @@ func ParseDescription(host string, dscr devianter.Text) string { | |||
| 316 | for n := range Styles { | 320 | for n := range Styles { |
| 317 | Styles := &Styles[n] | 321 | Styles := &Styles[n] |
| 318 | Styles.TxtRaw = x.Text[Styles.From:Styles.To] | 322 | Styles.TxtRaw = x.Text[Styles.From:Styles.To] |
| 319 | Styles.Txt = TagBuilder(Styles.TxtRaw, tags[Styles.From*Styles.To]...) | 323 | Styles.Txt = TagBuilder(esc(Styles.TxtRaw), tags[Styles.From*Styles.To]...) |
| 320 | } | 324 | } |
| 321 | } | 325 | } |
| 322 | 326 | ||
| @@ -325,13 +329,13 @@ func ParseDescription(host string, dscr devianter.Text) string { | |||
| 325 | if len(x.EntityRanges) != 0 { | 329 | if len(x.EntityRanges) != 0 { |
| 326 | d := entities[x.EntityRanges[0].Key] | 330 | d := entities[x.EntityRanges[0].Key] |
| 327 | parsedDescription.WriteString(`<a href="`) | 331 | parsedDescription.WriteString(`<a href="`) |
| 328 | parsedDescription.WriteString(ConvertDeviantArtURLToSkunkyArt(host, d.Url)) | 332 | parsedDescription.WriteString(esc(ConvertDeviantArtURLToSkunkyArt(host, d.Url))) |
| 329 | parsedDescription.WriteString(`"><img width="50%" src="`) | 333 | parsedDescription.WriteString(`"><img width="50%" src="`) |
| 330 | parsedDescription.WriteString(ParseMedia(host, d.Media)) | 334 | parsedDescription.WriteString(esc(ParseMedia(host, d.Media))) |
| 331 | parsedDescription.WriteString(`" title="`) | 335 | parsedDescription.WriteString(`" title="`) |
| 332 | parsedDescription.WriteString(d.Author.Username) | 336 | parsedDescription.WriteString(esc(d.Author.Username)) |
| 333 | parsedDescription.WriteString(" - ") | 337 | parsedDescription.WriteString(" - ") |
| 334 | parsedDescription.WriteString(d.Title) | 338 | parsedDescription.WriteString(esc(d.Title)) |
| 335 | parsedDescription.WriteString(`"></a>`) | 339 | parsedDescription.WriteString(`"></a>`) |
| 336 | } | 340 | } |
| 337 | case "unstyled": | 341 | case "unstyled": |
| @@ -342,22 +346,22 @@ func ParseDescription(host string, dscr devianter.Text) string { | |||
| 342 | tag = "h2" | 346 | tag = "h2" |
| 343 | } | 347 | } |
| 344 | 348 | ||
| 345 | parsedDescription.WriteString(x.Text[:r.From]) | 349 | parsedDescription.WriteString(esc(x.Text[:r.From])) |
| 346 | if len(urls) != 0 && len(x.EntityRanges) != 0 { | 350 | if len(urls) != 0 && len(x.EntityRanges) != 0 { |
| 347 | ra := &x.EntityRanges[0] | 351 | ra := &x.EntityRanges[0] |
| 348 | 352 | ||
| 349 | parsedDescription.WriteString(`<a target="_blank" href="`) | 353 | parsedDescription.WriteString(`<a target="_blank" href="`) |
| 350 | parsedDescription.WriteString(urls[ra.Key]) | 354 | parsedDescription.WriteString(esc(urls[ra.Key])) |
| 351 | parsedDescription.WriteString(`">`) | 355 | parsedDescription.WriteString(`">`) |
| 352 | parsedDescription.WriteString(r.Txt) | 356 | parsedDescription.WriteString(r.Txt) |
| 353 | parsedDescription.WriteString(`</a>`) | 357 | parsedDescription.WriteString(`</a>`) |
| 354 | } else if l > n+1 { | 358 | } else if l > n+1 { |
| 355 | parsedDescription.WriteString(r.Txt) | 359 | parsedDescription.WriteString(r.Txt) |
| 356 | } | 360 | } |
| 357 | parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:])) | 361 | parsedDescription.WriteString(TagBuilder(tag, esc(x.Text[r.To:]))) |
| 358 | } | 362 | } |
| 359 | } else { | 363 | } else { |
| 360 | parsedDescription.WriteString(x.Text) | 364 | parsedDescription.WriteString(esc(x.Text)) |
| 361 | } | 365 | } |
| 362 | } | 366 | } |
| 363 | parsedDescription.WriteString("<br>") | 367 | parsedDescription.WriteString("<br>") |
| @@ -377,9 +381,9 @@ func ParseDescription(host string, dscr devianter.Text) string { | |||
| 377 | if a.Key == "href" { | 381 | if a.Key == "href" { |
| 378 | url := DeleteTrackingFromURL(a.Val) | 382 | url := DeleteTrackingFromURL(a.Val) |
| 379 | parsedDescription.WriteString(`<a target="_blank" href="`) | 383 | parsedDescription.WriteString(`<a target="_blank" href="`) |
| 380 | parsedDescription.WriteString(url) | 384 | parsedDescription.WriteString(esc(url)) |
| 381 | parsedDescription.WriteString(`">`) | 385 | parsedDescription.WriteString(`">`) |
| 382 | parsedDescription.WriteString(GetValueOfTag(tt)) | 386 | parsedDescription.WriteString(esc(GetValueOfTag(tt))) |
| 383 | parsedDescription.WriteString("</a> ") | 387 | parsedDescription.WriteString("</a> ") |
| 384 | } | 388 | } |
| 385 | } | 389 | } |
| @@ -397,20 +401,28 @@ func ParseDescription(host string, dscr devianter.Text) string { | |||
| 397 | if title != "" { | 401 | if title != "" { |
| 398 | for x := -1; x < b; x++ { | 402 | for x := -1; x < b; x++ { |
| 399 | parsedDescription.WriteString(`<img src="`) | 403 | parsedDescription.WriteString(`<img src="`) |
| 400 | parsedDescription.WriteString(uri) | 404 | parsedDescription.WriteString(esc(uri)) |
| 401 | parsedDescription.WriteString(`" title="`) | 405 | parsedDescription.WriteString(`" title="`) |
| 402 | parsedDescription.WriteString(title) | 406 | parsedDescription.WriteString(esc(title)) |
| 403 | parsedDescription.WriteString(`">`) | 407 | parsedDescription.WriteString(`">`) |
| 404 | } | 408 | } |
| 405 | } | 409 | } |
| 406 | } | 410 | } |
| 407 | case "br", "li", "ul", "p", "b": | 411 | case "br", "li", "ul", "p", "b": |
| 408 | parsedDescription.WriteString(token.String()) | 412 | // The bare tag, not token.String(): that would carry over |
| 413 | // whatever attributes DeviantArt's markup put on it. | ||
| 414 | if token.Type == html.EndTagToken { | ||
| 415 | parsedDescription.WriteString("</") | ||
| 416 | } else { | ||
| 417 | parsedDescription.WriteString("<") | ||
| 418 | } | ||
| 419 | parsedDescription.WriteString(token.Data) | ||
| 420 | parsedDescription.WriteString(">") | ||
| 409 | case "div": | 421 | case "div": |
| 410 | parsedDescription.WriteString("<p> ") | 422 | parsedDescription.WriteString("<p> ") |
| 411 | } | 423 | } |
| 412 | case html.TextToken: | 424 | case html.TextToken: |
| 413 | parsedDescription.Write(tt.Text()) | 425 | parsedDescription.WriteString(esc(string(tt.Text()))) |
| 414 | } | 426 | } |
| 415 | } | 427 | } |
| 416 | } | 428 | } |
app/util.go +40 −23
| @@ -4,6 +4,8 @@ import ( | |||
| 4 | "context" | 4 | "context" |
| 5 | "encoding/json" | 5 | "encoding/json" |
| 6 | "fmt" | 6 | "fmt" |
| 7 | htmlesc "html" | ||
| 8 | "html/template" | ||
| 7 | "io" | 9 | "io" |
| 8 | "net/http" | 10 | "net/http" |
| 9 | "net/url" | 11 | "net/url" |
| @@ -11,7 +13,6 @@ import ( | |||
| 11 | "skunkyart/static" | 13 | "skunkyart/static" |
| 12 | "strconv" | 14 | "strconv" |
| 13 | "strings" | 15 | "strings" |
| 14 | "text/template" | ||
| 15 | "time" | 16 | "time" |
| 16 | 17 | ||
| 17 | "github.com/krazywarez/devianter" | 18 | "github.com/krazywarez/devianter" |
| @@ -41,6 +42,14 @@ func tryWithExitStatus(err error, code int) { | |||
| 41 | } | 42 | } |
| 42 | } | 43 | } |
| 43 | 44 | ||
| 45 | // esc escapes s for use as HTML text or inside a quoted attribute. The Go-built | ||
| 46 | // fragments bypass html/template's contextual escaping because they are handed | ||
| 47 | // to it as template.HTML, so every DeviantArt-supplied string they contain has | ||
| 48 | // to be escaped here instead. | ||
| 49 | func esc(s string) string { | ||
| 50 | return htmlesc.EscapeString(s) | ||
| 51 | } | ||
| 52 | |||
| 44 | // restore swallows a panic in the calling goroutine so that one bad parse cannot | 53 | // restore swallows a panic in the calling goroutine so that one bad parse cannot |
| 45 | // take the whole process down. The panic is logged rather than dropped silently. | 54 | // take the whole process down. The panic is logged rather than dropped silently. |
| 46 | func restore() { | 55 | func restore() { |
| @@ -101,44 +110,48 @@ type skunkyart struct { | |||
| 101 | API API | 110 | API API |
| 102 | Version string | 111 | Version string |
| 103 | 112 | ||
| 113 | // The template.HTML fields hold fragments the Go builders already | ||
| 114 | // escaped, so html/template inserts them as-is. Everything typed string is | ||
| 115 | // escaped by the template at the point of use. | ||
| 104 | Templates struct { | 116 | Templates struct { |
| 105 | About instanceAbout | 117 | About instanceAbout |
| 106 | 118 | ||
| 107 | SomeList string | 119 | SomeList template.HTML |
| 108 | DDStrips string | 120 | DDStrips template.HTML |
| 109 | Deviation struct { | 121 | Deviation struct { |
| 110 | Post devianter.Post | 122 | Post devianter.Post |
| 111 | Related string | 123 | Description template.HTML |
| 112 | StringTime string | 124 | Related template.HTML |
| 113 | Tags string | 125 | StringTime string |
| 114 | Comments string | 126 | Tags template.HTML |
| 127 | Comments template.HTML | ||
| 115 | } | 128 | } |
| 116 | 129 | ||
| 117 | GroupUser struct { | 130 | GroupUser struct { |
| 118 | GR devianter.GRuser | 131 | GR devianter.GRuser |
| 119 | Admins string | 132 | Admins template.HTML |
| 120 | Group bool | 133 | Group bool |
| 121 | CreationDate string | 134 | CreationDate string |
| 122 | 135 | ||
| 123 | About struct { | 136 | About struct { |
| 124 | A devianter.About | 137 | A devianter.About |
| 125 | 138 | ||
| 126 | DescriptionFormatted string | 139 | DescriptionFormatted template.HTML |
| 127 | Interests, Social string | 140 | Interests, Social template.HTML |
| 128 | Comments string | 141 | Comments template.HTML |
| 129 | BG string | 142 | BG string |
| 130 | BGMeta devianter.Deviation | 143 | BGMeta devianter.Deviation |
| 131 | } | 144 | } |
| 132 | 145 | ||
| 133 | Gallery struct { | 146 | Gallery struct { |
| 134 | Folders string | 147 | Folders template.HTML |
| 135 | Pages int | 148 | Pages int |
| 136 | List string | 149 | List template.HTML |
| 137 | } | 150 | } |
| 138 | } | 151 | } |
| 139 | Search struct { | 152 | Search struct { |
| 140 | Content devianter.Search | 153 | Content devianter.Search |
| 141 | List string | 154 | List template.HTML |
| 142 | } | 155 | } |
| 143 | } | 156 | } |
| 144 | } | 157 | } |
| @@ -182,15 +195,19 @@ func URLBuilder(host string, strs ...string) string { | |||
| 182 | return str.String() | 195 | return str.String() |
| 183 | } | 196 | } |
| 184 | 197 | ||
| 185 | // Error responds 502 with the error DeviantArt reported upstream. | 198 | // Error responds 502 with the error DeviantArt reported upstream. Only the |
| 199 | // first line is shown: a WAF block arrives as a whole HTML page, which is | ||
| 200 | // neither readable nor safe to echo. | ||
| 186 | func (s skunkyart) Error(dAerr devianter.Error) { | 201 | func (s skunkyart) Error(dAerr devianter.Error) { |
| 187 | s.Writer.WriteHeader(502) | 202 | s.Writer.WriteHeader(502) |
| 188 | 203 | ||
| 204 | reason, _, _ := strings.Cut(dAerr.Error, "\n") | ||
| 205 | |||
| 189 | var msg strings.Builder | 206 | var msg strings.Builder |
| 190 | msg.WriteString(`<html><link rel="stylesheet" href="`) | 207 | msg.WriteString(`<html><link rel="stylesheet" href="`) |
| 191 | msg.WriteString(URLBuilder(s.Host, "stylesheet")) | 208 | msg.WriteString(URLBuilder(s.Host, "stylesheet")) |
| 192 | msg.WriteString(`" /><h3>DeviantArt error — '`) | 209 | msg.WriteString(`" /><h3>DeviantArt error — '`) |
| 193 | msg.WriteString(dAerr.Error) | 210 | msg.WriteString(esc(reason)) |
| 194 | msg.WriteString("'</h3></html>") | 211 | msg.WriteString("'</h3></html>") |
| 195 | 212 | ||
| 196 | wr(s.Writer, msg.String()) | 213 | wr(s.Writer, msg.String()) |
| @@ -319,11 +336,11 @@ func ConvertDeviantArtURLToSkunkyArt(host, url string) (output string) { | |||
| 319 | func BuildUserPlate(host, name string) string { | 336 | func BuildUserPlate(host, name string) string { |
| 320 | var htm strings.Builder | 337 | var htm strings.Builder |
| 321 | htm.WriteString(`<div class="user-plate"><img src="`) | 338 | htm.WriteString(`<div class="user-plate"><img src="`) |
| 322 | htm.WriteString(URLBuilder(host, "media", "emojitar", name, "?type=a")) | 339 | htm.WriteString(esc(URLBuilder(host, "media", "emojitar", name, "?type=a"))) |
| 323 | htm.WriteString(`"><a href="`) | 340 | htm.WriteString(`"><a href="`) |
| 324 | htm.WriteString(URLBuilder(host, "group_user", "?type=about&q=", name)) | 341 | htm.WriteString(esc(URLBuilder(host, "group_user", "?type=about&q=", name))) |
| 325 | htm.WriteString(`">`) | 342 | htm.WriteString(`">`) |
| 326 | htm.WriteString(name) | 343 | htm.WriteString(esc(name)) |
| 327 | htm.WriteString(`</a></div>`) | 344 | htm.WriteString(`</a></div>`) |
| 328 | return htm.String() | 345 | return htm.String() |
| 329 | } | 346 | } |
| @@ -355,7 +372,7 @@ func (s skunkyart) NavBase(c DeviationList) string { | |||
| 355 | prevrev := func(msg string, page int, onpage bool) { | 372 | prevrev := func(msg string, page int, onpage bool) { |
| 356 | if !onpage { | 373 | if !onpage { |
| 357 | list.WriteString(`<a href="`) | 374 | list.WriteString(`<a href="`) |
| 358 | list.WriteString(s._pth) | 375 | list.WriteString(esc(s._pth)) |
| 359 | list.WriteString(`?p=`) | 376 | list.WriteString(`?p=`) |
| 360 | list.WriteString(strconv.Itoa(page)) | 377 | list.WriteString(strconv.Itoa(page)) |
| 361 | if s.Type != 0 { | 378 | if s.Type != 0 { |
| @@ -364,11 +381,11 @@ func (s skunkyart) NavBase(c DeviationList) string { | |||
| 364 | } | 381 | } |
| 365 | if s.Query != "" { | 382 | if s.Query != "" { |
| 366 | list.WriteString("&q=") | 383 | list.WriteString("&q=") |
| 367 | list.WriteString(s.Query) | 384 | list.WriteString(esc(s.Query)) |
| 368 | } | 385 | } |
| 369 | if f := s.Args.Get("folder"); f != "" { | 386 | if f := s.Args.Get("folder"); f != "" { |
| 370 | list.WriteString("&folder=") | 387 | list.WriteString("&folder=") |
| 371 | list.WriteString(f) | 388 | list.WriteString(esc(f)) |
| 372 | } | 389 | } |
| 373 | list.WriteString(`">`) | 390 | list.WriteString(`">`) |
| 374 | list.WriteString(msg) | 391 | list.WriteString(msg) |
app/wrapper.go +43 −40
| @@ -1,6 +1,7 @@ | |||
| 1 | package app | 1 | package app |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "html/template" | ||
| 4 | "regexp" | 5 | "regexp" |
| 5 | "strconv" | 6 | "strconv" |
| 6 | "strings" | 7 | "strings" |
| @@ -42,33 +43,33 @@ func (s skunkyart) GRUser() { | |||
| 42 | var about = &x.ModuleData.GroupAbout | 43 | var about = &x.ModuleData.GroupAbout |
| 43 | group.Group = true | 44 | group.Group = true |
| 44 | group.CreationDate = x.ModuleData.GroupAbout.FoundatedAt.UTC().String() | 45 | group.CreationDate = x.ModuleData.GroupAbout.FoundatedAt.UTC().String() |
| 45 | group.About.DescriptionFormatted = ParseDescription(s.Host, about.Description) | 46 | group.About.DescriptionFormatted = template.HTML(ParseDescription(s.Host, about.Description)) //nolint:gosec // G203: ParseDescription escapes its input |
| 46 | } else if false { | 47 | } else if false { |
| 47 | group.About.A = x.ModuleData.About | 48 | group.About.A = x.ModuleData.About |
| 48 | var about = &group.About.A | 49 | var about = &group.About.A |
| 49 | group.CreationDate = time.Unix(time.Now().Unix()-x.ModuleData.About.RegDate, 0).UTC().String() | 50 | group.CreationDate = time.Unix(time.Now().Unix()-x.ModuleData.About.RegDate, 0).UTC().String() |
| 50 | group.About.DescriptionFormatted = ParseDescription(s.Host, about.Description) | 51 | group.About.DescriptionFormatted = template.HTML(ParseDescription(s.Host, about.Description)) //nolint:gosec // G203: ParseDescription escapes its input |
| 51 | 52 | ||
| 52 | for _, val := range x.ModuleData.About.SocialLinks { | 53 | for _, val := range x.ModuleData.About.SocialLinks { |
| 53 | var social strings.Builder | 54 | var social strings.Builder |
| 54 | social.WriteString(`<a target="_blank" href="`) | 55 | social.WriteString(`<a target="_blank" href="`) |
| 55 | social.WriteString(val.Value) | 56 | social.WriteString(esc(val.Value)) |
| 56 | social.WriteString(`">`) | 57 | social.WriteString(`">`) |
| 57 | social.WriteString(val.Value) | 58 | social.WriteString(esc(val.Value)) |
| 58 | social.WriteString("</a><br>") | 59 | social.WriteString("</a><br>") |
| 59 | group.About.Social += social.String() | 60 | group.About.Social += template.HTML(social.String()) //nolint:gosec // G203: escaped above |
| 60 | } | 61 | } |
| 61 | 62 | ||
| 62 | for _, val := range x.ModuleData.About.Interests { | 63 | for _, val := range x.ModuleData.About.Interests { |
| 63 | var interest strings.Builder | 64 | var interest strings.Builder |
| 64 | interest.WriteString(val.Label) | 65 | interest.WriteString(esc(val.Label)) |
| 65 | interest.WriteString(": <b>") | 66 | interest.WriteString(": <b>") |
| 66 | interest.WriteString(val.Value) | 67 | interest.WriteString(esc(val.Value)) |
| 67 | interest.WriteString("</b><br>") | 68 | interest.WriteString("</b><br>") |
| 68 | group.About.Interests += interest.String() | 69 | group.About.Interests += template.HTML(interest.String()) //nolint:gosec // G203: escaped above |
| 69 | } | 70 | } |
| 70 | } | 71 | } |
| 71 | group.About.Comments = s.ParseComments(devianter.GetComments(strconv.Itoa(group.GR.Gruser.ID), "", s.Page, 4)) | 72 | group.About.Comments = template.HTML(s.ParseComments(devianter.GetComments(strconv.Itoa(group.GR.Gruser.ID), "", s.Page, 4))) //nolint:gosec // G203: ParseComments escapes its input |
| 72 | 73 | ||
| 73 | case "cover_deviation": | 74 | case "cover_deviation": |
| 74 | group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation | 75 | group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation |
| @@ -79,7 +80,7 @@ func (s skunkyart) GRUser() { | |||
| 79 | for _, z := range x.ModuleData.GroupAdmins.Results { | 80 | for _, z := range x.ModuleData.GroupAdmins.Results { |
| 80 | htm.WriteString(BuildUserPlate(s.Host, z.User.Username)) | 81 | htm.WriteString(BuildUserPlate(s.Host, z.User.Username)) |
| 81 | } | 82 | } |
| 82 | group.Admins += htm.String() | 83 | group.Admins += template.HTML(htm.String()) //nolint:gosec // G203: BuildUserPlate escapes its input |
| 83 | } | 84 | } |
| 84 | 85 | ||
| 85 | } | 86 | } |
| @@ -110,9 +111,9 @@ func (s skunkyart) GRUser() { | |||
| 110 | } | 111 | } |
| 111 | 112 | ||
| 112 | if folderid > 0 || (s.Type == 'f' && all) { | 113 | if folderid > 0 || (s.Type == 'f' && all) { |
| 113 | group.Gallery.List = s.DeviationList(content.Content.Results, true, DeviationList{ | 114 | group.Gallery.List = template.HTML(s.DeviationList(content.Content.Results, true, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input |
| 114 | More: content.Content.HasMore, | 115 | More: content.Content.HasMore, |
| 115 | }) | 116 | })) |
| 116 | } else { | 117 | } else { |
| 117 | for _, x := range content.Content.Gruser.Page.Modules { | 118 | for _, x := range content.Content.Gruser.Page.Modules { |
| 118 | if len(x.ModuleData.Folders.Results) != 0 { | 119 | if len(x.ModuleData.Folders.Results) != 0 { |
| @@ -124,11 +125,11 @@ func (s skunkyart) GRUser() { | |||
| 124 | 125 | ||
| 125 | if !x.Thumb.NSFW || CFG.Nsfw { | 126 | if !x.Thumb.NSFW || CFG.Nsfw { |
| 126 | folders.WriteString(`<a href="`) | 127 | folders.WriteString(`<a href="`) |
| 127 | folders.WriteString(ConvertDeviantArtURLToSkunkyArt(s.Host, x.Thumb.Url)) | 128 | folders.WriteString(esc(ConvertDeviantArtURLToSkunkyArt(s.Host, x.Thumb.Url))) |
| 128 | folders.WriteString(`"><img loading="lazy" src="`) | 129 | folders.WriteString(`"><img loading="lazy" src="`) |
| 129 | folders.WriteString(ParseMedia(s.Host, x.Thumb.Media)) | 130 | folders.WriteString(esc(ParseMedia(s.Host, x.Thumb.Media))) |
| 130 | folders.WriteString(`" title="`) | 131 | folders.WriteString(`" title="`) |
| 131 | folders.WriteString(x.Thumb.Title) | 132 | folders.WriteString(esc(x.Thumb.Title)) |
| 132 | folders.WriteString(`"></a>`) | 133 | folders.WriteString(`"></a>`) |
| 133 | } else { | 134 | } else { |
| 134 | folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`) | 135 | folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`) |
| @@ -138,25 +139,25 @@ func (s skunkyart) GRUser() { | |||
| 138 | folders.WriteString(`<a href="group_user?folder=`) | 139 | folders.WriteString(`<a href="group_user?folder=`) |
| 139 | folders.WriteString(strconv.Itoa(x.FolderId)) | 140 | folders.WriteString(strconv.Itoa(x.FolderId)) |
| 140 | folders.WriteString("&q=") | 141 | folders.WriteString("&q=") |
| 141 | folders.WriteString(s.Query) | 142 | folders.WriteString(esc(s.Query)) |
| 142 | folders.WriteString("&type=") | 143 | folders.WriteString("&type=") |
| 143 | folders.WriteRune(s.Type) | 144 | folders.WriteRune(s.Type) |
| 144 | folders.WriteString(`">`) | 145 | folders.WriteString(`">`) |
| 145 | folders.WriteString(x.Name) | 146 | folders.WriteString(esc(x.Name)) |
| 146 | folders.WriteString(`</a>`) | 147 | folders.WriteString(`</a>`) |
| 147 | 148 | ||
| 148 | folders.WriteString("</div>") | 149 | folders.WriteString("</div>") |
| 149 | } | 150 | } |
| 150 | } | 151 | } |
| 151 | folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`) | 152 | folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`) |
| 152 | group.Gallery.Folders = folders.String() | 153 | group.Gallery.Folders = template.HTML(folders.String()) //nolint:gosec // G203: escaped above |
| 153 | } | 154 | } |
| 154 | 155 | ||
| 155 | if x.Name == "folder_deviations" { | 156 | if x.Name == "folder_deviations" { |
| 156 | group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{ | 157 | group.Gallery.List = template.HTML(s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input |
| 157 | Pages: x.ModuleData.Folder.Pages, | 158 | Pages: x.ModuleData.Folder.Pages, |
| 158 | More: x.ModuleData.Folder.HasMore, | 159 | More: x.ModuleData.Folder.HasMore, |
| 159 | }) | 160 | })) |
| 160 | } | 161 | } |
| 161 | } | 162 | } |
| 162 | } | 163 | } |
| @@ -201,14 +202,14 @@ func (s skunkyart) Deviation(author, postname string) { | |||
| 201 | } | 202 | } |
| 202 | 203 | ||
| 203 | if post.Post.Deviation.TextContent.Excerpt != "" { | 204 | if post.Post.Deviation.TextContent.Excerpt != "" { |
| 204 | post.Post.Description = ParseDescription(s.Host, post.Post.Deviation.TextContent) | 205 | post.Description = template.HTML(ParseDescription(s.Host, post.Post.Deviation.TextContent)) //nolint:gosec // G203: ParseDescription escapes its input |
| 205 | } else { | 206 | } else { |
| 206 | post.Post.Description = ParseDescription(s.Host, post.Post.Deviation.Extended.DescriptionText) | 207 | post.Description = template.HTML(ParseDescription(s.Host, post.Post.Deviation.Extended.DescriptionText)) //nolint:gosec // G203: ParseDescription escapes its input |
| 207 | } | 208 | } |
| 208 | 209 | ||
| 209 | for _, x := range post.Post.Deviation.Extended.RelatedContent { | 210 | for _, x := range post.Post.Deviation.Extended.RelatedContent { |
| 210 | if len(x.Deviations) != 0 { | 211 | if len(x.Deviations) != 0 { |
| 211 | post.Related += s.DeviationList(x.Deviations, false) | 212 | post.Related += template.HTML(s.DeviationList(x.Deviations, false)) //nolint:gosec // G203: DeviationList escapes its input |
| 212 | } | 213 | } |
| 213 | } | 214 | } |
| 214 | 215 | ||
| @@ -216,15 +217,15 @@ func (s skunkyart) Deviation(author, postname string) { | |||
| 216 | for _, x := range post.Post.Deviation.Extended.Tags { | 217 | for _, x := range post.Post.Deviation.Extended.Tags { |
| 217 | var tag strings.Builder | 218 | var tag strings.Builder |
| 218 | tag.WriteString(` <a href="`) | 219 | tag.WriteString(` <a href="`) |
| 219 | tag.WriteString(URLBuilder(s.Host, "search", "?q=", x.Name, "&type=tag")) | 220 | tag.WriteString(esc(URLBuilder(s.Host, "search", "?q=", x.Name, "&type=tag"))) |
| 220 | tag.WriteString(`">#`) | 221 | tag.WriteString(`">#`) |
| 221 | tag.WriteString(x.Name) | 222 | tag.WriteString(esc(x.Name)) |
| 222 | tag.WriteString("</a>") | 223 | tag.WriteString("</a>") |
| 223 | 224 | ||
| 224 | post.Tags += tag.String() | 225 | post.Tags += template.HTML(tag.String()) //nolint:gosec // G203: escaped above |
| 225 | } | 226 | } |
| 226 | 227 | ||
| 227 | post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1)) | 228 | post.Comments = template.HTML(s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))) //nolint:gosec // G203: ParseComments escapes its input |
| 228 | post.StringTime = post.Post.Deviation.PublishedTime.UTC().String() | 229 | post.StringTime = post.Post.Deviation.PublishedTime.UTC().String() |
| 229 | post.Post.IMG = ParseMedia(s.Host, post.Post.Deviation.Media) | 230 | post.Post.IMG = ParseMedia(s.Host, post.Post.Deviation.Media) |
| 230 | 231 | ||
| @@ -241,20 +242,20 @@ func (s skunkyart) DD() { | |||
| 241 | var strips strings.Builder | 242 | var strips strings.Builder |
| 242 | for _, x := range dd.Strips { | 243 | for _, x := range dd.Strips { |
| 243 | strips.WriteString(`<h3 class="`) | 244 | strips.WriteString(`<h3 class="`) |
| 244 | strips.WriteString(x.Codename) | 245 | strips.WriteString(esc(x.Codename)) |
| 245 | strips.WriteString(`"> <a href="#`) | 246 | strips.WriteString(`"> <a href="#`) |
| 246 | strips.WriteString(x.Codename) | 247 | strips.WriteString(esc(x.Codename)) |
| 247 | strips.WriteString(`"># </a>`) | 248 | strips.WriteString(`"># </a>`) |
| 248 | strips.WriteString(x.Title) | 249 | strips.WriteString(esc(x.Title)) |
| 249 | strips.WriteString(`</h3>`) | 250 | strips.WriteString(`</h3>`) |
| 250 | 251 | ||
| 251 | strips.WriteString(s.DeviationList(x.Deviations, false)) | 252 | strips.WriteString(s.DeviationList(x.Deviations, false)) |
| 252 | } | 253 | } |
| 253 | s.Templates.DDStrips = strips.String() | 254 | s.Templates.DDStrips = template.HTML(strips.String()) //nolint:gosec // G203: escaped above |
| 254 | s.Templates.SomeList = s.DeviationList(dd.Deviations, true, DeviationList{ | 255 | s.Templates.SomeList = template.HTML(s.DeviationList(dd.Deviations, true, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input |
| 255 | Pages: 0, | 256 | Pages: 0, |
| 256 | More: dd.HasMore, | 257 | More: dd.HasMore, |
| 257 | }) | 258 | })) |
| 258 | if !s.Atom { | 259 | if !s.Atom { |
| 259 | s.ExecuteTemplate("daily.htm", "html", &s) | 260 | s.ExecuteTemplate("daily.htm", "html", &s) |
| 260 | } | 261 | } |
| @@ -310,14 +311,16 @@ func (s skunkyart) Search() { | |||
| 310 | } | 311 | } |
| 311 | 312 | ||
| 312 | if len(usernames) != 0 { | 313 | if len(usernames) != 0 { |
| 313 | ss.List += `<div class="content plates">` | 314 | var plates strings.Builder |
| 315 | plates.WriteString(`<div class="content plates">`) | ||
| 314 | for x := range len(usernames) { | 316 | for x := range len(usernames) { |
| 315 | ss.List += BuildUserPlate(s.Host, usernames[x]) | 317 | plates.WriteString(BuildUserPlate(s.Host, usernames[x])) |
| 316 | } | 318 | } |
| 317 | ss.List += `</div>` | 319 | plates.WriteString(`</div>`) |
| 318 | ss.List += s.NavBase(DeviationList{ | 320 | plates.WriteString(s.NavBase(DeviationList{ |
| 319 | More: true, | 321 | More: true, |
| 320 | }) | 322 | })) |
| 323 | ss.List = template.HTML(plates.String()) //nolint:gosec // G203: BuildUserPlate escapes its input | ||
| 321 | } | 324 | } |
| 322 | default: | 325 | default: |
| 323 | s.ReturnHTTPError(400) | 326 | s.ReturnHTTPError(400) |
| @@ -331,10 +334,10 @@ func (s skunkyart) Search() { | |||
| 331 | return | 334 | return |
| 332 | } | 335 | } |
| 333 | 336 | ||
| 334 | ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{ | 337 | ss.List = template.HTML(s.DeviationList(ss.Content.Results, false, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input |
| 335 | Pages: ss.Content.Pages, | 338 | Pages: ss.Content.Pages, |
| 336 | More: ss.Content.HasMore, | 339 | More: ss.Content.HasMore, |
| 337 | }) | 340 | })) |
| 338 | } | 341 | } |
| 339 | 342 | ||
| 340 | s.ExecuteTemplate("search.htm", "html", &s) | 343 | s.ExecuteTemplate("search.htm", "html", &s) |
static/html/deviantion.htm +2 −2
| @@ -22,11 +22,11 @@ | |||
| 22 | <span>{{T "deviation.published"}}<strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong> | 22 | <span>{{T "deviation.published"}}<strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong> |
| 23 | <br><a target="_blank" href="https://www.deviantart.com/{{.Templates.Deviation.Post.Deviation.Author.Username}}/art/art-{{.Templates.Deviation.Post.Deviation.ID}}">{{T "deviation.original"}}</a> | 23 | <br><a target="_blank" href="https://www.deviantart.com/{{.Templates.Deviation.Post.Deviation.Author.Username}}/art/art-{{.Templates.Deviation.Post.Deviation.ID}}">{{T "deviation.original"}}</a> |
| 24 | </span> | 24 | </span> |
| 25 | {{if (ne .Templates.Deviation.Post.Description "")}} | 25 | {{if (ne .Templates.Deviation.Description "")}} |
| 26 | <figcaption> | 26 | <figcaption> |
| 27 | <details> | 27 | <details> |
| 28 | <summary>{{T "deviation.description"}}</summary> | 28 | <summary>{{T "deviation.description"}}</summary> |
| 29 | {{.Templates.Deviation.Post.Description}} | 29 | {{.Templates.Deviation.Description}} |
| 30 | </details> | 30 | </details> |
| 31 | </figcaption> | 31 | </figcaption> |
| 32 | {{end}} | 32 | {{end}} |