attachments.js 3.2 KB

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