clean_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package clean
  2. import (
  3. "net/url"
  4. "strings"
  5. "testing"
  6. "golang.org/x/net/html"
  7. )
  8. var base, _ = url.Parse("https://example.com/posts/article")
  9. // body parses markup as the children of a <body>, like extracted content.
  10. func body(t *testing.T, markup string) *html.Node {
  11. t.Helper()
  12. doc, err := html.Parse(strings.NewReader("<body>" + markup + "</body>"))
  13. if err != nil {
  14. t.Fatal(err)
  15. }
  16. return doc.FirstChild.LastChild // html > body
  17. }
  18. func cleanString(t *testing.T, markup string) string {
  19. t.Helper()
  20. cleaned, err := Content(body(t, markup), base)
  21. if err != nil {
  22. t.Fatalf("Content(%q): %v", markup, err)
  23. }
  24. out, err := Render(cleaned)
  25. if err != nil {
  26. t.Fatalf("Render(%q): %v", markup, err)
  27. }
  28. if err := WellFormed("<div>" + out + "</div>"); err != nil {
  29. t.Fatalf("Content(%q) = %q, not well-formed: %v", markup, out, err)
  30. }
  31. return out
  32. }
  33. func TestContentSanitizes(t *testing.T) {
  34. tests := []struct {
  35. name, in, want string
  36. }{
  37. {"keeps basic markup",
  38. `<p>Hello <em>there</em> <strong>you</strong>.</p>`,
  39. `<p>Hello <em>there</em> <strong>you</strong>.</p>`},
  40. {"drops scripts and their content",
  41. `<p>a</p><script>alert(1)</script><p>b</p>`,
  42. `<p>a</p><p>b</p>`},
  43. {"drops styles and their content",
  44. `<style>p { color: red }</style><p>a</p>`,
  45. `<p>a</p>`},
  46. {"drops event handlers, classes, ids and inline styles",
  47. `<p class="x" id="y" style="color:red" onclick="evil()">a</p>`,
  48. `<p>a</p>`},
  49. {"drops forms but keeps their text",
  50. `<form action="/x"><p>Sign up</p><input name="email"><button>Go</button></form>`,
  51. `<p>Sign up</p>Go`},
  52. {"drops iframes and objects",
  53. `<p>a</p><iframe src="https://ads.example.com"></iframe><object data="x.swf"></object>`,
  54. `<p>a</p>`},
  55. {"drops javascript links but keeps the text",
  56. `<p><a href="javascript:alert(1)">click</a></p>`,
  57. `<p>click</p>`},
  58. {"makes relative links absolute",
  59. `<p><a href="/about">about</a> <a href="other">other</a> <a href="#notes">notes</a></p>`,
  60. `<p><a href="https://example.com/about">about</a> <a href="https://example.com/posts/other">other</a> <a href="https://example.com/posts/article#notes">notes</a></p>`},
  61. {"keeps mailto links",
  62. `<p><a href="mailto:a@example.com">mail</a></p>`,
  63. `<p><a href="mailto:a@example.com">mail</a></p>`},
  64. {"keeps valid table spans and drops invalid ones",
  65. `<table><tr><td colspan="2" rowspan="0">a</td></tr></table>`,
  66. `<table><tbody><tr><td colspan="2">a</td></tr></tbody></table>`},
  67. {"keeps valid lang and dir, drops invalid",
  68. `<p lang="fr" dir="rtl">a</p><p lang="not valid!" dir="sideways">b</p>`,
  69. `<p lang="fr" dir="rtl">a</p><p>b</p>`},
  70. {"keeps images in paragraphs and makes their src absolute",
  71. `<p>a</p><p><img src="/x.png" alt="x" class="big" onerror="evil()"></p>`,
  72. `<p>a</p><p><img src="https://example.com/x.png" alt="x"/></p>`},
  73. {"drops images with unsafe sources",
  74. `<p>a <img src="javascript:evil()" alt="x"></p>`,
  75. `<p>a </p>`},
  76. {"removes empty paragraphs",
  77. `<p>a</p><p> </p><p><br></p><p>b</p>`,
  78. `<p>a</p><p>b</p>`},
  79. {"repairs mailto and tel links mangled by go-trafilatura",
  80. `<p><a href="https://example.com/posts/mailto:a@example.com">mail</a> <a href="https://example.com/posts/article/tel:+15551234">call</a></p>`,
  81. `<p><a href="mailto:a@example.com">mail</a> <a href="tel:+15551234">call</a></p>`},
  82. {"restores code blocks from go-trafilatura's block code",
  83. "<code><code>a = 1\nb = 2\n</code></code><p>after</p>",
  84. "<pre><code>a = 1\nb = 2\n</code></pre><p>after</p>"},
  85. {"restores multi-line code inside a list item",
  86. "<ul><li><code>x\ny</code></li></ul>",
  87. "<ul><li><pre><code>x\ny</code></pre></li></ul>"},
  88. {"leaves inline code in a paragraph alone, even with a line break",
  89. "<p>Run <code>make\nbuild</code> now.</p>",
  90. "<p>Run <code>make\nbuild</code> now.</p>"},
  91. {"keeps code blocks",
  92. `<pre><code>if a &lt; b {
  93. return
  94. }</code></pre>`,
  95. "<pre><code>if a &lt; b {\n\treturn\n}</code></pre>"},
  96. }
  97. for _, tt := range tests {
  98. t.Run(tt.name, func(t *testing.T) {
  99. if got := cleanString(t, tt.in); got != tt.want {
  100. t.Errorf("got %q\nwant %q", got, tt.want)
  101. }
  102. })
  103. }
  104. }
  105. func TestContentRepairsNesting(t *testing.T) {
  106. // Each of these is invalid HTML that must still come out well-formed.
  107. for _, in := range []string{
  108. `<p>a<div>b</div>c</p>`,
  109. `<ul><p>loose</p><li>item</li></ul>`,
  110. `<a href="/a">outer <a href="/b">inner</a></a>`,
  111. `<table><td>no row</td></table>`,
  112. `<p>unclosed <em>emphasis<p>next`,
  113. `<li>orphan item</li>`,
  114. } {
  115. cleanString(t, in) // fails the test if not well-formed
  116. }
  117. }
  118. func TestContentEntities(t *testing.T) {
  119. // &nbsp; and other HTML entities aren't defined in XML; they must come out
  120. // as characters.
  121. got := cleanString(t, `<p>a&nbsp;b &copy; &mdash; &lt;tag&gt; &amp;</p>`)
  122. want := "<p>a b © — &lt;tag&gt; &amp;</p>"
  123. if got != want {
  124. t.Errorf("got %q, want %q", got, want)
  125. }
  126. }
  127. func TestShiftHeadings(t *testing.T) {
  128. tests := []struct{ in, want string }{
  129. {`<h1>a</h1><h2>b</h2><h3>c</h3>`, `<h2>a</h2><h3>b</h3><h4>c</h4>`},
  130. {`<h2>a</h2><h3>b</h3>`, `<h2>a</h2><h3>b</h3>`},
  131. {`<h3>a</h3><h4>b</h4>`, `<h3>a</h3><h4>b</h4>`},
  132. {`<h1>a</h1><h5>b</h5><h6>c</h6>`, `<h2>a</h2><h6>b</h6><h6>c</h6>`},
  133. {`<p>no headings</p>`, `<p>no headings</p>`},
  134. }
  135. for _, tt := range tests {
  136. if got := cleanString(t, tt.in); got != tt.want {
  137. t.Errorf("%s: got %q, want %q", tt.in, got, tt.want)
  138. }
  139. }
  140. }
  141. func TestWellFormed(t *testing.T) {
  142. good := []string{`<div><p>a</p><br/></div>`, `<p>&amp; &lt; &gt; &quot; &apos; &#160;</p>`}
  143. bad := []string{`<p>unclosed`, `<p>a</div>`, `<p>&nbsp;</p>`, `<br>`}
  144. for _, s := range good {
  145. if err := WellFormed(s); err != nil {
  146. t.Errorf("WellFormed(%q) = %v, want nil", s, err)
  147. }
  148. }
  149. for _, s := range bad {
  150. if err := WellFormed(s); err == nil {
  151. t.Errorf("WellFormed(%q) = nil, want an error", s)
  152. }
  153. }
  154. }