/* 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 = Buffer . allocUnsafe ( 0 ) ;
buffer . fill ( 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 = fs . createReadStream ( doc . path ) ;
readStream . on ( 'data' , function ( chunk ) {
buffer = Buffer . concat ( [ buffer , chunk ] ) ;
} ) ;
readStream . on ( 'error' , function ( err ) {
callback ( null , 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 ( { 'meta.boardId' : byBoard . boardId } )
. fetch ( )
. map ( attachment => {
let filebase64 = null ;
filebase64 = getBase64DataSync ( attachment ) ;
return {
_id : attachment . _id ,
cardId : attachment . meta . cardId ,
//url: FlowRouter.url(attachment.url()),
file : filebase64 ,
name : attachment . name ,
type : attachment . 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 ) ;
}
}