AttachmentViewer.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. 'use strict';
  2. const common = require('../common');
  3. describe('AttachmentViewer', function () {
  4. beforeEach(() => {
  5. mockCreateObjectUrl();
  6. });
  7. afterEach(() => {
  8. globalThis.cleanup()
  9. });
  10. describe('whole run (setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo)', function () {
  11. this.timeout(30000);
  12. jsc.property(
  13. 'displays & hides data as requested',
  14. common.jscMimeTypes(),
  15. 'string',
  16. 'string',
  17. 'string',
  18. 'string',
  19. // eslint-disable-next-line complexity
  20. function (mimeType, rawdata, filename, prefix, postfix) {
  21. let data = 'data:' + mimeType + ';base64,' + common.btoa(rawdata),
  22. mimePrefix = mimeType.substring(0, 6),
  23. previewSupported = (
  24. mimePrefix === 'image/' ||
  25. mimePrefix === 'audio/' ||
  26. mimePrefix === 'video/' ||
  27. mimeType.match(/\/pdf/i)
  28. ),
  29. results = [],
  30. result = '';
  31. // text node of attachment will truncate at null byte
  32. if (filename === '\u0000') {
  33. filename = '';
  34. }
  35. prefix = prefix.replace(/%(s|d)/g, '%%');
  36. postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<|>/g, '');
  37. document.body.innerHTML = (
  38. '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
  39. '<div id="attachment" class="hidden"></div>' +
  40. '<div id="templates">' +
  41. '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
  42. '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
  43. '<a class="alert-link">Download attachment</a>' +
  44. '</div>' +
  45. '</div>'
  46. );
  47. PrivateBin.AttachmentViewer.init();
  48. PrivateBin.Model.init();
  49. results.push(
  50. !PrivateBin.AttachmentViewer.hasAttachment() &&
  51. document.getElementById('attachment').classList.contains('hidden') &&
  52. document.getElementById('attachment').children.length === 0 &&
  53. document.getElementById('attachmenttemplate').classList.contains('hidden') &&
  54. document.getElementById('attachmentPreview').classList.contains('hidden')
  55. );
  56. global.atob = common.atob;
  57. if (filename.length) {
  58. PrivateBin.AttachmentViewer.setAttachment(data, filename);
  59. } else {
  60. PrivateBin.AttachmentViewer.setAttachment(data);
  61. }
  62. // // beyond this point we will get the blob URL instead of the data
  63. data = window.URL.createObjectURL(data);
  64. const attachment = PrivateBin.AttachmentViewer.getAttachments();
  65. results.push(
  66. PrivateBin.AttachmentViewer.hasAttachment() &&
  67. document.getElementById('attachment').classList.contains('hidden') &&
  68. document.getElementById('attachment').children.length > 0 &&
  69. document.getElementById('attachmentPreview').classList.contains('hidden') &&
  70. attachment[0][0] === data &&
  71. attachment[0][1] === filename
  72. );
  73. PrivateBin.AttachmentViewer.showAttachment();
  74. results.push(
  75. !document.getElementById('attachment').classList.contains('hidden') &&
  76. document.getElementById('attachment').children.length > 0 &&
  77. (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
  78. );
  79. PrivateBin.AttachmentViewer.hideAttachment();
  80. results.push(
  81. document.getElementById('attachment').classList.contains('hidden') &&
  82. (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
  83. );
  84. if (previewSupported) {
  85. PrivateBin.AttachmentViewer.hideAttachmentPreview();
  86. results.push(document.getElementById('attachmentPreview').classList.contains('hidden'));
  87. }
  88. PrivateBin.AttachmentViewer.showAttachment();
  89. results.push(
  90. !document.getElementById('attachment').classList.contains('hidden') &&
  91. (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
  92. );
  93. let element = document.createElement('div');
  94. PrivateBin.AttachmentViewer.moveAttachmentTo(element, attachment[0], prefix + '%s' + postfix);
  95. // messageIDs with links get a relaxed treatment
  96. if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
  97. const tempTA = document.createElement('textarea');
  98. tempTA.textContent = (prefix + filename + postfix);
  99. result = tempTA.textContent;
  100. } else {
  101. result = DOMPurify.sanitize(
  102. prefix + PrivateBin.Helper.htmlEntities(filename) + postfix, {
  103. ALLOWED_TAGS: ['a', 'i', 'span'],
  104. ALLOWED_ATTR: ['href', 'id']
  105. }
  106. );
  107. }
  108. if (filename.length) {
  109. results.push(
  110. element.querySelector('a').href === data &&
  111. element.querySelector('a').getAttribute('download') === filename &&
  112. element.querySelector('a').textContent === result
  113. );
  114. } else {
  115. results.push(element.querySelector('a').href === data);
  116. }
  117. PrivateBin.AttachmentViewer.removeAttachment();
  118. results.push(
  119. document.getElementById('attachment').classList.contains('hidden') &&
  120. document.getElementById('attachment').children.length === 0 &&
  121. document.getElementById('attachmentPreview').classList.contains('hidden')
  122. );
  123. return results.every(element => element);
  124. }
  125. );
  126. it(
  127. 'sanitizes file names in attachments',
  128. function() {
  129. document.body.innerHTML = (
  130. '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
  131. '<div id="attachment" class="hidden"></div>' +
  132. '<div id="templates">' +
  133. '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
  134. '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
  135. '<a class="alert-link">Download attachment</a>' +
  136. '</div>' +
  137. '</div>'
  138. );
  139. PrivateBin.AttachmentViewer.init();
  140. PrivateBin.Model.init();
  141. global.atob = common.atob;
  142. const maliciousFileNames = [
  143. '<script>alert("☹️");//<a',
  144. '"><meta http-equiv="refresh" content="0;url=http://example.com/">.txt'
  145. ];
  146. for (const filename of maliciousFileNames) {
  147. PrivateBin.AttachmentViewer.setAttachment('data:;base64,', filename);
  148. assert.ok(!document.body.innerHTML.includes(filename));
  149. }
  150. }
  151. );
  152. });
  153. describe('showAttachment()', function () {
  154. it('displays attachment even when attachmentPreview element is missing',
  155. function() {
  156. document.body.innerHTML = (
  157. '<div id="attachment" class="hidden"></div>' +
  158. '<div id="templates">' +
  159. '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
  160. '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
  161. '<a class="alert-link">Download attachment</a>' +
  162. '</div>' +
  163. '</div>'
  164. );
  165. // Note: attachmentPreview element is intentionally NOT created
  166. PrivateBin.AttachmentViewer.init();
  167. PrivateBin.Model.init();
  168. global.atob = common.atob;
  169. // Set attachment without preview element
  170. PrivateBin.AttachmentViewer.setAttachment('data:text/plain;base64,', 'test.txt');
  171. // Show attachment should work even without attachmentPreview
  172. PrivateBin.AttachmentViewer.showAttachment();
  173. const attachment = document.getElementById('attachment');
  174. assert.ok(!attachment.classList.contains('hidden'), 'Attachment should be visible');
  175. assert.ok(attachment.children.length > 0, 'Attachment should have content');
  176. }
  177. )
  178. });
  179. function mockCreateObjectUrl() {
  180. if (typeof window.URL.createObjectURL === 'undefined') {
  181. Object.defineProperty(
  182. window.URL,
  183. 'createObjectURL',
  184. {
  185. value: function (_blob) {
  186. return 'blob:' + location.origin + '/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed';
  187. }
  188. }
  189. );
  190. }
  191. }
  192. });