export.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /* global JsonRoutes */
  2. if (Meteor.isServer) {
  3. // todo XXX once we have a real API in place, move that route there
  4. // todo XXX also share the route definition between the client and the server
  5. // so that we could use something like
  6. // `ApiRoutes.path('boards/export', boardId)``
  7. // on the client instead of copy/pasting the route path manually between the
  8. // client and the server.
  9. /**
  10. * @operation export
  11. * @tag Boards
  12. *
  13. * @summary This route is used to export the board **FROM THE APPLICATION**.
  14. *
  15. * @description If user is already logged-in, pass loginToken as param
  16. * "authToken": '/api/boards/:boardId/export?authToken=:token'
  17. *
  18. * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
  19. * for detailed explanations
  20. *
  21. * @param {string} boardId the ID of the board we are exporting
  22. * @param {string} authToken the loginToken
  23. */
  24. JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) {
  25. const boardId = req.params.boardId;
  26. let user = null;
  27. // todo XXX for real API, first look for token in Authentication: header
  28. // then fallback to parameter
  29. const loginToken = req.query.authToken;
  30. if (loginToken) {
  31. const hashToken = Accounts._hashLoginToken(loginToken);
  32. user = Meteor.users.findOne({
  33. 'services.resume.loginTokens.hashedToken': hashToken,
  34. });
  35. }
  36. const exporter = new Exporter(boardId);
  37. if (exporter.canExport(user)) {
  38. JsonRoutes.sendResult(res, {
  39. code: 200,
  40. data: exporter.build(),
  41. });
  42. } else {
  43. // we could send an explicit error message, but on the other hand the only
  44. // way to get there is by hacking the UI so let's keep it raw.
  45. JsonRoutes.sendResult(res, 403);
  46. }
  47. });
  48. }
  49. class Exporter {
  50. constructor(boardId) {
  51. this._boardId = boardId;
  52. }
  53. build() {
  54. const byBoard = { boardId: this._boardId };
  55. const byBoardNoLinked = { boardId: this._boardId, linkedId: {$in: ['', null] } };
  56. // we do not want to retrieve boardId in related elements
  57. const noBoardId = {
  58. fields: {
  59. boardId: 0,
  60. },
  61. };
  62. const result = {
  63. _format: 'wekan-board-1.0.0',
  64. };
  65. _.extend(result, Boards.findOne(this._boardId, {
  66. fields: {
  67. stars: 0,
  68. },
  69. }));
  70. result.lists = Lists.find(byBoard, noBoardId).fetch();
  71. result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
  72. result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
  73. result.customFields = CustomFields.find(byBoard, noBoardId).fetch();
  74. result.comments = CardComments.find(byBoard, noBoardId).fetch();
  75. result.activities = Activities.find(byBoard, noBoardId).fetch();
  76. result.rules = Rules.find(byBoard, noBoardId).fetch();
  77. result.checklists = [];
  78. result.checklistItems = [];
  79. result.subtaskItems = [];
  80. result.triggers = [];
  81. result.actions = [];
  82. result.cards.forEach((card) => {
  83. result.checklists.push(...Checklists.find({
  84. cardId: card._id,
  85. }).fetch());
  86. result.checklistItems.push(...ChecklistItems.find({
  87. cardId: card._id,
  88. }).fetch());
  89. result.subtaskItems.push(...Cards.find({
  90. parentid: card._id,
  91. }).fetch());
  92. });
  93. result.rules.forEach((rule) => {
  94. result.triggers.push(...Triggers.find({
  95. _id: rule.triggerId,
  96. }, noBoardId).fetch());
  97. result.actions.push(...Actions.find({
  98. _id: rule.actionId,
  99. }, noBoardId).fetch());
  100. });
  101. // [Old] for attachments we only export IDs and absolute url to original doc
  102. // [New] Encode attachment to base64
  103. const getBase64Data = function(doc, callback) {
  104. let buffer = new Buffer(0);
  105. // callback has the form function (err, res) {}
  106. const readStream = doc.createReadStream();
  107. readStream.on('data', function(chunk) {
  108. buffer = Buffer.concat([buffer, chunk]);
  109. });
  110. readStream.on('error', function(err) {
  111. callback(err, null);
  112. });
  113. readStream.on('end', function() {
  114. // done
  115. callback(null, buffer.toString('base64'));
  116. });
  117. };
  118. const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
  119. result.attachments = Attachments.find(byBoard).fetch().map((attachment) => {
  120. return {
  121. _id: attachment._id,
  122. cardId: attachment.cardId,
  123. // url: FlowRouter.url(attachment.url()),
  124. file: getBase64DataSync(attachment),
  125. name: attachment.original.name,
  126. type: attachment.original.type,
  127. };
  128. });
  129. // we also have to export some user data - as the other elements only
  130. // include id but we have to be careful:
  131. // 1- only exports users that are linked somehow to that board
  132. // 2- do not export any sensitive information
  133. const users = {};
  134. result.members.forEach((member) => {
  135. users[member.userId] = true;
  136. });
  137. result.lists.forEach((list) => {
  138. users[list.userId] = true;
  139. });
  140. result.cards.forEach((card) => {
  141. users[card.userId] = true;
  142. if (card.members) {
  143. card.members.forEach((memberId) => {
  144. users[memberId] = true;
  145. });
  146. }
  147. });
  148. result.comments.forEach((comment) => {
  149. users[comment.userId] = true;
  150. });
  151. result.activities.forEach((activity) => {
  152. users[activity.userId] = true;
  153. });
  154. result.checklists.forEach((checklist) => {
  155. users[checklist.userId] = true;
  156. });
  157. const byUserIds = {
  158. _id: {
  159. $in: Object.getOwnPropertyNames(users),
  160. },
  161. };
  162. // we use whitelist to be sure we do not expose inadvertently
  163. // some secret fields that gets added to User later.
  164. const userFields = {
  165. fields: {
  166. _id: 1,
  167. username: 1,
  168. 'profile.fullname': 1,
  169. 'profile.initials': 1,
  170. 'profile.avatarUrl': 1,
  171. },
  172. };
  173. result.users = Users.find(byUserIds, userFields).fetch().map((user) => {
  174. // user avatar is stored as a relative url, we export absolute
  175. if (user.profile.avatarUrl) {
  176. user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
  177. }
  178. return user;
  179. });
  180. return result;
  181. }
  182. canExport(user) {
  183. const board = Boards.findOne(this._boardId);
  184. return board && board.isVisibleBy(user);
  185. }
  186. }