attachments.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
  2. const storeName = 'attachments';
  3. const defaultStoreOptions = {
  4. beforeWrite: fileObj => {
  5. if (!fileObj.isImage()) {
  6. return {
  7. type: 'application/octet-stream',
  8. };
  9. }
  10. return {};
  11. },
  12. };
  13. const Store = localFSStore
  14. ? new FS.Store.FileSystem(storeName, {
  15. path: localFSStore,
  16. ...defaultStoreOptions,
  17. })
  18. : new FS.Store.GridFS(storeName, {
  19. // XXX Add a new store for cover thumbnails so we don't load big images in
  20. // the general board view
  21. // If the uploaded document is not an image we need to enforce browser
  22. // download instead of execution. This is particularly important for HTML
  23. // files that the browser will just execute if we don't serve them with the
  24. // appropriate `application/octet-stream` MIME header which can lead to user
  25. // data leaks. I imagine other formats (like PDF) can also be attack vectors.
  26. // See https://github.com/wekan/wekan/issues/99
  27. // XXX Should we use `beforeWrite` option of CollectionFS instead of
  28. // collection-hooks?
  29. // We should use `beforeWrite`.
  30. ...defaultStoreOptions,
  31. });
  32. Attachments = new FS.Collection('attachments', {
  33. stores: [Store],
  34. });
  35. if (Meteor.isServer) {
  36. Meteor.startup(() => {
  37. Attachments.files._ensureIndex({ cardId: 1 });
  38. });
  39. Attachments.allow({
  40. insert(userId, doc) {
  41. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  42. },
  43. update(userId, doc) {
  44. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  45. },
  46. remove(userId, doc) {
  47. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  48. },
  49. // We authorize the attachment download either:
  50. // - if the board is public, everyone (even unconnected) can download it
  51. // - if the board is private, only board members can download it
  52. download(userId, doc) {
  53. const board = Boards.findOne(doc.boardId);
  54. if (board.isPublic()) {
  55. return true;
  56. } else {
  57. return board.hasMember(userId);
  58. }
  59. },
  60. fetch: ['boardId'],
  61. });
  62. }
  63. // XXX Enforce a schema for the Attachments CollectionFS
  64. if (Meteor.isServer) {
  65. Attachments.files.after.insert((userId, doc) => {
  66. // If the attachment doesn't have a source field
  67. // or its source is different than import
  68. if (!doc.source || doc.source !== 'import') {
  69. // Add activity about adding the attachment
  70. Activities.insert({
  71. userId,
  72. type: 'card',
  73. activityType: 'addAttachment',
  74. attachmentId: doc._id,
  75. boardId: doc.boardId,
  76. cardId: doc.cardId,
  77. listId: doc.listId,
  78. swimlaneId: doc.swimlaneId,
  79. });
  80. } else {
  81. // Don't add activity about adding the attachment as the activity
  82. // be imported and delete source field
  83. Attachments.update(
  84. {
  85. _id: doc._id,
  86. },
  87. {
  88. $unset: {
  89. source: '',
  90. },
  91. },
  92. );
  93. }
  94. });
  95. Attachments.files.before.remove((userId, doc) => {
  96. Activities.insert({
  97. userId,
  98. type: 'card',
  99. activityType: 'deleteAttachment',
  100. attachmentId: doc._id,
  101. boardId: doc.boardId,
  102. cardId: doc.cardId,
  103. listId: doc.listId,
  104. swimlaneId: doc.swimlaneId,
  105. });
  106. });
  107. Attachments.files.after.remove((userId, doc) => {
  108. Activities.remove({
  109. attachmentId: doc._id,
  110. });
  111. });
  112. }
  113. export default Attachments;