attachments.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import { ObjectID } from 'bson';
  2. import DOMPurify from 'dompurify';
  3. const filesize = require('filesize');
  4. const prettyMilliseconds = require('pretty-ms');
  5. Template.attachmentsGalery.events({
  6. 'click .js-add-attachment': Popup.open('cardAttachments'),
  7. // If we let this event bubble, FlowRouter will handle it and empty the page
  8. // content, see #101.
  9. 'click .js-download'(event) {
  10. event.stopPropagation();
  11. },
  12. 'click .js-open-attachment-menu': Popup.open('attachmentActions'),
  13. });
  14. Template.attachmentsGalery.helpers({
  15. isBoardAdmin() {
  16. return Meteor.user().isBoardAdmin();
  17. },
  18. fileSize(size) {
  19. const ret = filesize(size);
  20. return ret;
  21. },
  22. sanitize(value) {
  23. return DOMPurify.sanitize(value);
  24. },
  25. });
  26. Template.cardAttachmentsPopup.onCreated(function() {
  27. this.uploads = new ReactiveVar([]);
  28. });
  29. Template.cardAttachmentsPopup.helpers({
  30. getEstimateTime(upload) {
  31. const ret = prettyMilliseconds(upload.estimateTime.get());
  32. return ret;
  33. },
  34. getEstimateSpeed(upload) {
  35. const ret = filesize(upload.estimateSpeed.get(), {round: 0}) + "/s";
  36. return ret;
  37. },
  38. uploads() {
  39. return Template.instance().uploads.get();
  40. }
  41. });
  42. Template.cardAttachmentsPopup.events({
  43. 'change .js-attach-file'(event, templateInstance) {
  44. const card = this;
  45. const files = event.currentTarget.files;
  46. if (files) {
  47. let uploads = [];
  48. for (const file of files) {
  49. const fileId = new ObjectID().toString();
  50. // If filename is not same as sanitized filename, has XSS, then cancel upload
  51. if (file.name !== DOMPurify.sanitize(file.name)) {
  52. return false;
  53. }
  54. const config = {
  55. file: file,
  56. fileId: fileId,
  57. meta: Utils.getCommonAttachmentMetaFrom(card),
  58. chunkSize: 'dynamic',
  59. };
  60. config.meta.fileId = fileId;
  61. const uploader = Attachments.insert(
  62. config,
  63. false,
  64. );
  65. uploader.on('start', function() {
  66. uploads.push(this);
  67. templateInstance.uploads.set(uploads);
  68. });
  69. uploader.on('uploaded', (error, fileRef) => {
  70. if (!error) {
  71. if (fileRef.isImage) {
  72. card.setCover(fileRef._id);
  73. }
  74. }
  75. });
  76. uploader.on('end', (error, fileRef) => {
  77. uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
  78. templateInstance.uploads.set(uploads);
  79. if (uploads.length == 0 ) {
  80. Popup.back();
  81. }
  82. });
  83. uploader.start();
  84. }
  85. }
  86. },
  87. 'click .js-computer-upload'(event, templateInstance) {
  88. templateInstance.find('.js-attach-file').click();
  89. event.preventDefault();
  90. },
  91. 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
  92. });
  93. const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
  94. const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
  95. let pastedResults = null;
  96. Template.previewClipboardImagePopup.onRendered(() => {
  97. // we can paste image from clipboard
  98. const handle = results => {
  99. if (results.dataURL.startsWith('data:image/')) {
  100. const direct = results => {
  101. $('img.preview-clipboard-image').attr('src', results.dataURL);
  102. pastedResults = results;
  103. };
  104. if (MAX_IMAGE_PIXEL) {
  105. // if has size limitation on image we shrink it before uploading
  106. Utils.shrinkImage({
  107. dataurl: results.dataURL,
  108. maxSize: MAX_IMAGE_PIXEL,
  109. ratio: COMPRESS_RATIO,
  110. callback(changed) {
  111. if (changed !== false && !!changed) {
  112. results.dataURL = changed;
  113. }
  114. direct(results);
  115. },
  116. });
  117. } else {
  118. direct(results);
  119. }
  120. }
  121. };
  122. $(document.body).pasteImageReader(handle);
  123. // we can also drag & drop image file to it
  124. $(document.body).dropImageReader(handle);
  125. });
  126. Template.previewClipboardImagePopup.events({
  127. 'click .js-upload-pasted-image'() {
  128. const card = this;
  129. if (pastedResults && pastedResults.file) {
  130. const file = pastedResults.file;
  131. window.oPasted = pastedResults;
  132. const fileId = new ObjectID().toString();
  133. const config = {
  134. file,
  135. fileId: fileId,
  136. meta: Utils.getCommonAttachmentMetaFrom(card),
  137. fileName: file.name || file.type.replace('image/', 'clipboard.'),
  138. chunkSize: 'dynamic',
  139. };
  140. config.meta.fileId = fileId;
  141. const uploader = Attachments.insert(
  142. config,
  143. false,
  144. );
  145. uploader.on('uploaded', (error, fileRef) => {
  146. if (!error) {
  147. if (fileRef.isImage) {
  148. card.setCover(fileRef._id);
  149. }
  150. }
  151. });
  152. uploader.on('end', (error, fileRef) => {
  153. pastedResults = null;
  154. $(document.body).pasteImageReader(() => {});
  155. Popup.back();
  156. });
  157. uploader.start();
  158. }
  159. },
  160. });
  161. BlazeComponent.extendComponent({
  162. isCover() {
  163. const ret = Cards.findOne(this.data().meta.cardId).coverId == this.data()._id;
  164. return ret;
  165. },
  166. events() {
  167. return [
  168. {
  169. 'click .js-rename': Popup.open('attachmentRename'),
  170. 'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', function() {
  171. Attachments.remove(this._id);
  172. Popup.back(2);
  173. }),
  174. 'click .js-add-cover'() {
  175. Cards.findOne(this.data().meta.cardId).setCover(this.data()._id);
  176. Popup.back();
  177. },
  178. 'click .js-remove-cover'() {
  179. Cards.findOne(this.data().meta.cardId).unsetCover();
  180. Popup.back();
  181. },
  182. 'click .js-move-storage-fs'() {
  183. Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
  184. Popup.back();
  185. },
  186. 'click .js-move-storage-gridfs'() {
  187. Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
  188. Popup.back();
  189. },
  190. 'click .js-move-storage-s3'() {
  191. Meteor.call('moveAttachmentToStorage', this.data()._id, "s3");
  192. Popup.back();
  193. },
  194. }
  195. ]
  196. }
  197. }).register('attachmentActionsPopup');
  198. BlazeComponent.extendComponent({
  199. getNameWithoutExtension() {
  200. const ret = this.data().name.replace(new RegExp("\." + this.data().extension + "$"), "");
  201. return ret;
  202. },
  203. events() {
  204. return [
  205. {
  206. 'keydown input.js-edit-attachment-name'(evt) {
  207. // enter = save
  208. if (evt.keyCode === 13) {
  209. this.find('button[type=submit]').click();
  210. }
  211. },
  212. 'click button.js-submit-edit-attachment-name'(event) {
  213. // save button pressed
  214. event.preventDefault();
  215. const name = this.$('.js-edit-attachment-name')[0]
  216. .value
  217. .trim() + this.data().extensionWithDot;
  218. Meteor.call('renameAttachment', this.data()._id, name);
  219. Popup.back(2);
  220. },
  221. }
  222. ]
  223. }
  224. }).register('attachmentRenamePopup');