htmlCleanup.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. /* eslint-disable no-irregular-whitespace */
  3. import { describe, it, assert } from 'vitest'
  4. import { htmlCleanup } from '../htmlCleanup.ts'
  5. // htmlCleanup
  6. describe('htmlCleanup utility', () => {
  7. it('removes comments', () => {
  8. const source = '<div><!--test comment--><a href="test">test</a></div>'
  9. const should = '<div><a href="test">test</a></div>'
  10. const result = htmlCleanup(source)
  11. assert.equal(result, should, source)
  12. })
  13. it('keeps text as is', () => {
  14. const source = 'some link to somewhere'
  15. const should = 'some link to somewhere'
  16. const result = htmlCleanup(source)
  17. assert.equal(result, should, source)
  18. })
  19. it('keeps lists as is', () => {
  20. const source = '<li>a</li><li>b</li>'
  21. const should = '<li>a</li><li>b</li>'
  22. const result = htmlCleanup(source)
  23. assert.equal(result, should, source)
  24. })
  25. it('keeps link', () => {
  26. const source = '<p><a href="some_link">some link to somewhere</a></p>'
  27. const should = '<p><a href="some_link">some link to somewhere</a></p>'
  28. const result = htmlCleanup(source)
  29. assert.equal(result, should, source)
  30. })
  31. // This is done on backend
  32. // source = '<div><p id="123" data-id="abc">some link to somewhere</p></div>'
  33. // should = '<p>some link to somewhere</p>'
  34. // result = htmlCleanup(source)
  35. // assert.equal(result, should, source)
  36. it('removes "small" tag', () => {
  37. const source = '<small>some link to somewhere</small>'
  38. const should = 'some link to somewhere'
  39. const result = htmlCleanup(source)
  40. assert.equal(result, should, source)
  41. })
  42. it('removes "time" tag', () => {
  43. const source = '<div><time>some link to somewhere</time></a>'
  44. const should = '<div>some link to somewhere</div>'
  45. const result = htmlCleanup(source)
  46. assert.equal(result, should, source)
  47. })
  48. it('removes wrapper for several children', () => {
  49. const source = '<h1>some h1 for somewhere</h1><p><hr></p>'
  50. const should = '<h1>some h1 for somewhere</h1><p></p><hr><p></p>'
  51. const result = htmlCleanup(source)
  52. assert.equal(result, should, source)
  53. })
  54. it('removes wrapper for "br"', () => {
  55. const source = '<div><br></div>'
  56. const should = '<p></p>'
  57. const result = htmlCleanup(source)
  58. assert.equal(result, should, source)
  59. })
  60. it('keeps inner div', () => {
  61. const source = '<div class="xxx"><br></div>'
  62. const should = '<p class="xxx"></p>'
  63. const result = htmlCleanup(source)
  64. assert.equal(result, should, source)
  65. })
  66. it('removes form', () => {
  67. const source = '<form class="xxx">test 123</form>'
  68. const should = 'test 123'
  69. const result = htmlCleanup(source)
  70. assert.equal(result, should, source)
  71. })
  72. it('removes subform', () => {
  73. const source = '<form class="xxx">test 123</form> some other value'
  74. const should = 'test 123 some other value'
  75. const result = htmlCleanup(source)
  76. assert.equal(result, should, source)
  77. })
  78. it('removes form tag and input', () => {
  79. const source =
  80. '<form class="xxx">test 123</form> some other value<input value="should not be shown">'
  81. const should = 'test 123 some other value'
  82. const result = htmlCleanup(source)
  83. assert.equal(result, should, source)
  84. })
  85. it('removes svg', () => {
  86. const source =
  87. '<font size="3" color="red">This is some text!</font><svg><use xlink:href="assets/images/icons.svg#icon-status"></svg>'
  88. const should = '<font size="3" color="red">This is some text!</font>'
  89. const result = htmlCleanup(source)
  90. assert.equal(result, should, source)
  91. })
  92. it('removes "w" an "o" tags', () => {
  93. const source =
  94. '<p>some link to somewhere from word<w:sdt>abc</w:sdt></p><o:p></o:p></a>'
  95. // should = "<div><p>some link to somewhere from wordabc</p></div>"
  96. const should = '<p>some link to somewhere from wordabc</p>'
  97. const result = htmlCleanup(source)
  98. assert.equal(result, should, source)
  99. })
  100. it('clears external tags', () => {
  101. const source =
  102. '<div><div><label for="Ticket_888344_group_id">Gruppe <span>*</span></label></div><div><div></div></div><div><div><span></span><span></span></div></div><div><div><label for="Ticket_888344_owner_id">Besitzer <span></span></label></div><div><div></div></div></div><div><div><div><svg><use xlink:href="http://localhost:3000/assets/images/icons.svg#icon-arrow-down"></use></svg></div><span></span><span></span></div></div><div><div> <label for="Ticket_888344_state_id">Status <span>*</span></label></div></div></div>\n'
  103. const should =
  104. '<div><div>Gruppe <span>*</span></div><div><div></div></div><div><div><span></span><span></span></div></div><div><div>Besitzer <span></span></div><div><div></div></div></div><div><div><div></div><span></span><span></span></div></div><div><div> Status <span>*</span></div></div></div>'
  105. const result = htmlCleanup(source)
  106. assert.equal(result, should, source)
  107. })
  108. it('clears html head', () => {
  109. const source =
  110. '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n<html>\n<head>\n <meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n <title></title>\n <meta name="generator" content="LibreOffice 4.4.7.2 (MacOSX)"/>\n <style type="text/css">\n @page { margin: 0.79in }\n p { margin-bottom: 0.1in; line-height: 120% }\n a:link { so-language: zxx }\n </style>\n</head>\n<body lang="en-US" dir="ltr">\n<p align="center" style="margin-bottom: 0in; line-height: 100%">1.\nGehe a<b>uf </b><b>https://www.pfe</b>rdiathek.ge</p>\n<p align="center" style="margin-bottom: 0in; line-height: 100%"><br/>\n\n</p>\n<p align="center" style="margin-bottom: 0in; line-height: 100%">2.\nMel<font color="#800000">de Dich mit folgende</font> Zugangsdaten an:</p>\n<p align="center" style="margin-bottom: 0in; line-height: 100%">Benutzer:\nme@xxx.net</p>\n<p align="center" style="margin-bottom: 0in; line-height: 100%">Passwort:\nxxx.</p>\n</body>\n</html>'
  111. const should =
  112. '<p align="center" style="margin-bottom: 0in; line-height: 100%">1.\nGehe a<b>uf </b><b>https://www.pfe</b>rdiathek.ge</p><p align="center" style="margin-bottom: 0in; line-height: 100%"></p><p align="center" style="margin-bottom: 0in; line-height: 100%">2.\nMel<font color="#800000">de Dich mit folgende</font> Zugangsdaten an:</p><p align="center" style="margin-bottom: 0in; line-height: 100%">Benutzer:\nme@xxx.net</p><p align="center" style="margin-bottom: 0in; line-height: 100%">Passwort:\nxxx.</p>\n'
  113. const result = htmlCleanup(source)
  114. assert.equal(result, should, source)
  115. })
  116. it('clears table', () => {
  117. const source =
  118. '<table bgcolor="green" aaa="1"><thead><tr><th colspan="2" abc="a">aaa</th></tr></thead><tbody><tr><td>value</td></tr></tbody></table>'
  119. const should =
  120. '<table bgcolor="green" aaa="1"><thead><tr><th colspan="2" abc="a">aaa</th></tr></thead><tbody><tr><td>value</td></tr></tbody></table>'
  121. const result = htmlCleanup(source)
  122. assert.equal(result, should, source)
  123. })
  124. it('clears lists and new lines', () => {
  125. const source = `<div>Wir führen eine (Produktiv-) Freshdesk Migratione für hosted Kunden kostenfrei durch.</div><div><br></div><div>
  126. <h3>Ablauf der Migration:</h3>
  127. <div><ul>
  128. <li>Abstimmung zum Projektablauf und Zeitplan</li>
  129. <li>Produktivmigration zum gewünschten Zeitpunkt. Dabei werden folgende Attribute übertragen:<br><ul>
  130. <li>Companys</li>
  131. <li>User (Agenten+Kontakte)</li>
  132. <li>Gruppen</li>
  133. <li>Tickets (inkl. aller Artikel und Anhänge)</li>
  134. <li>Individuelle Felder (User, Ticket, Company)</li>
  135. <li>Time-Accounting der Tickets (sofern im Freshdesk-Plan enthalten)</li>
  136. </ul>
  137. </li>
  138. <li>Nach erfolgreicher Migration ist direkt die Anmeldung durch den hinterlegten User möglich<br>
  139. </li>
  140. <li>Passwörter von anderen Usern können nicht mit übergeben werden, daher müssen sich weitere User über Standard Authentifizierung, wie z.B. der Passwort-zurücksetzen Funktion am System anmelden</li>
  141. <li>Zum gewünschten Zeitpunkt werden die Postfächer aktiviert (durch uns)</li>
  142. <li>Manuelle Zuweisung des Postfaches (durch den Kunden)</li>
  143. </ul></div>
  144. </div><h3>Weiteres Vorgehen:</h3><div>Während der Migration kommt es zu einer Downtime, in der keine Tickets erstellt werden können. Die Downtime kann vorab abgeschätzt werden. Dafür brauchen wir folgende Informationen:<br><ul>
  145. <li>Name des bisherigen Freshdesk Plans (davon ist die Anzahl der Tickets abhängig, die über die API abgefragt werden können)</li>
  146. <li>Ticketanzahl gesamt</li>
  147. </ul>
  148. <div>Außerdem benötigen wir:</div>
  149. </div><div><ul>
  150. <li>Email-Adresse und den API-Token eines Benutzers, der Zugriff auf alle relevanten Tickets hat</li>
  151. <li>Name der Zammad hosted Instanz</li>
  152. <li>gewünschter Zeitpunkt der Produktivmigration</li>
  153. <li>gewünschter Zeitpunkt für die Aktivierung des Zammad-Postfaches</li>
  154. </ul></div><div>Sobald wir alle Informationen haben, werden wir Ihnen die Downtime zukommen lassen und danach mit dem Kunden alle weiteren Termine abstimmen.<br>
  155. </div><div><br></div><h3>zusätzliche Testmigration?</h3><div>Möchte der Kunde auf Nummer Sicher gehen und eine Testmigration durchführen, damit er genügend Zeit hat sich mit dem Zammad System und den dazugehörigen Einstellungen vertraut machen? Das ist kein Problem! Über den kostenfreien Migrationsservice in Form der oben beschriebenen Produktiv-Migration hinaus, bieten wir eine zusätzliche Testmigration für 1.450€ an. Dieses Migrationspaket beinhaltet eine zusätzliche Testmigration sowie eventuelle Anpassungswünsche (vgl. Checkliste OTRS Migration). Je nachdem, ob sich weitere Anpassungswünsche aus der Checkliste ergeben, können die Kosten steigen.</div>`
  156. const should =
  157. '<div>Wir führen eine (Produktiv-) Freshdesk Migratione für hosted Kunden&nbsp;kostenfrei&nbsp;durch.</div><p></p><div><h3>Ablauf der Migration:</h3><div><ul><li>Abstimmung zum Projektablauf und Zeitplan</li><li>Produktivmigration zum gewünschten Zeitpunkt. Dabei werden folgende Attribute übertragen:<ul><li>Companys</li><li>User (Agenten+Kontakte)</li><li>Gruppen</li><li>Tickets (inkl. aller Artikel und Anhänge)</li><li>Individuelle Felder (User, Ticket, Company)</li><li>Time-Accounting der Tickets (sofern im Freshdesk-Plan enthalten)</li></ul></li><li>Nach erfolgreicher Migration ist direkt die Anmeldung durch den hinterlegten User möglich</li><li>Passwörter von anderen Usern können nicht mit übergeben werden, daher müssen sich weitere User über Standard Authentifizierung, wie z.B. der Passwort-zurücksetzen Funktion am System anmelden</li><li>Zum gewünschten Zeitpunkt werden die Postfächer aktiviert (durch uns)</li><li>Manuelle Zuweisung des Postfaches (durch den Kunden)</li></ul></div></div><h3>Weiteres Vorgehen:</h3><div>Während der Migration kommt es zu einer Downtime, in der keine Tickets erstellt werden können. Die Downtime kann vorab abgeschätzt werden. Dafür brauchen wir folgende Informationen:<ul><li>Name des bisherigen Freshdesk Plans (davon ist die Anzahl der Tickets abhängig, die über die API abgefragt werden können)</li><li>Ticketanzahl gesamt</li></ul><div>Außerdem benötigen wir:</div></div><div><ul><li>Email-Adresse und den API-Token eines Benutzers, der Zugriff auf alle relevanten Tickets hat</li><li>Name der Zammad hosted Instanz</li><li>gewünschter Zeitpunkt der Produktivmigration</li><li>gewünschter Zeitpunkt für die Aktivierung des Zammad-Postfaches</li></ul></div><div>Sobald wir alle Informationen haben, werden wir Ihnen die Downtime zukommen lassen und danach mit dem Kunden alle weiteren Termine abstimmen.</div><p></p><h3>zusätzliche Testmigration?</h3><div>Möchte der Kunde auf Nummer Sicher gehen und eine Testmigration durchführen, damit er genügend Zeit hat sich mit dem Zammad System und den dazugehörigen Einstellungen vertraut machen? Das ist kein Problem! Über den kostenfreien Migrationsservice in Form der oben beschriebenen Produktiv-Migration hinaus, bieten wir eine zusätzliche Testmigration für 1.450€ an. Dieses&nbsp;Migrationspaket&nbsp;beinhaltet eine zusätzliche Testmigration sowie eventuelle Anpassungswünsche (vgl. Checkliste OTRS Migration). Je nachdem, ob sich weitere Anpassungswünsche aus der Checkliste ergeben, können die Kosten steigen.</div>'
  158. const result = htmlCleanup(source)
  159. assert.equal(result, should, source)
  160. })
  161. test("doesn't remove extra break lines", () => {
  162. const source = `<p>This is a note<br><br></p><blockquote type="cite">
  163. <p>On .+, #{article.created_by.fullname} wrote:</p>\n<p><br></p>
  164. <p>#{article.body}</p>\n
  165. </blockquote><p><br></p>`
  166. const should =
  167. '<p>This is a note<br></p><blockquote type="cite"><p>On .+, #{article.created_by.fullname} wrote:</p><p><br></p><p>#{article.body}</p></blockquote><p><br></p>'
  168. const result = htmlCleanup(source)
  169. assert.equal(result, should, source)
  170. })
  171. // strip out browser-inserted (broken) link (see https://github.com/zammad/zammad/issues/2019)
  172. // should not be possible in the new tech stack
  173. // source =
  174. // '<div><a href="https://example.com/#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}">test</a></div>'
  175. // should =
  176. // '<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}">test</a>'
  177. // result = htmlCleanup(source)
  178. // assert.equal(result, should, source)
  179. // this is done on the backend now
  180. // source =
  181. // '<table bgcolor="green" aaa="1" style="color: red"><thead><tr style="margin-top: 10px"><th colspan="2" abc="a" style="margin-top: 12px">aaa</th></tr></thead><tbody><tr><td>value</td></tr></tbody></table>'
  182. // should =
  183. // '<table bgcolor="green" style="color:red;"><thead><tr style="margin-top:10px;"><th colspan="2" style="margin-top:12px;">aaa</th></tr></thead><tbody><tr><td>value</td></tr></tbody></table>'
  184. // result = htmlCleanup(source)
  185. // result.get(0).outerHTML
  186. // // equal(result.get(0).outerHTML, should, source) / string order is different on browsers
  187. // assert.equal(result.first().attr('bgcolor'), 'green')
  188. // assert.equal(result.first().attr('style'), 'color:red;')
  189. // assert.equal(result.first().attr('aaa'), undefined)
  190. // assert.equal(result.find('tr').first().attr('style'), 'margin-top:10px;')
  191. // assert.equal(result.find('th').first().attr('colspan'), '2')
  192. // assert.equal(result.find('th').first().attr('abc'), undefined)
  193. // assert.equal(result.find('th').first().attr('style'), 'margin-top:12px;')
  194. // source =
  195. // '<table bgcolor="green" aaa="1" style="color:red; display: none;"><thead><tr><th colspan="2" abc="a">aaa</th></tr></thead><tbody><tr><td>value</td></tr></tbody></table>'
  196. // should =
  197. // '<table bgcolor="green" style="color:red;"><thead><tr><th colspan="2">aaa</th></tr></thead><tbody><tr><td>value</td></tr></tbody></table>'
  198. // result = htmlCleanup(source)
  199. // // equal(result.get(0).outerHTML, should, source) / string order is different on browsers
  200. // assert.equal(result.first().attr('bgcolor'), 'green')
  201. // assert.equal(result.first().attr('style'), 'color:red;')
  202. // assert.equal(result.first().attr('aaa'), undefined)
  203. // assert.equal(result.find('tr').first().attr('style'), undefined)
  204. // assert.equal(result.find('th').first().attr('colspan'), '2')
  205. // assert.equal(result.find('th').first().attr('abc'), undefined)
  206. // assert.equal(result.find('th').first().attr('style'), undefined)
  207. // https://github.com/zammad/zammad/issues/4445
  208. // source =
  209. // '<meta charset=\'utf-8\'><span style="color: rgb(219, 219, 220);">This is a black font colour with white background</span>'
  210. // should = '<span>This is a black font colour with white background</span>'
  211. // result = htmlCleanup(source)
  212. // assert.equal(result, should, source)
  213. // source =
  214. // '<meta charset=\'utf-8\'><div class="article-content" style="box-sizing: border-box; color: rgb(219, 219, 220); position: relative; z-index: 1; padding: 0px 55px;"><div class="bubble-gap" style="box-sizing: border-box;"><div class="internal-border" style="box-sizing: border-box; padding: 5px; border-radius: 5px; margin: -5px;"><div class="textBubble" style="box-sizing: border-box; padding: 10px 20px; background: var(--background-article-customer); border-radius: 2px; border-color: var(--border-article-customer); box-shadow: none; position: relative;"><div class="textBubble-content" id="article-content-4" data-id="4" style="box-sizing: border-box; overflow: hidden; position: relative;"><div class="richtext-content" style="box-sizing: border-box;"><div style="box-sizing: border-box;">This is a black font colour with white background</div></div></div></div></div></div></div>'
  215. // should =
  216. // '<div><div><div><div><div id="article-content-4"><div><div>This is a black font colour with white background</div></div></div></div></div></div></div>'
  217. // result = htmlCleanup(source)
  218. // assert.equal(result, should, source)
  219. })