diff options
author | Lauri Ojansivu <x@xet7.org> | 2020-01-16 06:15:10 +0200 |
---|---|---|
committer | Lauri Ojansivu <x@xet7.org> | 2020-01-16 06:15:10 +0200 |
commit | 6a01170d8696322462c4065ce0cf4a637a058975 (patch) | |
tree | f0c8315dfd918816300ea93eebab3bad1c0dc2be /.snap-meteor-1.8/export.js | |
parent | 55a2aa90cbbf44200e9b0b9f4bd08b6177f1bb95 (diff) | |
download | wekan-6a01170d8696322462c4065ce0cf4a637a058975.tar.gz wekan-6a01170d8696322462c4065ce0cf4a637a058975.tar.bz2 wekan-6a01170d8696322462c4065ce0cf4a637a058975.zip |
Use Meteor 1.8.x on Snap.
Diffstat (limited to '.snap-meteor-1.8/export.js')
-rw-r--r-- | .snap-meteor-1.8/export.js | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/.snap-meteor-1.8/export.js b/.snap-meteor-1.8/export.js new file mode 100644 index 00000000..cc979ce0 --- /dev/null +++ b/.snap-meteor-1.8/export.js @@ -0,0 +1,238 @@ +/* global JsonRoutes */ +if (Meteor.isServer) { + // todo XXX once we have a real API in place, move that route there + // todo XXX also share the route definition between the client and the server + // so that we could use something like + // `ApiRoutes.path('boards/export', boardId)`` + // on the client instead of copy/pasting the route path manually between the + // client and the server. + /** + * @operation export + * @tag Boards + * + * @summary This route is used to export the board. + * + * @description If user is already logged-in, pass loginToken as param + * "authToken": '/api/boards/:boardId/export?authToken=:token' + * + * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/ + * for detailed explanations + * + * @param {string} boardId the ID of the board we are exporting + * @param {string} authToken the loginToken + */ + JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) { + const boardId = req.params.boardId; + let user = null; + + const loginToken = req.query.authToken; + if (loginToken) { + const hashToken = Accounts._hashLoginToken(loginToken); + user = Meteor.users.findOne({ + 'services.resume.loginTokens.hashedToken': hashToken, + }); + } else if (!Meteor.settings.public.sandstorm) { + Authentication.checkUserId(req.userId); + user = Users.findOne({ _id: req.userId, isAdmin: true }); + } + + const exporter = new Exporter(boardId); + if (exporter.canExport(user)) { + JsonRoutes.sendResult(res, { + code: 200, + data: exporter.build(), + }); + } else { + // we could send an explicit error message, but on the other hand the only + // way to get there is by hacking the UI so let's keep it raw. + JsonRoutes.sendResult(res, 403); + } + }); +} + +// exporter maybe is broken since Gridfs introduced, add fs and path + +export class Exporter { + constructor(boardId) { + this._boardId = boardId; + } + + build() { + const fs = Npm.require('fs'); + const os = Npm.require('os'); + const path = Npm.require('path'); + + const byBoard = { boardId: this._boardId }; + const byBoardNoLinked = { + boardId: this._boardId, + linkedId: { $in: ['', null] }, + }; + // we do not want to retrieve boardId in related elements + const noBoardId = { + fields: { + boardId: 0, + }, + }; + const result = { + _format: 'wekan-board-1.0.0', + }; + _.extend( + result, + Boards.findOne(this._boardId, { + fields: { + stars: 0, + }, + }), + ); + result.lists = Lists.find(byBoard, noBoardId).fetch(); + result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); + result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); + result.customFields = CustomFields.find( + { boardIds: { $in: [this.boardId] } }, + { fields: { boardId: 0 } }, + ).fetch(); + result.comments = CardComments.find(byBoard, noBoardId).fetch(); + result.activities = Activities.find(byBoard, noBoardId).fetch(); + result.rules = Rules.find(byBoard, noBoardId).fetch(); + result.checklists = []; + result.checklistItems = []; + result.subtaskItems = []; + result.triggers = []; + result.actions = []; + result.cards.forEach(card => { + result.checklists.push( + ...Checklists.find({ + cardId: card._id, + }).fetch(), + ); + result.checklistItems.push( + ...ChecklistItems.find({ + cardId: card._id, + }).fetch(), + ); + result.subtaskItems.push( + ...Cards.find({ + parentId: card._id, + }).fetch(), + ); + }); + result.rules.forEach(rule => { + result.triggers.push( + ...Triggers.find( + { + _id: rule.triggerId, + }, + noBoardId, + ).fetch(), + ); + result.actions.push( + ...Actions.find( + { + _id: rule.actionId, + }, + noBoardId, + ).fetch(), + ); + }); + + // [Old] for attachments we only export IDs and absolute url to original doc + // [New] Encode attachment to base64 + const getBase64Data = function(doc, callback) { + let buffer = new Buffer(0); + // callback has the form function (err, res) {} + const tmpFile = path.join( + os.tmpdir(), + `tmpexport${process.pid}${Math.random()}`, + ); + const tmpWriteable = fs.createWriteStream(tmpFile); + const readStream = doc.createReadStream(); + readStream.on('data', function(chunk) { + buffer = Buffer.concat([buffer, chunk]); + }); + readStream.on('error', function(err) { + callback(err, null); + }); + readStream.on('end', function() { + // done + fs.unlink(tmpFile, () => { + //ignored + }); + callback(null, buffer.toString('base64')); + }); + readStream.pipe(tmpWriteable); + }; + const getBase64DataSync = Meteor.wrapAsync(getBase64Data); + result.attachments = Attachments.find(byBoard) + .fetch() + .map(attachment => { + return { + _id: attachment._id, + cardId: attachment.cardId, + // url: FlowRouter.url(attachment.url()), + file: getBase64DataSync(attachment), + name: attachment.original.name, + type: attachment.original.type, + }; + }); + + // we also have to export some user data - as the other elements only + // include id but we have to be careful: + // 1- only exports users that are linked somehow to that board + // 2- do not export any sensitive information + const users = {}; + result.members.forEach(member => { + users[member.userId] = true; + }); + result.lists.forEach(list => { + users[list.userId] = true; + }); + result.cards.forEach(card => { + users[card.userId] = true; + if (card.members) { + card.members.forEach(memberId => { + users[memberId] = true; + }); + } + }); + result.comments.forEach(comment => { + users[comment.userId] = true; + }); + result.activities.forEach(activity => { + users[activity.userId] = true; + }); + result.checklists.forEach(checklist => { + users[checklist.userId] = true; + }); + const byUserIds = { + _id: { + $in: Object.getOwnPropertyNames(users), + }, + }; + // we use whitelist to be sure we do not expose inadvertently + // some secret fields that gets added to User later. + const userFields = { + fields: { + _id: 1, + username: 1, + 'profile.fullname': 1, + 'profile.initials': 1, + 'profile.avatarUrl': 1, + }, + }; + result.users = Users.find(byUserIds, userFields) + .fetch() + .map(user => { + // user avatar is stored as a relative url, we export absolute + if ((user.profile || {}).avatarUrl) { + user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl); + } + return user; + }); + return result; + } + + canExport(user) { + const board = Boards.findOne(this._boardId); + return board && board.isVisibleBy(user); + } +} |