From 4889e9732d2458d60e2a079d3e54e128f6ce1b53 Mon Sep 17 00:00:00 2001 From: BoHong Li Date: Wed, 8 Mar 2017 18:45:51 +0800 Subject: [PATCH] Use JavaScript Standard Style Introduce JavaScript Standard Style as project style rule, and fixed all fail on backend code. --- app.js | 1122 ++++++----- lib/auth.js | 360 ++-- lib/config.js | 330 ++- lib/history.js | 309 +-- lib/letter-avatars.js | 38 +- lib/logger.js | 36 +- .../20160515114000-user-add-tokens.js | 22 +- .../20160607060246-support-revision.js | 14 +- .../20160703062241-support-authorship.js | 18 +- .../20161009040430-support-delete-note.js | 8 +- .../20161201050312-support-email-signin.js | 12 +- lib/models/author.js | 74 +- lib/models/index.js | 68 +- lib/models/note.js | 1021 +++++----- lib/models/revision.js | 584 +++--- lib/models/temp.js | 32 +- lib/models/user.js | 280 ++- lib/realtime.js | 1793 ++++++++--------- lib/response.js | 1128 +++++------ lib/workers/dmpWorker.js | 249 ++- package.json | 9 +- 21 files changed, 3723 insertions(+), 3784 deletions(-) diff --git a/app.js b/app.js index 67a6254..c68652b 100644 --- a/app.js +++ b/app.js @@ -1,654 +1,656 @@ -//app -//external modules -var express = require('express'); -var toobusy = require('toobusy-js'); -var ejs = require('ejs'); -var passport = require('passport'); -var methodOverride = require('method-override'); -var cookieParser = require('cookie-parser'); -var bodyParser = require('body-parser'); +// app +// external modules +var express = require('express') +var toobusy = require('toobusy-js') +var ejs = require('ejs') +var passport = require('passport') +var methodOverride = require('method-override') +var cookieParser = require('cookie-parser') +var bodyParser = require('body-parser') var compression = require('compression') -var session = require('express-session'); -var SequelizeStore = require('connect-session-sequelize')(session.Store); -var fs = require('fs'); -var url = require('url'); -var path = require('path'); -var imgur = require('imgur'); -var formidable = require('formidable'); -var morgan = require('morgan'); -var passportSocketIo = require("passport.socketio"); -var helmet = require('helmet'); -var i18n = require('i18n'); -var flash = require('connect-flash'); -var validator = require('validator'); +var session = require('express-session') +var SequelizeStore = require('connect-session-sequelize')(session.Store) +var fs = require('fs') +var url = require('url') +var path = require('path') +var imgur = require('imgur') +var formidable = require('formidable') +var morgan = require('morgan') +var passportSocketIo = require('passport.socketio') +var helmet = require('helmet') +var i18n = require('i18n') +var flash = require('connect-flash') +var validator = require('validator') -//core -var config = require("./lib/config.js"); -var logger = require("./lib/logger.js"); -var auth = require("./lib/auth.js"); -var response = require("./lib/response.js"); -var models = require("./lib/models"); +// core +var config = require('./lib/config.js') +var logger = require('./lib/logger.js') +var auth = require('./lib/auth.js') +var response = require('./lib/response.js') +var models = require('./lib/models') -//server setup +// server setup +var app = express() +var server = null if (config.usessl) { - var ca = (function () { - var i, len, results; - results = []; - for (i = 0, len = config.sslcapath.length; i < len; i++) { - results.push(fs.readFileSync(config.sslcapath[i], 'utf8')); - } - return results; - })(); - var options = { - key: fs.readFileSync(config.sslkeypath, 'utf8'), - cert: fs.readFileSync(config.sslcertpath, 'utf8'), - ca: ca, - dhparam: fs.readFileSync(config.dhparampath, 'utf8'), - requestCert: false, - rejectUnauthorized: false - }; - var app = express(); - var server = require('https').createServer(options, app); + var ca = (function () { + var i, len, results + results = [] + for (i = 0, len = config.sslcapath.length; i < len; i++) { + results.push(fs.readFileSync(config.sslcapath[i], 'utf8')) + } + return results + })() + var options = { + key: fs.readFileSync(config.sslkeypath, 'utf8'), + cert: fs.readFileSync(config.sslcertpath, 'utf8'), + ca: ca, + dhparam: fs.readFileSync(config.dhparampath, 'utf8'), + requestCert: false, + rejectUnauthorized: false + } + server = require('https').createServer(options, app) } else { - var app = express(); - var server = require('http').createServer(app); + server = require('http').createServer(app) } -//logger +// logger app.use(morgan('combined', { - "stream": logger.stream -})); + 'stream': logger.stream +})) -//socket io -var io = require('socket.io')(server); +// socket io +var io = require('socket.io')(server) io.engine.ws = new (require('uws').Server)({ - noServer: true, - perMessageDeflate: false -}); + noServer: true, + perMessageDeflate: false +}) -//others -var realtime = require("./lib/realtime.js"); +// others +var realtime = require('./lib/realtime.js') -//assign socket io to realtime -realtime.io = io; +// assign socket io to realtime +realtime.io = io -//methodOverride -app.use(methodOverride('_method')); - -// create application/json parser -var jsonParser = bodyParser.json({ - limit: 1024 * 1024 * 10 // 10 mb -}); +// methodOverride +app.use(methodOverride('_method')) // create application/x-www-form-urlencoded parser var urlencodedParser = bodyParser.urlencoded({ - extended: false, - limit: 1024 * 1024 * 10 // 10 mb -}); + extended: false, + limit: 1024 * 1024 * 10 // 10 mb +}) -//session store +// session store var sessionStore = new SequelizeStore({ - db: models.sequelize -}); + db: models.sequelize +}) -//compression -app.use(compression()); +// compression +app.use(compression()) // use hsts to tell https users stick to this app.use(helmet.hsts({ - maxAge: 31536000 * 1000, // 365 days - includeSubdomains: true, - preload: true -})); + maxAge: 31536000 * 1000, // 365 days + includeSubdomains: true, + preload: true +})) i18n.configure({ - locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo'], - cookie: 'locale', - directory: __dirname + '/locales' -}); + locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo'], + cookie: 'locale', + directory: path.join(__dirname, '/locales') +}) -app.use(cookieParser()); +app.use(cookieParser()) -app.use(i18n.init); +app.use(i18n.init) // routes without sessions // static files -app.use('/', express.static(__dirname + '/public', { maxAge: config.staticcachetime })); +app.use('/', express.static(path.join(__dirname, '/public'), { maxAge: config.staticcachetime })) -//session +// session app.use(session({ - name: config.sessionname, - secret: config.sessionsecret, - resave: false, //don't save session if unmodified - saveUninitialized: true, //always create session to ensure the origin - rolling: true, // reset maxAge on every response - cookie: { - maxAge: config.sessionlife - }, - store: sessionStore -})); + name: config.sessionname, + secret: config.sessionsecret, + resave: false, // don't save session if unmodified + saveUninitialized: true, // always create session to ensure the origin + rolling: true, // reset maxAge on every response + cookie: { + maxAge: config.sessionlife + }, + store: sessionStore +})) // session resumption -var tlsSessionStore = {}; +var tlsSessionStore = {} server.on('newSession', function (id, data, cb) { - tlsSessionStore[id.toString('hex')] = data; - cb(); -}); + tlsSessionStore[id.toString('hex')] = data + cb() +}) server.on('resumeSession', function (id, cb) { - cb(null, tlsSessionStore[id.toString('hex')] || null); -}); + cb(null, tlsSessionStore[id.toString('hex')] || null) +}) -//middleware which blocks requests when we're too busy +// middleware which blocks requests when we're too busy app.use(function (req, res, next) { - if (toobusy()) { - response.errorServiceUnavailable(res); - } else { - next(); - } -}); + if (toobusy()) { + response.errorServiceUnavailable(res) + } else { + next() + } +}) -app.use(flash()); +app.use(flash()) -//passport -app.use(passport.initialize()); -app.use(passport.session()); +// passport +app.use(passport.initialize()) +app.use(passport.session()) +auth.registerAuthMethod() -//serialize and deserialize +// serialize and deserialize passport.serializeUser(function (user, done) { - logger.info('serializeUser: ' + user.id); - return done(null, user.id); -}); + logger.info('serializeUser: ' + user.id) + return done(null, user.id) +}) passport.deserializeUser(function (id, done) { - models.User.findOne({ - where: { - id: id - } - }).then(function (user) { - logger.info('deserializeUser: ' + user.id); - return done(null, user); - }).catch(function (err) { - logger.error(err); - return done(err, null); - }); -}); + models.User.findOne({ + where: { + id: id + } + }).then(function (user) { + logger.info('deserializeUser: ' + user.id) + return done(null, user) + }).catch(function (err) { + logger.error(err) + return done(err, null) + }) +}) // check uri is valid before going further -app.use(function(req, res, next) { - try { - decodeURIComponent(req.path); - } catch (err) { - logger.error(err); - return response.errorBadRequest(res); - } - next(); -}); +app.use(function (req, res, next) { + try { + decodeURIComponent(req.path) + } catch (err) { + logger.error(err) + return response.errorBadRequest(res) + } + next() +}) // redirect url without trailing slashes -app.use(function(req, res, next) { - if ("GET" == req.method && req.path.substr(-1) == '/' && req.path.length > 1) { - var query = req.url.slice(req.path.length); - var urlpath = req.path.slice(0, -1); - var serverurl = config.serverurl; - if (config.urlpath) serverurl = serverurl.slice(0, -(config.urlpath.length + 1)); - res.redirect(301, serverurl + urlpath + query); - } else { - next(); - } -}); +app.use(function (req, res, next) { + if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) { + var query = req.url.slice(req.path.length) + var urlpath = req.path.slice(0, -1) + var serverurl = config.serverurl + if (config.urlpath) serverurl = serverurl.slice(0, -(config.urlpath.length + 1)) + res.redirect(301, serverurl + urlpath + query) + } else { + next() + } +}) // routes need sessions -//template files -app.set('views', __dirname + '/public/views'); -//set render engine -app.engine('ejs', ejs.renderFile); -//set view engine -app.set('view engine', 'ejs'); -//get index -app.get("/", response.showIndex); -//get 403 forbidden -app.get("/403", function (req, res) { - response.errorForbidden(res); -}); -//get 404 not found -app.get("/404", function (req, res) { - response.errorNotFound(res); -}); -//get 500 internal error -app.get("/500", function (req, res) { - response.errorInternalError(res); -}); -//get status -app.get("/status", function (req, res, next) { - realtime.getStatus(function (data) { - res.set({ - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(data); - }); -}); -//get status -app.get("/temp", function (req, res) { - var host = req.get('host'); - if (config.alloworigin.indexOf(host) == -1) - response.errorForbidden(res); - else { - var tempid = req.query.tempid; - if (!tempid) - response.errorForbidden(res); - else { - models.Temp.findOne({ - where: { - id: tempid - } - }).then(function (temp) { - if (!temp) - response.errorNotFound(res); - else { - res.header("Access-Control-Allow-Origin", "*"); - res.send({ - temp: temp.data - }); - temp.destroy().catch(function (err) { - if (err) - logger.error('remove temp failed: ' + err); - }); - } - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); +// template files +app.set('views', path.join(__dirname, '/public/views')) +// set render engine +app.engine('ejs', ejs.renderFile) +// set view engine +app.set('view engine', 'ejs') +// get index +app.get('/', response.showIndex) +// get 403 forbidden +app.get('/403', function (req, res) { + response.errorForbidden(res) +}) +// get 404 not found +app.get('/404', function (req, res) { + response.errorNotFound(res) +}) +// get 500 internal error +app.get('/500', function (req, res) { + response.errorInternalError(res) +}) +// get status +app.get('/status', function (req, res, next) { + realtime.getStatus(function (data) { + res.set({ + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(data) + }) +}) +// get status +app.get('/temp', function (req, res) { + var host = req.get('host') + if (config.alloworigin.indexOf(host) === -1) { + response.errorForbidden(res) + } else { + var tempid = req.query.tempid + if (!tempid) { + response.errorForbidden(res) + } else { + models.Temp.findOne({ + where: { + id: tempid } - } -}); -//post status -app.post("/temp", urlencodedParser, function (req, res) { - var host = req.get('host'); - if (config.alloworigin.indexOf(host) == -1) - response.errorForbidden(res); - else { - var data = req.body.data; - if (!data) - response.errorForbidden(res); - else { - if (config.debug) - logger.info('SERVER received temp from [' + host + ']: ' + req.body.data); - models.Temp.create({ - data: data - }).then(function (temp) { - if (temp) { - res.header("Access-Control-Allow-Origin", "*"); - res.send({ - status: 'ok', - id: temp.id - }); - } else - response.errorInternalError(res); - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); + }).then(function (temp) { + if (!temp) { + response.errorNotFound(res) + } else { + res.header('Access-Control-Allow-Origin', '*') + res.send({ + temp: temp.data + }) + temp.destroy().catch(function (err) { + if (err) { + logger.error('remove temp failed: ' + err) + } + }) } + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) } -}); + } +}) +// post status +app.post('/temp', urlencodedParser, function (req, res) { + var host = req.get('host') + if (config.alloworigin.indexOf(host) === -1) { + response.errorForbidden(res) + } else { + var data = req.body.data + if (!data) { + response.errorForbidden(res) + } else { + if (config.debug) { + logger.info('SERVER received temp from [' + host + ']: ' + req.body.data) + } + models.Temp.create({ + data: data + }).then(function (temp) { + if (temp) { + res.header('Access-Control-Allow-Origin', '*') + res.send({ + status: 'ok', + id: temp.id + }) + } else { + response.errorInternalError(res) + } + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + } + } +}) -function setReturnToFromReferer(req) { - var referer = req.get('referer'); - if (!req.session) req.session = {}; - req.session.returnTo = referer; +function setReturnToFromReferer (req) { + var referer = req.get('referer') + if (!req.session) req.session = {} + req.session.returnTo = referer } -//facebook auth +// facebook auth if (config.facebook) { - app.get('/auth/facebook', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('facebook')(req, res, next); - }); - //facebook auth callback - app.get('/auth/facebook/callback', + app.get('/auth/facebook', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('facebook')(req, res, next) + }) + // facebook auth callback + app.get('/auth/facebook/callback', passport.authenticate('facebook', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } -//twitter auth +// twitter auth if (config.twitter) { - app.get('/auth/twitter', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('twitter')(req, res, next); - }); - //twitter auth callback - app.get('/auth/twitter/callback', + app.get('/auth/twitter', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('twitter')(req, res, next) + }) + // twitter auth callback + app.get('/auth/twitter/callback', passport.authenticate('twitter', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } -//github auth +// github auth if (config.github) { - app.get('/auth/github', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('github')(req, res, next); - }); - //github auth callback - app.get('/auth/github/callback', + app.get('/auth/github', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('github')(req, res, next) + }) + // github auth callback + app.get('/auth/github/callback', passport.authenticate('github', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); - //github callback actions - app.get('/auth/github/callback/:noteId/:action', response.githubActions); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) + // github callback actions + app.get('/auth/github/callback/:noteId/:action', response.githubActions) } -//gitlab auth +// gitlab auth if (config.gitlab) { - app.get('/auth/gitlab', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('gitlab')(req, res, next); - }); - //gitlab auth callback - app.get('/auth/gitlab/callback', + app.get('/auth/gitlab', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('gitlab')(req, res, next) + }) + // gitlab auth callback + app.get('/auth/gitlab/callback', passport.authenticate('gitlab', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); - //gitlab callback actions - app.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) + // gitlab callback actions + app.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) } -//dropbox auth +// dropbox auth if (config.dropbox) { - app.get('/auth/dropbox', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('dropbox-oauth2')(req, res, next); - }); - //dropbox auth callback - app.get('/auth/dropbox/callback', + app.get('/auth/dropbox', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('dropbox-oauth2')(req, res, next) + }) + // dropbox auth callback + app.get('/auth/dropbox/callback', passport.authenticate('dropbox-oauth2', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } -//google auth +// google auth if (config.google) { - app.get('/auth/google', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('google', { scope: ['profile'] })(req, res, next); - }); - //google auth callback - app.get('/auth/google/callback', + app.get('/auth/google', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('google', { scope: ['profile'] })(req, res, next) + }) + // google auth callback + app.get('/auth/google/callback', passport.authenticate('google', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } // ldap auth if (config.ldap) { - app.post('/auth/ldap', urlencodedParser, function (req, res, next) { - if (!req.body.username || !req.body.password) return response.errorBadRequest(res); - setReturnToFromReferer(req); - passport.authenticate('ldapauth', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/', - failureFlash: true - })(req, res, next); - }); + app.post('/auth/ldap', urlencodedParser, function (req, res, next) { + if (!req.body.username || !req.body.password) return response.errorBadRequest(res) + setReturnToFromReferer(req) + passport.authenticate('ldapauth', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/', + failureFlash: true + })(req, res, next) + }) } // email auth if (config.email) { - if (config.allowemailregister) - app.post('/register', urlencodedParser, function (req, res, next) { - if (!req.body.email || !req.body.password) return response.errorBadRequest(res); - if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res); - models.User.findOrCreate({ - where: { - email: req.body.email - }, - defaults: { - password: req.body.password - } - }).spread(function (user, created) { - if (user) { - if (created) { - if (config.debug) logger.info('user registered: ' + user.id); - req.flash('info', "You've successfully registered, please signin."); - } else { - if (config.debug) logger.info('user found: ' + user.id); - req.flash('error', "This email has been used, please try another one."); - } - return res.redirect(config.serverurl + '/'); - } - req.flash('error', "Failed to register your account, please try again."); - return res.redirect(config.serverurl + '/'); - }).catch(function (err) { - logger.error('auth callback failed: ' + err); - return response.errorInternalError(res); - }); - }); - - app.post('/login', urlencodedParser, function (req, res, next) { - if (!req.body.email || !req.body.password) return response.errorBadRequest(res); - if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res); - setReturnToFromReferer(req); - passport.authenticate('local', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/', - failureFlash: 'Invalid email or password.' - })(req, res, next); - }); -} -//logout -app.get('/logout', function (req, res) { - if (config.debug && req.isAuthenticated()) - logger.info('user logout: ' + req.user.id); - req.logout(); - res.redirect(config.serverurl + '/'); -}); -var history = require("./lib/history.js"); -//get history -app.get('/history', history.historyGet); -//post history -app.post('/history', urlencodedParser, history.historyPost); -//post history by note id -app.post('/history/:noteId', urlencodedParser, history.historyPost); -//delete history -app.delete('/history', history.historyDelete); -//delete history by note id -app.delete('/history/:noteId', history.historyDelete); -//get me info -app.get('/me', function (req, res) { - if (req.isAuthenticated()) { - models.User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (!user) - return response.errorNotFound(res); - var profile = models.User.getProfile(user); - res.send({ - status: 'ok', - id: req.user.id, - name: profile.name, - photo: profile.photo - }); - }).catch(function (err) { - logger.error('read me failed: ' + err); - return response.errorInternalError(res); - }); - } else { - res.send({ - status: 'forbidden' - }); - } -}); - -//upload image -app.post('/uploadimage', function (req, res) { - var form = new formidable.IncomingForm(); - - form.keepExtensions = true; - - if (config.imageUploadType === 'filesystem') { - form.uploadDir = "public/uploads"; - } - - form.parse(req, function (err, fields, files) { - if (err || !files.image || !files.image.path) { - response.errorForbidden(res); - } else { - if (config.debug) - logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)); - - try { - switch (config.imageUploadType) { - case 'filesystem': - res.send({ - link: url.resolve(config.serverurl + '/', files.image.path.match(/^public\/(.+$)/)[1]) - }); - - break; - - case 's3': - var AWS = require('aws-sdk'); - var awsConfig = new AWS.Config(config.s3); - var s3 = new AWS.S3(awsConfig); - - fs.readFile(files.image.path, function (err, buffer) { - var params = { - Bucket: config.s3bucket, - Key: path.join('uploads', path.basename(files.image.path)), - Body: buffer - }; - - s3.putObject(params, function (err, data) { - if (err) { - logger.error(err); - res.status(500).end('upload image error'); - } else { - res.send({ - link: `https://s3-${config.s3.region}.amazonaws.com/${config.s3bucket}/${params.Key}` - }); - } - }); - - }); - - break; - - case 'imgur': - default: - imgur.setClientId(config.imgur.clientID); - imgur.uploadFile(files.image.path) - .then(function (json) { - if (config.debug) - logger.info('SERVER uploadimage success: ' + JSON.stringify(json)); - res.send({ - link: json.data.link.replace(/^http:\/\//i, 'https://') - }); - }) - .catch(function (err) { - logger.error(err); - return res.status(500).end('upload image error'); - }); - break; - } - } catch (err) { - logger.error(err); - return res.status(500).end('upload image error'); - } + if (config.allowemailregister) { + app.post('/register', urlencodedParser, function (req, res, next) { + if (!req.body.email || !req.body.password) return response.errorBadRequest(res) + if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) + models.User.findOrCreate({ + where: { + email: req.body.email + }, + defaults: { + password: req.body.password } - }); -}); -//get new note -app.get("/new", response.newNote); -//get publish note -app.get("/s/:shortid", response.showPublishNote); -//publish note actions -app.get("/s/:shortid/:action", response.publishNoteActions); -//get publish slide -app.get("/p/:shortid", response.showPublishSlide); -//publish slide actions -app.get("/p/:shortid/:action", response.publishSlideActions); -//get note by id -app.get("/:noteId", response.showNote); -//note actions -app.get("/:noteId/:action", response.noteActions); -//note actions with action id -app.get("/:noteId/:action/:actionId", response.noteActions); + }).spread(function (user, created) { + if (user) { + if (created) { + if (config.debug) { + logger.info('user registered: ' + user.id) + } + req.flash('info', "You've successfully registered, please signin.") + } else { + if (config.debug) { + logger.info('user found: ' + user.id) + } + req.flash('error', 'This email has been used, please try another one.') + } + return res.redirect(config.serverurl + '/') + } + req.flash('error', 'Failed to register your account, please try again.') + return res.redirect(config.serverurl + '/') + }).catch(function (err) { + logger.error('auth callback failed: ' + err) + return response.errorInternalError(res) + }) + }) + } + + app.post('/login', urlencodedParser, function (req, res, next) { + if (!req.body.email || !req.body.password) return response.errorBadRequest(res) + if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) + setReturnToFromReferer(req) + passport.authenticate('local', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/', + failureFlash: 'Invalid email or password.' + })(req, res, next) + }) +} +// logout +app.get('/logout', function (req, res) { + if (config.debug && req.isAuthenticated()) { logger.info('user logout: ' + req.user.id) } + req.logout() + res.redirect(config.serverurl + '/') +}) +var history = require('./lib/history.js') +// get history +app.get('/history', history.historyGet) +// post history +app.post('/history', urlencodedParser, history.historyPost) +// post history by note id +app.post('/history/:noteId', urlencodedParser, history.historyPost) +// delete history +app.delete('/history', history.historyDelete) +// delete history by note id +app.delete('/history/:noteId', history.historyDelete) +// get me info +app.get('/me', function (req, res) { + if (req.isAuthenticated()) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (!user) { return response.errorNotFound(res) } + var profile = models.User.getProfile(user) + res.send({ + status: 'ok', + id: req.user.id, + name: profile.name, + photo: profile.photo + }) + }).catch(function (err) { + logger.error('read me failed: ' + err) + return response.errorInternalError(res) + }) + } else { + res.send({ + status: 'forbidden' + }) + } +}) + +// upload image +app.post('/uploadimage', function (req, res) { + var form = new formidable.IncomingForm() + + form.keepExtensions = true + + if (config.imageUploadType === 'filesystem') { + form.uploadDir = 'public/uploads' + } + + form.parse(req, function (err, fields, files) { + if (err || !files.image || !files.image.path) { + response.errorForbidden(res) + } else { + if (config.debug) { logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)) } + + try { + switch (config.imageUploadType) { + case 'filesystem': + res.send({ + link: url.resolve(config.serverurl + '/', files.image.path.match(/^public\/(.+$)/)[1]) + }) + + break + + case 's3': + var AWS = require('aws-sdk') + var awsConfig = new AWS.Config(config.s3) + var s3 = new AWS.S3(awsConfig) + + fs.readFile(files.image.path, function (err, buffer) { + if (err) { + logger.error(err) + res.status(500).end('upload image error') + return + } + var params = { + Bucket: config.s3bucket, + Key: path.join('uploads', path.basename(files.image.path)), + Body: buffer + } + + s3.putObject(params, function (err, data) { + if (err) { + logger.error(err) + res.status(500).end('upload image error') + return + } + res.send({ + link: `https://s3-${config.s3.region}.amazonaws.com/${config.s3bucket}/${params.Key}` + }) + }) + }) + break + case 'imgur': + default: + imgur.setClientId(config.imgur.clientID) + imgur.uploadFile(files.image.path) + .then(function (json) { + if (config.debug) { logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) } + res.send({ + link: json.data.link.replace(/^http:\/\//i, 'https://') + }) + }) + .catch(function (err) { + logger.error(err) + return res.status(500).end('upload image error') + }) + break + } + } catch (err) { + logger.error(err) + return res.status(500).end('upload image error') + } + } + }) +}) +// get new note +app.get('/new', response.newNote) +// get publish note +app.get('/s/:shortid', response.showPublishNote) +// publish note actions +app.get('/s/:shortid/:action', response.publishNoteActions) +// get publish slide +app.get('/p/:shortid', response.showPublishSlide) +// publish slide actions +app.get('/p/:shortid/:action', response.publishSlideActions) +// get note by id +app.get('/:noteId', response.showNote) +// note actions +app.get('/:noteId/:action', response.noteActions) +// note actions with action id +app.get('/:noteId/:action/:actionId', response.noteActions) // response not found if no any route matches app.get('*', function (req, res) { - response.errorNotFound(res); -}); + response.errorNotFound(res) +}) -//socket.io secure -io.use(realtime.secure); -//socket.io auth +// socket.io secure +io.use(realtime.secure) +// socket.io auth io.use(passportSocketIo.authorize({ - cookieParser: cookieParser, - key: config.sessionname, - secret: config.sessionsecret, - store: sessionStore, - success: realtime.onAuthorizeSuccess, - fail: realtime.onAuthorizeFail -})); -//socket.io heartbeat -io.set('heartbeat interval', config.heartbeatinterval); -io.set('heartbeat timeout', config.heartbeattimeout); -//socket.io connection -io.sockets.on('connection', realtime.connection); + cookieParser: cookieParser, + key: config.sessionname, + secret: config.sessionsecret, + store: sessionStore, + success: realtime.onAuthorizeSuccess, + fail: realtime.onAuthorizeFail +})) +// socket.io heartbeat +io.set('heartbeat interval', config.heartbeatinterval) +io.set('heartbeat timeout', config.heartbeattimeout) +// socket.io connection +io.sockets.on('connection', realtime.connection) -//listen -function startListen() { - server.listen(config.port, function () { - var schema = config.usessl ? 'HTTPS' : 'HTTP'; - logger.info('%s Server listening at port %d', schema, config.port); - config.maintenance = false; - }); +// listen +function startListen () { + server.listen(config.port, function () { + var schema = config.usessl ? 'HTTPS' : 'HTTP' + logger.info('%s Server listening at port %d', schema, config.port) + config.maintenance = false + }) } // sync db then start listen models.sequelize.sync().then(function () { - // check if realtime is ready - if (realtime.isReady()) { - models.Revision.checkAllNotesRevision(function (err, notes) { - if (err) throw new Error(err); - if (!notes || notes.length <= 0) return startListen(); - }); - } else { - throw new Error('server still not ready after db synced'); - } -}); + // check if realtime is ready + if (realtime.isReady()) { + models.Revision.checkAllNotesRevision(function (err, notes) { + if (err) throw new Error(err) + if (!notes || notes.length <= 0) return startListen() + }) + } else { + throw new Error('server still not ready after db synced') + } +}) // log uncaught exception process.on('uncaughtException', function (err) { - logger.error('An uncaught exception has occured.'); - logger.error(err); - logger.error('Process will exit now.'); - process.exit(1); -}); + logger.error('An uncaught exception has occured.') + logger.error(err) + logger.error('Process will exit now.') + process.exit(1) +}) // install exit handler -function handleTermSignals() { - config.maintenance = true; - // disconnect all socket.io clients - Object.keys(io.sockets.sockets).forEach(function (key) { - var socket = io.sockets.sockets[key]; - // notify client server going into maintenance status - socket.emit('maintenance'); - setTimeout(function () { - socket.disconnect(true); - }, 0); - }); - var checkCleanTimer = setInterval(function () { - if (realtime.isReady()) { - models.Revision.checkAllNotesRevision(function (err, notes) { - if (err) return logger.error(err); - if (!notes || notes.length <= 0) { - clearInterval(checkCleanTimer); - return process.exit(0); - } - }); +function handleTermSignals () { + config.maintenance = true + // disconnect all socket.io clients + Object.keys(io.sockets.sockets).forEach(function (key) { + var socket = io.sockets.sockets[key] + // notify client server going into maintenance status + socket.emit('maintenance') + setTimeout(function () { + socket.disconnect(true) + }, 0) + }) + var checkCleanTimer = setInterval(function () { + if (realtime.isReady()) { + models.Revision.checkAllNotesRevision(function (err, notes) { + if (err) return logger.error(err) + if (!notes || notes.length <= 0) { + clearInterval(checkCleanTimer) + return process.exit(0) } - }, 100); + }) + } + }, 100) } -process.on('SIGINT', handleTermSignals); -process.on('SIGTERM', handleTermSignals); +process.on('SIGINT', handleTermSignals) +process.on('SIGTERM', handleTermSignals) diff --git a/lib/auth.js b/lib/auth.js index 4b14e42..ef1d646 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,190 +1,192 @@ -//auth -//external modules -var passport = require('passport'); -var FacebookStrategy = require('passport-facebook').Strategy; -var TwitterStrategy = require('passport-twitter').Strategy; -var GithubStrategy = require('passport-github').Strategy; -var GitlabStrategy = require('passport-gitlab2').Strategy; -var DropboxStrategy = require('passport-dropbox-oauth2').Strategy; -var GoogleStrategy = require('passport-google-oauth20').Strategy; -var LdapStrategy = require('passport-ldapauth'); -var LocalStrategy = require('passport-local').Strategy; -var validator = require('validator'); +// auth +// external modules +var passport = require('passport') +var FacebookStrategy = require('passport-facebook').Strategy +var TwitterStrategy = require('passport-twitter').Strategy +var GithubStrategy = require('passport-github').Strategy +var GitlabStrategy = require('passport-gitlab2').Strategy +var DropboxStrategy = require('passport-dropbox-oauth2').Strategy +var GoogleStrategy = require('passport-google-oauth20').Strategy +var LdapStrategy = require('passport-ldapauth') +var LocalStrategy = require('passport-local').Strategy +var validator = require('validator') -//core -var config = require('./config.js'); -var logger = require("./logger.js"); -var models = require("./models"); +// core +var config = require('./config.js') +var logger = require('./logger.js') +var models = require('./models') -function callback(accessToken, refreshToken, profile, done) { - //logger.info(profile.displayName || profile.username); - var stringifiedProfile = JSON.stringify(profile); - models.User.findOrCreate({ +function callback (accessToken, refreshToken, profile, done) { + // logger.info(profile.displayName || profile.username); + var stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: stringifiedProfile, + accessToken: accessToken, + refreshToken: refreshToken + } + }).spread(function (user, created) { + if (user) { + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (user.accessToken !== accessToken) { + user.accessToken = accessToken + needSave = true + } + if (user.refreshToken !== refreshToken) { + user.refreshToken = refreshToken + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + } + } + }).catch(function (err) { + logger.error('auth callback failed: ' + err) + return done(err, null) + }) +} + +function registerAuthMethod () { +// facebook + if (config.facebook) { + passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.serverurl + '/auth/facebook/callback' + }, callback)) + } +// twitter + if (config.twitter) { + passport.use(new TwitterStrategy({ + consumerKey: config.twitter.consumerKey, + consumerSecret: config.twitter.consumerSecret, + callbackURL: config.serverurl + '/auth/twitter/callback' + }, callback)) + } +// github + if (config.github) { + passport.use(new GithubStrategy({ + clientID: config.github.clientID, + clientSecret: config.github.clientSecret, + callbackURL: config.serverurl + '/auth/github/callback' + }, callback)) + } +// gitlab + if (config.gitlab) { + passport.use(new GitlabStrategy({ + baseURL: config.gitlab.baseURL, + clientID: config.gitlab.clientID, + clientSecret: config.gitlab.clientSecret, + callbackURL: config.serverurl + '/auth/gitlab/callback' + }, callback)) + } +// dropbox + if (config.dropbox) { + passport.use(new DropboxStrategy({ + apiVersion: '2', + clientID: config.dropbox.clientID, + clientSecret: config.dropbox.clientSecret, + callbackURL: config.serverurl + '/auth/dropbox/callback' + }, callback)) + } +// google + if (config.google) { + passport.use(new GoogleStrategy({ + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.serverurl + '/auth/google/callback' + }, callback)) + } +// ldap + if (config.ldap) { + passport.use(new LdapStrategy({ + server: { + url: config.ldap.url || null, + bindDn: config.ldap.bindDn || null, + bindCredentials: config.ldap.bindCredentials || null, + searchBase: config.ldap.searchBase || null, + searchFilter: config.ldap.searchFilter || null, + searchAttributes: config.ldap.searchAttributes || null, + tlsOptions: config.ldap.tlsOptions || null + } + }, + function (user, done) { + var profile = { + id: 'LDAP-' + user.uidNumber, + username: user.uid, + displayName: user.displayName, + emails: user.mail ? [user.mail] : [], + avatarUrl: null, + profileUrl: null, + provider: 'ldap' + } + var stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ where: { - profileid: profile.id.toString() + profileid: profile.id.toString() }, defaults: { - profile: stringifiedProfile, - accessToken: accessToken, - refreshToken: refreshToken + profile: stringifiedProfile } - }).spread(function (user, created) { + }).spread(function (user, created) { if (user) { - var needSave = false; - if (user.profile != stringifiedProfile) { - user.profile = stringifiedProfile; - needSave = true; - } - if (user.accessToken != accessToken) { - user.accessToken = accessToken; - needSave = true; - } - if (user.refreshToken != refreshToken) { - user.refreshToken = refreshToken; - needSave = true; - } - if (needSave) { - user.save().then(function () { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - }); - } else { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - } + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + } } - }).catch(function (err) { - logger.error('auth callback failed: ' + err); - return done(err, null); - }); + }).catch(function (err) { + logger.error('ldap auth failed: ' + err) + return done(err, null) + }) + })) + } +// email + if (config.email) { + passport.use(new LocalStrategy({ + usernameField: 'email' + }, + function (email, password, done) { + if (!validator.isEmail(email)) return done(null, false) + models.User.findOne({ + where: { + email: email + } + }).then(function (user) { + if (!user) return done(null, false) + if (!user.verifyPassword(password)) return done(null, false) + return done(null, user) + }).catch(function (err) { + logger.error(err) + return done(err) + }) + })) + } } -//facebook -if (config.facebook) { - module.exports = passport.use(new FacebookStrategy({ - clientID: config.facebook.clientID, - clientSecret: config.facebook.clientSecret, - callbackURL: config.serverurl + '/auth/facebook/callback' - }, callback)); -} -//twitter -if (config.twitter) { - passport.use(new TwitterStrategy({ - consumerKey: config.twitter.consumerKey, - consumerSecret: config.twitter.consumerSecret, - callbackURL: config.serverurl + '/auth/twitter/callback' - }, callback)); -} -//github -if (config.github) { - passport.use(new GithubStrategy({ - clientID: config.github.clientID, - clientSecret: config.github.clientSecret, - callbackURL: config.serverurl + '/auth/github/callback' - }, callback)); -} -//gitlab -if (config.gitlab) { - passport.use(new GitlabStrategy({ - baseURL: config.gitlab.baseURL, - clientID: config.gitlab.clientID, - clientSecret: config.gitlab.clientSecret, - callbackURL: config.serverurl + '/auth/gitlab/callback' - }, callback)); -} -//dropbox -if (config.dropbox) { - passport.use(new DropboxStrategy({ - apiVersion: '2', - clientID: config.dropbox.clientID, - clientSecret: config.dropbox.clientSecret, - callbackURL: config.serverurl + '/auth/dropbox/callback' - }, callback)); -} -//google -if (config.google) { - passport.use(new GoogleStrategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.serverurl + '/auth/google/callback' - }, callback)); -} -// ldap -if (config.ldap) { - passport.use(new LdapStrategy({ - server: { - url: config.ldap.url || null, - bindDn: config.ldap.bindDn || null, - bindCredentials: config.ldap.bindCredentials || null, - searchBase: config.ldap.searchBase || null, - searchFilter: config.ldap.searchFilter || null, - searchAttributes: config.ldap.searchAttributes || null, - tlsOptions: config.ldap.tlsOptions || null - }, - }, - function(user, done) { - var profile = { - id: 'LDAP-' + user.uidNumber, - username: user.uid, - displayName: user.displayName, - emails: user.mail ? [user.mail] : [], - avatarUrl: null, - profileUrl: null, - provider: 'ldap', - } - var stringifiedProfile = JSON.stringify(profile); - models.User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile, - } - }).spread(function (user, created) { - if (user) { - var needSave = false; - if (user.profile != stringifiedProfile) { - user.profile = stringifiedProfile; - needSave = true; - } - if (needSave) { - user.save().then(function () { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - }); - } else { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - } - } - }).catch(function (err) { - logger.error('ldap auth failed: ' + err); - return done(err, null); - }); - })); -} -// email -if (config.email) { - passport.use(new LocalStrategy({ - usernameField: 'email' - }, - function(email, password, done) { - if (!validator.isEmail(email)) return done(null, false); - models.User.findOne({ - where: { - email: email - } - }).then(function (user) { - if (!user) return done(null, false); - if (!user.verifyPassword(password)) return done(null, false); - return done(null, user); - }).catch(function (err) { - logger.error(err); - return done(err); - }); - })); +module.exports = { + registerAuthMethod: registerAuthMethod } diff --git a/lib/config.js b/lib/config.js index 4d2fbf7..af4c22c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,118 +1,117 @@ // external modules -var fs = require('fs'); -var path = require('path'); -var fs = require('fs'); +var fs = require('fs') +var path = require('path') // configs -var env = process.env.NODE_ENV || 'development'; -var config = require(path.join(__dirname, '..', 'config.json'))[env]; -var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development')); +var env = process.env.NODE_ENV || 'development' +var config = require(path.join(__dirname, '..', 'config.json'))[env] +var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development')) // Create function that reads docker secrets but fails fast in case of a non docker environment -var handleDockerSecret = fs.existsSync('/run/secrets/') ? function(secret) { - return fs.existsSync('/run/secrets/' + secret) ? fs.readFileSync('/run/secrets/' + secret) : null; -} : function() { - return null -}; +var handleDockerSecret = fs.existsSync('/run/secrets/') ? function (secret) { + return fs.existsSync('/run/secrets/' + secret) ? fs.readFileSync('/run/secrets/' + secret) : null +} : function () { + return null +} // url -var domain = process.env.DOMAIN || process.env.HMD_DOMAIN || config.domain || ''; -var urlpath = process.env.URL_PATH || process.env.HMD_URL_PATH || config.urlpath || ''; -var port = process.env.PORT || process.env.HMD_PORT || config.port || 3000; -var alloworigin = process.env.HMD_ALLOW_ORIGIN ? process.env.HMD_ALLOW_ORIGIN.split(',') : (config.alloworigin || ['localhost']); +var domain = process.env.DOMAIN || process.env.HMD_DOMAIN || config.domain || '' +var urlpath = process.env.URL_PATH || process.env.HMD_URL_PATH || config.urlpath || '' +var port = process.env.PORT || process.env.HMD_PORT || config.port || 3000 +var alloworigin = process.env.HMD_ALLOW_ORIGIN ? process.env.HMD_ALLOW_ORIGIN.split(',') : (config.alloworigin || ['localhost']) -var usessl = !!config.usessl; +var usessl = !!config.usessl var protocolusessl = (usessl === true && typeof process.env.HMD_PROTOCOL_USESSL === 'undefined' && typeof config.protocolusessl === 'undefined') - ? true : (process.env.HMD_PROTOCOL_USESSL ? (process.env.HMD_PROTOCOL_USESSL === 'true') : !!config.protocolusessl); -var urladdport = process.env.HMD_URL_ADDPORT ? (process.env.HMD_URL_ADDPORT === 'true') : !!config.urladdport; + ? true : (process.env.HMD_PROTOCOL_USESSL ? (process.env.HMD_PROTOCOL_USESSL === 'true') : !!config.protocolusessl) +var urladdport = process.env.HMD_URL_ADDPORT ? (process.env.HMD_URL_ADDPORT === 'true') : !!config.urladdport -var usecdn = process.env.HMD_USECDN ? (process.env.HMD_USECDN === 'true') : ((typeof config.usecdn === 'boolean') ? config.usecdn : true); +var usecdn = process.env.HMD_USECDN ? (process.env.HMD_USECDN === 'true') : ((typeof config.usecdn === 'boolean') ? config.usecdn : true) -var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_ANONYMOUS === 'true') : ((typeof config.allowanonymous === 'boolean') ? config.allowanonymous : true); +var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_ANONYMOUS === 'true') : ((typeof config.allowanonymous === 'boolean') ? config.allowanonymous : true) -var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl; +var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl -var permissions = ['editable', 'limited', 'locked', 'protected', 'private']; +var permissions = ['editable', 'limited', 'locked', 'protected', 'private'] if (allowanonymous) { - permissions.unshift('freely'); + permissions.unshift('freely') } -var defaultpermission = process.env.HMD_DEFAULT_PERMISSION || config.defaultpermission; -defaultpermission = permissions.indexOf(defaultpermission) != -1 ? defaultpermission : 'editable'; +var defaultpermission = process.env.HMD_DEFAULT_PERMISSION || config.defaultpermission +defaultpermission = permissions.indexOf(defaultpermission) !== -1 ? defaultpermission : 'editable' // db -var dburl = process.env.HMD_DB_URL || process.env.DATABASE_URL || config.dburl; -var db = config.db || {}; +var dburl = process.env.HMD_DB_URL || process.env.DATABASE_URL || config.dburl +var db = config.db || {} // ssl path -var sslkeypath = (fs.existsSync('/run/secrets/key.pem') ? '/run/secrets/key.pem' : null) || config.sslkeypath || ''; -var sslcertpath = (fs.existsSync('/run/secrets/cert.pem') ? '/run/secrets/cert.pem' : null) || config.sslcertpath || ''; -var sslcapath = (fs.existsSync('/run/secrets/ca.pem') ? '/run/secrets/ca.pem' : null) || config.sslcapath || ''; -var dhparampath = (fs.existsSync('/run/secrets/dhparam.pem') ? '/run/secrets/dhparam.pem' : null) || config.dhparampath || ''; +var sslkeypath = (fs.existsSync('/run/secrets/key.pem') ? '/run/secrets/key.pem' : null) || config.sslkeypath || '' +var sslcertpath = (fs.existsSync('/run/secrets/cert.pem') ? '/run/secrets/cert.pem' : null) || config.sslcertpath || '' +var sslcapath = (fs.existsSync('/run/secrets/ca.pem') ? '/run/secrets/ca.pem' : null) || config.sslcapath || '' +var dhparampath = (fs.existsSync('/run/secrets/dhparam.pem') ? '/run/secrets/dhparam.pem' : null) || config.dhparampath || '' // other path -var tmppath = config.tmppath || './tmp'; -var defaultnotepath = config.defaultnotepath || './public/default.md'; -var docspath = config.docspath || './public/docs'; -var indexpath = config.indexpath || './public/views/index.ejs'; -var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs'; -var errorpath = config.errorpath || './public/views/error.ejs'; -var prettypath = config.prettypath || './public/views/pretty.ejs'; -var slidepath = config.slidepath || './public/views/slide.ejs'; +var tmppath = config.tmppath || './tmp' +var defaultnotepath = config.defaultnotepath || './public/default.md' +var docspath = config.docspath || './public/docs' +var indexpath = config.indexpath || './public/views/index.ejs' +var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs' +var errorpath = config.errorpath || './public/views/error.ejs' +var prettypath = config.prettypath || './public/views/pretty.ejs' +var slidepath = config.slidepath || './public/views/slide.ejs' // session -var sessionname = config.sessionname || 'connect.sid'; -var sessionsecret = handleDockerSecret('sessionsecret') || config.sessionsecret || 'secret'; -var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000; //14 days +var sessionname = config.sessionname || 'connect.sid' +var sessionsecret = handleDockerSecret('sessionsecret') || config.sessionsecret || 'secret' +var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000 // 14 days // static files -var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000; // 1 day +var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000 // 1 day // socket.io -var heartbeatinterval = config.heartbeatinterval || 5000; -var heartbeattimeout = config.heartbeattimeout || 10000; +var heartbeatinterval = config.heartbeatinterval || 5000 +var heartbeattimeout = config.heartbeattimeout || 10000 // document -var documentmaxlength = config.documentmaxlength || 100000; +var documentmaxlength = config.documentmaxlength || 100000 // image upload setting, available options are imgur/s3/filesystem -var imageUploadType = process.env.HMD_IMAGE_UPLOAD_TYPE || config.imageUploadType || 'imgur'; +var imageUploadType = process.env.HMD_IMAGE_UPLOAD_TYPE || config.imageUploadType || 'imgur' -config.s3 = config.s3 || {}; +config.s3 = config.s3 || {} var s3 = { - accessKeyId: handleDockerSecret('s3_acccessKeyId') || process.env.HMD_S3_ACCESS_KEY_ID || config.s3.accessKeyId, - secretAccessKey: handleDockerSecret('s3_secretAccessKey') || process.env.HMD_S3_SECRET_ACCESS_KEY || config.s3.secretAccessKey, - region: process.env.HMD_S3_REGION || config.s3.region + accessKeyId: handleDockerSecret('s3_acccessKeyId') || process.env.HMD_S3_ACCESS_KEY_ID || config.s3.accessKeyId, + secretAccessKey: handleDockerSecret('s3_secretAccessKey') || process.env.HMD_S3_SECRET_ACCESS_KEY || config.s3.secretAccessKey, + region: process.env.HMD_S3_REGION || config.s3.region } -var s3bucket = process.env.HMD_S3_BUCKET || config.s3.bucket; +var s3bucket = process.env.HMD_S3_BUCKET || config.s3.bucket // auth -var facebook = (process.env.HMD_FACEBOOK_CLIENTID && process.env.HMD_FACEBOOK_CLIENTSECRET || fs.existsSync('/run/secrets/facebook_clientID') && fs.existsSync('/run/secrets/facebook_clientSecret')) ? { - clientID: handleDockerSecret('facebook_clientID') || process.env.HMD_FACEBOOK_CLIENTID, - clientSecret: handleDockerSecret('facebook_clientSecret') || process.env.HMD_FACEBOOK_CLIENTSECRET -} : config.facebook || false; -var twitter = (process.env.HMD_TWITTER_CONSUMERKEY && process.env.HMD_TWITTER_CONSUMERSECRET || fs.existsSync('/run/secrets/twitter_consumerKey') && fs.existsSync('/run/secrets/twitter_consumerSecret')) ? { - consumerKey: handleDockerSecret('twitter_consumerKey') || process.env.HMD_TWITTER_CONSUMERKEY, - consumerSecret: handleDockerSecret('twitter_consumerSecret') || process.env.HMD_TWITTER_CONSUMERSECRET -} : config.twitter || false; -var github = (process.env.HMD_GITHUB_CLIENTID && process.env.HMD_GITHUB_CLIENTSECRET || fs.existsSync('/run/secrets/github_clientID') && fs.existsSync('/run/secrets/github_clientSecret')) ? { - clientID: handleDockerSecret('github_clientID') || process.env.HMD_GITHUB_CLIENTID, - clientSecret: handleDockerSecret('github_clientSecret') || process.env.HMD_GITHUB_CLIENTSECRET -} : config.github || false; -var gitlab = (process.env.HMD_GITLAB_CLIENTID && process.env.HMD_GITLAB_CLIENTSECRET || fs.existsSync('/run/secrets/gitlab_clientID') && fs.existsSync('/run/secrets/gitlab_clientSecret')) ? { - baseURL: process.env.HMD_GITLAB_BASEURL, - clientID: handleDockerSecret('gitlab_clientID') || process.env.HMD_GITLAB_CLIENTID, - clientSecret: handleDockerSecret('gitlab_clientSecret') || process.env.HMD_GITLAB_CLIENTSECRET -} : config.gitlab || false; +var facebook = ((process.env.HMD_FACEBOOK_CLIENTID && process.env.HMD_FACEBOOK_CLIENTSECRET) || (fs.existsSync('/run/secrets/facebook_clientID') && fs.existsSync('/run/secrets/facebook_clientSecret'))) ? { + clientID: handleDockerSecret('facebook_clientID') || process.env.HMD_FACEBOOK_CLIENTID, + clientSecret: handleDockerSecret('facebook_clientSecret') || process.env.HMD_FACEBOOK_CLIENTSECRET +} : config.facebook || false +var twitter = ((process.env.HMD_TWITTER_CONSUMERKEY && process.env.HMD_TWITTER_CONSUMERSECRET) || (fs.existsSync('/run/secrets/twitter_consumerKey') && fs.existsSync('/run/secrets/twitter_consumerSecret'))) ? { + consumerKey: handleDockerSecret('twitter_consumerKey') || process.env.HMD_TWITTER_CONSUMERKEY, + consumerSecret: handleDockerSecret('twitter_consumerSecret') || process.env.HMD_TWITTER_CONSUMERSECRET +} : config.twitter || false +var github = ((process.env.HMD_GITHUB_CLIENTID && process.env.HMD_GITHUB_CLIENTSECRET) || (fs.existsSync('/run/secrets/github_clientID') && fs.existsSync('/run/secrets/github_clientSecret'))) ? { + clientID: handleDockerSecret('github_clientID') || process.env.HMD_GITHUB_CLIENTID, + clientSecret: handleDockerSecret('github_clientSecret') || process.env.HMD_GITHUB_CLIENTSECRET +} : config.github || false +var gitlab = ((process.env.HMD_GITLAB_CLIENTID && process.env.HMD_GITLAB_CLIENTSECRET) || (fs.existsSync('/run/secrets/gitlab_clientID') && fs.existsSync('/run/secrets/gitlab_clientSecret'))) ? { + baseURL: process.env.HMD_GITLAB_BASEURL, + clientID: handleDockerSecret('gitlab_clientID') || process.env.HMD_GITLAB_CLIENTID, + clientSecret: handleDockerSecret('gitlab_clientSecret') || process.env.HMD_GITLAB_CLIENTSECRET +} : config.gitlab || false var dropbox = ((process.env.HMD_DROPBOX_CLIENTID && process.env.HMD_DROPBOX_CLIENTSECRET) || (fs.existsSync('/run/secrets/dropbox_clientID') && fs.existsSync('/run/secrets/dropbox_clientSecret'))) ? { - clientID: handleDockerSecret('dropbox_clientID') || process.env.HMD_DROPBOX_CLIENTID, - clientSecret: handleDockerSecret('dropbox_clientSecret') || process.env.HMD_DROPBOX_CLIENTSECRET -} : (config.dropbox && config.dropbox.clientID && config.dropbox.clientSecret && config.dropbox) || false; -var google = ((process.env.HMD_GOOGLE_CLIENTID && process.env.HMD_GOOGLE_CLIENTSECRET) - || (fs.existsSync('/run/secrets/google_clientID') && fs.existsSync('/run/secrets/google_clientSecret'))) ? { - clientID: handleDockerSecret('google_clientID') || process.env.HMD_GOOGLE_CLIENTID, - clientSecret: handleDockerSecret('google_clientSecret') || process.env.HMD_GOOGLE_CLIENTSECRET -} : (config.google && config.google.clientID && config.google.clientSecret && config.google) || false; + clientID: handleDockerSecret('dropbox_clientID') || process.env.HMD_DROPBOX_CLIENTID, + clientSecret: handleDockerSecret('dropbox_clientSecret') || process.env.HMD_DROPBOX_CLIENTSECRET +} : (config.dropbox && config.dropbox.clientID && config.dropbox.clientSecret && config.dropbox) || false +var google = ((process.env.HMD_GOOGLE_CLIENTID && process.env.HMD_GOOGLE_CLIENTSECRET) || + (fs.existsSync('/run/secrets/google_clientID') && fs.existsSync('/run/secrets/google_clientSecret'))) ? { + clientID: handleDockerSecret('google_clientID') || process.env.HMD_GOOGLE_CLIENTID, + clientSecret: handleDockerSecret('google_clientSecret') || process.env.HMD_GOOGLE_CLIENTSECRET + } : (config.google && config.google.clientID && config.google.clientSecret && config.google) || false var ldap = config.ldap || (( process.env.HMD_LDAP_URL || process.env.HMD_LDAP_BINDDN || @@ -123,106 +122,97 @@ var ldap = config.ldap || (( process.env.HMD_LDAP_SEARCHATTRIBUTES || process.env.HMD_LDAP_TLS_CA || process.env.HMD_LDAP_PROVIDERNAME -) ? {} : false); -if (process.env.HMD_LDAP_URL) - ldap.url = process.env.HMD_LDAP_URL; -if (process.env.HMD_LDAP_BINDDN) - ldap.bindDn = process.env.HMD_LDAP_BINDDN; -if (process.env.HMD_LDAP_BINDCREDENTIALS) - ldap.bindCredentials = process.env.HMD_LDAP_BINDCREDENTIALS; -if (process.env.HMD_LDAP_TOKENSECRET) - ldap.tokenSecret = process.env.HMD_LDAP_TOKENSECRET; -if (process.env.HMD_LDAP_SEARCHBASE) - ldap.searchBase = process.env.HMD_LDAP_SEARCHBASE; -if (process.env.HMD_LDAP_SEARCHFILTER) - ldap.searchFilter = process.env.HMD_LDAP_SEARCHFILTER; -if (process.env.HMD_LDAP_SEARCHATTRIBUTES) - ldap.searchAttributes = process.env.HMD_LDAP_SEARCHATTRIBUTES; +) ? {} : false) +if (process.env.HMD_LDAP_URL) { ldap.url = process.env.HMD_LDAP_URL } +if (process.env.HMD_LDAP_BINDDN) { ldap.bindDn = process.env.HMD_LDAP_BINDDN } +if (process.env.HMD_LDAP_BINDCREDENTIALS) { ldap.bindCredentials = process.env.HMD_LDAP_BINDCREDENTIALS } +if (process.env.HMD_LDAP_TOKENSECRET) { ldap.tokenSecret = process.env.HMD_LDAP_TOKENSECRET } +if (process.env.HMD_LDAP_SEARCHBASE) { ldap.searchBase = process.env.HMD_LDAP_SEARCHBASE } +if (process.env.HMD_LDAP_SEARCHFILTER) { ldap.searchFilter = process.env.HMD_LDAP_SEARCHFILTER } +if (process.env.HMD_LDAP_SEARCHATTRIBUTES) { ldap.searchAttributes = process.env.HMD_LDAP_SEARCHATTRIBUTES } if (process.env.HMD_LDAP_TLS_CA) { - var ca = { - ca: process.env.HMD_LDAP_TLS_CA.split(',') - } - ldap.tlsOptions = ldap.tlsOptions ? Object.assign(ldap.tlsOptions, ca) : ca; - if (Array.isArray(ldap.tlsOptions.ca) && ldap.tlsOptions.ca.length > 0) { - var i, len, results; - results = []; - for (i = 0, len = ldap.tlsOptions.ca.length; i < len; i++) { - results.push(fs.readFileSync(ldap.tlsOptions.ca[i], 'utf8')); - } - ldap.tlsOptions.ca = results; + var ca = { + ca: process.env.HMD_LDAP_TLS_CA.split(',') + } + ldap.tlsOptions = ldap.tlsOptions ? Object.assign(ldap.tlsOptions, ca) : ca + if (Array.isArray(ldap.tlsOptions.ca) && ldap.tlsOptions.ca.length > 0) { + var i, len, results + results = [] + for (i = 0, len = ldap.tlsOptions.ca.length; i < len; i++) { + results.push(fs.readFileSync(ldap.tlsOptions.ca[i], 'utf8')) } + ldap.tlsOptions.ca = results + } } if (process.env.HMD_LDAP_PROVIDERNAME) { - ldap.providerName = process.env.HMD_LDAP_PROVIDERNAME; + ldap.providerName = process.env.HMD_LDAP_PROVIDERNAME } -var imgur = handleDockerSecret('imgur_clientid') || process.env.HMD_IMGUR_CLIENTID || config.imgur || false; -var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email; -var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_ALLOW_EMAIL_REGISTER === 'true') : ((typeof config.allowemailregister === 'boolean') ? config.allowemailregister : true); +var imgur = handleDockerSecret('imgur_clientid') || process.env.HMD_IMGUR_CLIENTID || config.imgur || false +var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email +var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_ALLOW_EMAIL_REGISTER === 'true') : ((typeof config.allowemailregister === 'boolean') ? config.allowemailregister : true) -function getserverurl() { - var url = ''; - if (domain) { - var protocol = protocolusessl ? 'https://' : 'http://'; - url = protocol + domain; - if (urladdport && ((usessl && port != 443) || (!usessl && port != 80))) - url += ':' + port; - } - if (urlpath) - url += '/' + urlpath; - return url; +function getserverurl () { + var url = '' + if (domain) { + var protocol = protocolusessl ? 'https://' : 'http://' + url = protocol + domain + if (urladdport && ((usessl && port !== 443) || (!usessl && port !== 80))) { url += ':' + port } + } + if (urlpath) { url += '/' + urlpath } + return url } -var version = '0.5.0'; -var minimumCompatibleVersion = '0.5.0'; -var maintenance = true; -var cwd = path.join(__dirname, '..'); +var version = '0.5.0' +var minimumCompatibleVersion = '0.5.0' +var maintenance = true +var cwd = path.join(__dirname, '..') module.exports = { - version: version, - minimumCompatibleVersion: minimumCompatibleVersion, - maintenance: maintenance, - debug: debug, - urlpath: urlpath, - port: port, - alloworigin: alloworigin, - usessl: usessl, - serverurl: getserverurl(), - usecdn: usecdn, - allowanonymous: allowanonymous, - allowfreeurl: allowfreeurl, - defaultpermission: defaultpermission, - dburl: dburl, - db: db, - sslkeypath: path.join(cwd, sslkeypath), - sslcertpath: path.join(cwd, sslcertpath), - sslcapath: path.join(cwd, sslcapath), - dhparampath: path.join(cwd, dhparampath), - tmppath: path.join(cwd, tmppath), - defaultnotepath: path.join(cwd, defaultnotepath), - docspath: path.join(cwd, docspath), - indexpath: path.join(cwd, indexpath), - hackmdpath: path.join(cwd, hackmdpath), - errorpath: path.join(cwd, errorpath), - prettypath: path.join(cwd, prettypath), - slidepath: path.join(cwd, slidepath), - sessionname: sessionname, - sessionsecret: sessionsecret, - sessionlife: sessionlife, - staticcachetime: staticcachetime, - heartbeatinterval: heartbeatinterval, - heartbeattimeout: heartbeattimeout, - documentmaxlength: documentmaxlength, - facebook: facebook, - twitter: twitter, - github: github, - gitlab: gitlab, - dropbox: dropbox, - google: google, - ldap: ldap, - imgur: imgur, - email: email, - allowemailregister: allowemailregister, - imageUploadType: imageUploadType, - s3: s3, - s3bucket: s3bucket -}; + version: version, + minimumCompatibleVersion: minimumCompatibleVersion, + maintenance: maintenance, + debug: debug, + urlpath: urlpath, + port: port, + alloworigin: alloworigin, + usessl: usessl, + serverurl: getserverurl(), + usecdn: usecdn, + allowanonymous: allowanonymous, + allowfreeurl: allowfreeurl, + defaultpermission: defaultpermission, + dburl: dburl, + db: db, + sslkeypath: path.join(cwd, sslkeypath), + sslcertpath: path.join(cwd, sslcertpath), + sslcapath: path.join(cwd, sslcapath), + dhparampath: path.join(cwd, dhparampath), + tmppath: path.join(cwd, tmppath), + defaultnotepath: path.join(cwd, defaultnotepath), + docspath: path.join(cwd, docspath), + indexpath: path.join(cwd, indexpath), + hackmdpath: path.join(cwd, hackmdpath), + errorpath: path.join(cwd, errorpath), + prettypath: path.join(cwd, prettypath), + slidepath: path.join(cwd, slidepath), + sessionname: sessionname, + sessionsecret: sessionsecret, + sessionlife: sessionlife, + staticcachetime: staticcachetime, + heartbeatinterval: heartbeatinterval, + heartbeattimeout: heartbeattimeout, + documentmaxlength: documentmaxlength, + facebook: facebook, + twitter: twitter, + github: github, + gitlab: gitlab, + dropbox: dropbox, + google: google, + ldap: ldap, + imgur: imgur, + email: email, + allowemailregister: allowemailregister, + imageUploadType: imageUploadType, + s3: s3, + s3bucket: s3bucket +} diff --git a/lib/history.js b/lib/history.js index e7fb308..69337dc 100644 --- a/lib/history.js +++ b/lib/history.js @@ -1,172 +1,175 @@ -//history -//external modules -var async = require('async'); +// history +// external modules -//core -var config = require("./config.js"); -var logger = require("./logger.js"); -var response = require("./response.js"); -var models = require("./models"); +// core +var config = require('./config.js') +var logger = require('./logger.js') +var response = require('./response.js') +var models = require('./models') -//public +// public var History = { - historyGet: historyGet, - historyPost: historyPost, - historyDelete: historyDelete, - updateHistory: updateHistory -}; - -function getHistory(userid, callback) { - models.User.findOne({ - where: { - id: userid - } - }).then(function (user) { - if (!user) - return callback(null, null); - var history = {}; - if (user.history) - history = parseHistoryToObject(JSON.parse(user.history)); - if (config.debug) - logger.info('read history success: ' + user.id); - return callback(null, history); - }).catch(function (err) { - logger.error('read history failed: ' + err); - return callback(err, null); - }); + historyGet: historyGet, + historyPost: historyPost, + historyDelete: historyDelete, + updateHistory: updateHistory } -function setHistory(userid, history, callback) { - models.User.update({ - history: JSON.stringify(parseHistoryToArray(history)) - }, { - where: { - id: userid - } - }).then(function (count) { - return callback(null, count); - }).catch(function (err) { - logger.error('set history failed: ' + err); - return callback(err, null); - }); -} - -function updateHistory(userid, noteId, document, time) { - if (userid && noteId && typeof document !== 'undefined') { - getHistory(userid, function (err, history) { - if (err || !history) return; - if (!history[noteId]) { - history[noteId] = {}; - } - var noteHistory = history[noteId]; - var noteInfo = models.Note.parseNoteInfo(document); - noteHistory.id = noteId; - noteHistory.text = noteInfo.title; - noteHistory.time = time || Date.now(); - noteHistory.tags = noteInfo.tags; - setHistory(userid, history, function (err, count) { - return; - }); - }); +function getHistory (userid, callback) { + models.User.findOne({ + where: { + id: userid } -} - -function parseHistoryToArray(history) { - var _history = []; - Object.keys(history).forEach(function (key) { - var item = history[key]; - _history.push(item); - }); - return _history; -} - -function parseHistoryToObject(history) { - var _history = {}; - for (var i = 0, l = history.length; i < l; i++) { - var item = history[i]; - _history[item.id] = item; + }).then(function (user) { + if (!user) { + return callback(null, null) } - return _history; + var history = {} + if (user.history) { + history = parseHistoryToObject(JSON.parse(user.history)) + } + if (config.debug) { + logger.info('read history success: ' + user.id) + } + return callback(null, history) + }).catch(function (err) { + logger.error('read history failed: ' + err) + return callback(err, null) + }) } -function historyGet(req, res) { - if (req.isAuthenticated()) { - getHistory(req.user.id, function (err, history) { - if (err) return response.errorInternalError(res); - if (!history) return response.errorNotFound(res); - res.send({ - history: parseHistoryToArray(history) - }); - }); +function setHistory (userid, history, callback) { + models.User.update({ + history: JSON.stringify(parseHistoryToArray(history)) + }, { + where: { + id: userid + } + }).then(function (count) { + return callback(null, count) + }).catch(function (err) { + logger.error('set history failed: ' + err) + return callback(err, null) + }) +} + +function updateHistory (userid, noteId, document, time) { + if (userid && noteId && typeof document !== 'undefined') { + getHistory(userid, function (err, history) { + if (err || !history) return + if (!history[noteId]) { + history[noteId] = {} + } + var noteHistory = history[noteId] + var noteInfo = models.Note.parseNoteInfo(document) + noteHistory.id = noteId + noteHistory.text = noteInfo.title + noteHistory.time = time || Date.now() + noteHistory.tags = noteInfo.tags + setHistory(userid, history, function (err, count) { + if (err) { + logger.log(err) + } + }) + }) + } +} + +function parseHistoryToArray (history) { + var _history = [] + Object.keys(history).forEach(function (key) { + var item = history[key] + _history.push(item) + }) + return _history +} + +function parseHistoryToObject (history) { + var _history = {} + for (var i = 0, l = history.length; i < l; i++) { + var item = history[i] + _history[item.id] = item + } + return _history +} + +function historyGet (req, res) { + if (req.isAuthenticated()) { + getHistory(req.user.id, function (err, history) { + if (err) return response.errorInternalError(res) + if (!history) return response.errorNotFound(res) + res.send({ + history: parseHistoryToArray(history) + }) + }) + } else { + return response.errorForbidden(res) + } +} + +function historyPost (req, res) { + if (req.isAuthenticated()) { + var noteId = req.params.noteId + if (!noteId) { + if (typeof req.body['history'] === 'undefined') return response.errorBadRequest(res) + if (config.debug) { logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history) } + try { + var history = JSON.parse(req.body.history) + } catch (err) { + return response.errorBadRequest(res) + } + if (Array.isArray(history)) { + setHistory(req.user.id, history, function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) + } else { + return response.errorBadRequest(res) + } } else { - return response.errorForbidden(res); - } -} - -function historyPost(req, res) { - if (req.isAuthenticated()) { - var noteId = req.params.noteId; - if (!noteId) { - if (typeof req.body['history'] === 'undefined') return response.errorBadRequest(res); - if (config.debug) - logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history); - try { - var history = JSON.parse(req.body.history); - } catch (err) { - return response.errorBadRequest(res); - } - if (Array.isArray(history)) { - setHistory(req.user.id, history, function (err, count) { - if (err) return response.errorInternalError(res); - res.end(); - }); - } else { - return response.errorBadRequest(res); - } + if (typeof req.body['pinned'] === 'undefined') return response.errorBadRequest(res) + getHistory(req.user.id, function (err, history) { + if (err) return response.errorInternalError(res) + if (!history) return response.errorNotFound(res) + if (!history[noteId]) return response.errorNotFound(res) + if (req.body.pinned === 'true' || req.body.pinned === 'false') { + history[noteId].pinned = (req.body.pinned === 'true') + setHistory(req.user.id, history, function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) } else { - if (typeof req.body['pinned'] === 'undefined') return response.errorBadRequest(res); - getHistory(req.user.id, function (err, history) { - if (err) return response.errorInternalError(res); - if (!history) return response.errorNotFound(res); - if (!history[noteId]) return response.errorNotFound(res); - if (req.body.pinned === 'true' || req.body.pinned === 'false') { - history[noteId].pinned = (req.body.pinned === 'true'); - setHistory(req.user.id, history, function (err, count) { - if (err) return response.errorInternalError(res); - res.end(); - }); - } else { - return response.errorBadRequest(res); - } - }); + return response.errorBadRequest(res) } - } else { - return response.errorForbidden(res); + }) } + } else { + return response.errorForbidden(res) + } } -function historyDelete(req, res) { - if (req.isAuthenticated()) { - var noteId = req.params.noteId; - if (!noteId) { - setHistory(req.user.id, [], function (err, count) { - if (err) return response.errorInternalError(res); - res.end(); - }); - } else { - getHistory(req.user.id, function (err, history) { - if (err) return response.errorInternalError(res); - if (!history) return response.errorNotFound(res); - delete history[noteId]; - setHistory(req.user.id, history, function (err, count) { - if (err) return response.errorInternalError(res); - res.end(); - }); - }); - } +function historyDelete (req, res) { + if (req.isAuthenticated()) { + var noteId = req.params.noteId + if (!noteId) { + setHistory(req.user.id, [], function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) } else { - return response.errorForbidden(res); + getHistory(req.user.id, function (err, history) { + if (err) return response.errorInternalError(res) + if (!history) return response.errorNotFound(res) + delete history[noteId] + setHistory(req.user.id, history, function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) + }) } + } else { + return response.errorForbidden(res) + } } -module.exports = History; \ No newline at end of file +module.exports = History diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js index 3afa03f..92bd36e 100644 --- a/lib/letter-avatars.js +++ b/lib/letter-avatars.js @@ -1,25 +1,23 @@ -"use strict"; - // external modules -var randomcolor = require('randomcolor'); +var randomcolor = require('randomcolor') // core -module.exports = function(name) { - var color = randomcolor({ - seed: name, - luminosity: 'dark' - }); - var letter = name.substring(0, 1).toUpperCase(); +module.exports = function (name) { + var color = randomcolor({ + seed: name, + luminosity: 'dark' + }) + var letter = name.substring(0, 1).toUpperCase() - var svg = ''; - svg += ''; - svg += ''; - svg += ''; - svg += ''; - svg += '' + letter + ''; - svg += ''; - svg += ''; - svg += ''; + var svg = '' + svg += '' + svg += '' + svg += '' + svg += '' + svg += '' + letter + '' + svg += '' + svg += '' + svg += '' - return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64'); -}; + return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64') +} diff --git a/lib/logger.js b/lib/logger.js index 61299c1..23e302d 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,22 +1,22 @@ -var winston = require('winston'); -winston.emitErrs = true; +var winston = require('winston') +winston.emitErrs = true var logger = new winston.Logger({ - transports: [ - new winston.transports.Console({ - level: 'debug', - handleExceptions: true, - json: false, - colorize: true, - timestamp: true - }) - ], - exitOnError: false -}); + transports: [ + new winston.transports.Console({ + level: 'debug', + handleExceptions: true, + json: false, + colorize: true, + timestamp: true + }) + ], + exitOnError: false +}) -module.exports = logger; +module.exports = logger module.exports.stream = { - write: function(message, encoding){ - logger.info(message); - } -}; \ No newline at end of file + write: function (message, encoding) { + logger.info(message) + } +} diff --git a/lib/migrations/20160515114000-user-add-tokens.js b/lib/migrations/20160515114000-user-add-tokens.js index 3af490a..20c0e03 100644 --- a/lib/migrations/20160515114000-user-add-tokens.js +++ b/lib/migrations/20160515114000-user-add-tokens.js @@ -1,15 +1,11 @@ -"use strict"; - module.exports = { - up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING); - queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING); - return; - }, + up: function (queryInterface, Sequelize) { + queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING) + queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING) + }, - down: function (queryInterface, Sequelize) { - queryInterface.removeColumn('Users', 'accessToken'); - queryInterface.removeColumn('Users', 'refreshToken'); - return; - } -}; \ No newline at end of file + down: function (queryInterface, Sequelize) { + queryInterface.removeColumn('Users', 'accessToken') + queryInterface.removeColumn('Users', 'refreshToken') + } +} diff --git a/lib/migrations/20160607060246-support-revision.js b/lib/migrations/20160607060246-support-revision.js index fa647d9..618bb4d 100644 --- a/lib/migrations/20160607060246-support-revision.js +++ b/lib/migrations/20160607060246-support-revision.js @@ -1,8 +1,6 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE); + queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE) queryInterface.createTable('Revisions', { id: { type: Sequelize.UUID, @@ -15,13 +13,11 @@ module.exports = { length: Sequelize.INTEGER, createdAt: Sequelize.DATE, updatedAt: Sequelize.DATE - }); - return; + }) }, down: function (queryInterface, Sequelize) { - queryInterface.dropTable('Revisions'); - queryInterface.removeColumn('Notes', 'savedAt'); - return; + queryInterface.dropTable('Revisions') + queryInterface.removeColumn('Notes', 'savedAt') } -}; +} diff --git a/lib/migrations/20160703062241-support-authorship.js b/lib/migrations/20160703062241-support-authorship.js index 239327e..98381d4 100644 --- a/lib/migrations/20160703062241-support-authorship.js +++ b/lib/migrations/20160703062241-support-authorship.js @@ -1,9 +1,7 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT); - queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT); + queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT) + queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT) queryInterface.createTable('Authors', { id: { type: Sequelize.INTEGER, @@ -15,14 +13,12 @@ module.exports = { userId: Sequelize.UUID, createdAt: Sequelize.DATE, updatedAt: Sequelize.DATE - }); - return; + }) }, down: function (queryInterface, Sequelize) { - queryInterface.dropTable('Authors'); - queryInterface.removeColumn('Revisions', 'authorship'); - queryInterface.removeColumn('Notes', 'authorship'); - return; + queryInterface.dropTable('Authors') + queryInterface.removeColumn('Revisions', 'authorship') + queryInterface.removeColumn('Notes', 'authorship') } -}; +} diff --git a/lib/migrations/20161009040430-support-delete-note.js b/lib/migrations/20161009040430-support-delete-note.js index 92ff6f7..984920b 100644 --- a/lib/migrations/20161009040430-support-delete-note.js +++ b/lib/migrations/20161009040430-support-delete-note.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE); + queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE) }, down: function (queryInterface, Sequelize) { - queryInterface.removeColumn('Notes', 'deletedAt'); + queryInterface.removeColumn('Notes', 'deletedAt') } -}; +} diff --git a/lib/migrations/20161201050312-support-email-signin.js b/lib/migrations/20161201050312-support-email-signin.js index b5aaf77..a97d3be 100644 --- a/lib/migrations/20161201050312-support-email-signin.js +++ b/lib/migrations/20161201050312-support-email-signin.js @@ -1,13 +1,11 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Users', 'email', Sequelize.TEXT); - queryInterface.addColumn('Users', 'password', Sequelize.TEXT); + queryInterface.addColumn('Users', 'email', Sequelize.TEXT) + queryInterface.addColumn('Users', 'password', Sequelize.TEXT) }, down: function (queryInterface, Sequelize) { - queryInterface.removeColumn('Users', 'email'); - queryInterface.removeColumn('Users', 'password'); + queryInterface.removeColumn('Users', 'email') + queryInterface.removeColumn('Users', 'password') } -}; +} diff --git a/lib/models/author.js b/lib/models/author.js index 0b0f149..5e39c34 100644 --- a/lib/models/author.js +++ b/lib/models/author.js @@ -1,43 +1,37 @@ -"use strict"; - // external modules -var Sequelize = require("sequelize"); - -// core -var logger = require("../logger.js"); +var Sequelize = require('sequelize') module.exports = function (sequelize, DataTypes) { - var Author = sequelize.define("Author", { - id: { - type: Sequelize.INTEGER, - primaryKey: true, - autoIncrement: true - }, - color: { - type: DataTypes.STRING - } - }, { - indexes: [ - { - unique: true, - fields: ['noteId', 'userId'] - } - ], - classMethods: { - associate: function (models) { - Author.belongsTo(models.Note, { - foreignKey: "noteId", - as: "note", - constraints: false - }); - Author.belongsTo(models.User, { - foreignKey: "userId", - as: "user", - constraints: false - }); - } - } - }); - - return Author; -}; \ No newline at end of file + var Author = sequelize.define('Author', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true + }, + color: { + type: DataTypes.STRING + } + }, { + indexes: [ + { + unique: true, + fields: ['noteId', 'userId'] + } + ], + classMethods: { + associate: function (models) { + Author.belongsTo(models.Note, { + foreignKey: 'noteId', + as: 'note', + constraints: false + }) + Author.belongsTo(models.User, { + foreignKey: 'userId', + as: 'user', + constraints: false + }) + } + } + }) + return Author +} diff --git a/lib/models/index.js b/lib/models/index.js index e83956e..96babc2 100644 --- a/lib/models/index.js +++ b/lib/models/index.js @@ -1,57 +1,55 @@ -"use strict"; - // external modules -var fs = require("fs"); -var path = require("path"); -var Sequelize = require("sequelize"); +var fs = require('fs') +var path = require('path') +var Sequelize = require('sequelize') // core -var config = require('../config.js'); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -var dbconfig = config.db; -dbconfig.logging = config.debug ? logger.info : false; +var dbconfig = config.db +dbconfig.logging = config.debug ? logger.info : false -var sequelize = null; +var sequelize = null // Heroku specific -if (config.dburl) - sequelize = new Sequelize(config.dburl, dbconfig); -else - sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig); +if (config.dburl) { + sequelize = new Sequelize(config.dburl, dbconfig) +} else { + sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig) +} // [Postgres] Handling NULL bytes // https://github.com/sequelize/sequelize/issues/6485 -function stripNullByte(value) { - return value ? value.replace(/\u0000/g, "") : value; +function stripNullByte (value) { + return value ? value.replace(/\u0000/g, '') : value } -sequelize.stripNullByte = stripNullByte; +sequelize.stripNullByte = stripNullByte -function processData(data, _default, process) { - if (data === undefined) return data; - else return data === null ? _default : (process ? process(data) : data); +function processData (data, _default, process) { + if (data === undefined) return data + else return data === null ? _default : (process ? process(data) : data) } -sequelize.processData = processData; +sequelize.processData = processData -var db = {}; +var db = {} -fs - .readdirSync(__dirname) +fs.readdirSync(__dirname) .filter(function (file) { - return (file.indexOf(".") !== 0) && (file !== "index.js"); + return (file.indexOf('.') !== 0) && (file !== 'index.js') }) .forEach(function (file) { - var model = sequelize.import(path.join(__dirname, file)); - db[model.name] = model; - }); + var model = sequelize.import(path.join(__dirname, file)) + db[model.name] = model + }) Object.keys(db).forEach(function (modelName) { - if ("associate" in db[modelName]) { - db[modelName].associate(db); - } -}); + if ('associate' in db[modelName]) { + db[modelName].associate(db) + } +}) -db.sequelize = sequelize; -db.Sequelize = Sequelize; +db.sequelize = sequelize +db.Sequelize = Sequelize -module.exports = db; +module.exports = db diff --git a/lib/models/note.js b/lib/models/note.js index 8b38d3f..bef9ee2 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -1,535 +1,524 @@ -"use strict"; - // external modules -var fs = require('fs'); -var path = require('path'); -var LZString = require('lz-string'); -var md = require('markdown-it')(); -var metaMarked = require('meta-marked'); -var cheerio = require('cheerio'); -var shortId = require('shortid'); -var Sequelize = require("sequelize"); -var async = require('async'); -var moment = require('moment'); -var DiffMatchPatch = require('diff-match-patch'); -var dmp = new DiffMatchPatch(); -var S = require('string'); +var fs = require('fs') +var path = require('path') +var LZString = require('lz-string') +var md = require('markdown-it')() +var metaMarked = require('meta-marked') +var cheerio = require('cheerio') +var shortId = require('shortid') +var Sequelize = require('sequelize') +var async = require('async') +var moment = require('moment') +var DiffMatchPatch = require('diff-match-patch') +var dmp = new DiffMatchPatch() +var S = require('string') // core -var config = require("../config.js"); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -//ot -var ot = require("../ot/index.js"); +// ot +var ot = require('../ot/index.js') // permission types -var permissionTypes = ["freely", "editable", "limited", "locked", "protected", "private"]; +var permissionTypes = ['freely', 'editable', 'limited', 'locked', 'protected', 'private'] module.exports = function (sequelize, DataTypes) { - var Note = sequelize.define("Note", { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - shortid: { - type: DataTypes.STRING, - unique: true, - allowNull: false, - defaultValue: shortId.generate - }, - alias: { - type: DataTypes.STRING, - unique: true - }, - permission: { - type: DataTypes.ENUM, - values: permissionTypes - }, - viewcount: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0 - }, - title: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('title'), ""); - }, - set: function (value) { - this.setDataValue('title', sequelize.stripNullByte(value)); - } - }, - content: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('content'), ""); - }, - set: function (value) { - this.setDataValue('content', sequelize.stripNullByte(value)); - } - }, - authorship: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse); - }, - set: function (value) { - this.setDataValue('authorship', JSON.stringify(value)); - } - }, - lastchangeAt: { - type: DataTypes.DATE - }, - savedAt: { - type: DataTypes.DATE + var Note = sequelize.define('Note', { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + shortid: { + type: DataTypes.STRING, + unique: true, + allowNull: false, + defaultValue: shortId.generate + }, + alias: { + type: DataTypes.STRING, + unique: true + }, + permission: { + type: DataTypes.ENUM, + values: permissionTypes + }, + viewcount: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + title: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('title'), '') + }, + set: function (value) { + this.setDataValue('title', sequelize.stripNullByte(value)) + } + }, + content: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('content'), '') + }, + set: function (value) { + this.setDataValue('content', sequelize.stripNullByte(value)) + } + }, + authorship: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse) + }, + set: function (value) { + this.setDataValue('authorship', JSON.stringify(value)) + } + }, + lastchangeAt: { + type: DataTypes.DATE + }, + savedAt: { + type: DataTypes.DATE + } + }, { + paranoid: true, + classMethods: { + associate: function (models) { + Note.belongsTo(models.User, { + foreignKey: 'ownerId', + as: 'owner', + constraints: false + }) + Note.belongsTo(models.User, { + foreignKey: 'lastchangeuserId', + as: 'lastchangeuser', + constraints: false + }) + Note.hasMany(models.Revision, { + foreignKey: 'noteId', + constraints: false + }) + Note.hasMany(models.Author, { + foreignKey: 'noteId', + as: 'authors', + constraints: false + }) + }, + checkFileExist: function (filePath) { + try { + return fs.statSync(filePath).isFile() + } catch (err) { + return false } - }, { - paranoid: true, - classMethods: { - associate: function (models) { - Note.belongsTo(models.User, { - foreignKey: "ownerId", - as: "owner", - constraints: false - }); - Note.belongsTo(models.User, { - foreignKey: "lastchangeuserId", - as: "lastchangeuser", - constraints: false - }); - Note.hasMany(models.Revision, { - foreignKey: "noteId", - constraints: false - }); - Note.hasMany(models.Author, { - foreignKey: "noteId", - as: "authors", - constraints: false - }); - }, - checkFileExist: function (filePath) { - try { - return fs.statSync(filePath).isFile(); - } catch (err) { - return false; - } - }, - checkNoteIdValid: function (id) { - var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - var result = id.match(uuidRegex); - if (result && result.length == 1) - return true; - else - return false; - }, - parseNoteId: function (noteId, callback) { - async.series({ - parseNoteIdByAlias: function (_callback) { - // try to parse note id by alias (e.g. doc) - Note.findOne({ - where: { - alias: noteId - } + }, + checkNoteIdValid: function (id) { + var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + var result = id.match(uuidRegex) + if (result && result.length === 1) { return true } else { return false } + }, + parseNoteId: function (noteId, callback) { + async.series({ + parseNoteIdByAlias: function (_callback) { + // try to parse note id by alias (e.g. doc) + Note.findOne({ + where: { + alias: noteId + } + }).then(function (note) { + if (note) { + let filePath = path.join(config.docspath, noteId + '.md') + if (Note.checkFileExist(filePath)) { + // if doc in filesystem have newer modified time than last change time + // then will update the doc in db + var fsModifiedTime = moment(fs.statSync(filePath).mtime) + var dbModifiedTime = moment(note.lastchangeAt || note.createdAt) + var body = fs.readFileSync(filePath, 'utf8') + var contentLength = body.length + var title = Note.parseNoteTitle(body) + if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { + note.update({ + title: title, + content: body, + lastchangeAt: fsModifiedTime + }).then(function (note) { + sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { + if (err) return _callback(err, null) + // update authorship on after making revision of docs + var patch = dmp.patch_fromText(revision.patch) + var operations = Note.transformPatchToOperations(patch, contentLength) + var authorship = note.authorship + for (let i = 0; i < operations.length; i++) { + authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship) + } + note.update({ + authorship: JSON.stringify(authorship) }).then(function (note) { - if (note) { - var filePath = path.join(config.docspath, noteId + '.md'); - if (Note.checkFileExist(filePath)) { - // if doc in filesystem have newer modified time than last change time - // then will update the doc in db - var fsModifiedTime = moment(fs.statSync(filePath).mtime); - var dbModifiedTime = moment(note.lastchangeAt || note.createdAt); - var body = fs.readFileSync(filePath, 'utf8'); - var contentLength = body.length; - var title = Note.parseNoteTitle(body); - if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { - note.update({ - title: title, - content: body, - lastchangeAt: fsModifiedTime - }).then(function (note) { - sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { - if (err) return _callback(err, null); - // update authorship on after making revision of docs - var patch = dmp.patch_fromText(revision.patch); - var operations = Note.transformPatchToOperations(patch, contentLength); - var authorship = note.authorship; - for (var i = 0; i < operations.length; i++) { - authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship); - } - note.update({ - authorship: JSON.stringify(authorship) - }).then(function (note) { - return callback(null, note.id); - }).catch(function (err) { - return _callback(err, null); - }); - }); - }).catch(function (err) { - return _callback(err, null); - }); - } else { - return callback(null, note.id); - } - } else { - return callback(null, note.id); - } - } else { - var filePath = path.join(config.docspath, noteId + '.md'); - if (Note.checkFileExist(filePath)) { - Note.create({ - alias: noteId, - owner: null, - permission: 'locked' - }).then(function (note) { - return callback(null, note.id); - }).catch(function (err) { - return _callback(err, null); - }); - } else { - return _callback(null, null); - } - } + return callback(null, note.id) }).catch(function (err) { - return _callback(err, null); - }); - }, - parseNoteIdByLZString: function (_callback) { - // try to parse note id by LZString Base64 - try { - var id = LZString.decompressFromBase64(noteId); - if (id && Note.checkNoteIdValid(id)) - return callback(null, id); - else - return _callback(null, null); - } catch (err) { - return _callback(err, null); - } - }, - parseNoteIdByShortId: function (_callback) { - // try to parse note id by shortId - try { - if (shortId.isValid(noteId)) { - Note.findOne({ - where: { - shortid: noteId - } - }).then(function (note) { - if (!note) return _callback(null, null); - return callback(null, note.id); - }).catch(function (err) { - return _callback(err, null); - }); - } else { - return _callback(null, null); - } - } catch (err) { - return _callback(err, null); - } - } - }, function (err, result) { - if (err) { - logger.error(err); - return callback(err, null); - } - return callback(null, null); - }); - }, - parseNoteInfo: function (body) { - var parsed = Note.extractMeta(body); - var $ = cheerio.load(md.render(parsed.markdown)); - return { - title: Note.extractNoteTitle(parsed.meta, $), - tags: Note.extractNoteTags(parsed.meta, $) - }; - }, - parseNoteTitle: function (body) { - var parsed = Note.extractMeta(body); - var $ = cheerio.load(md.render(parsed.markdown)); - return Note.extractNoteTitle(parsed.meta, $); - }, - extractNoteTitle: function (meta, $) { - var title = ""; - if (meta.title && (typeof meta.title == "string" || typeof meta.title == "number")) { - title = meta.title; + return _callback(err, null) + }) + }) + }).catch(function (err) { + return _callback(err, null) + }) + } else { + return callback(null, note.id) + } } else { - var h1s = $("h1"); - if (h1s.length > 0 && h1s.first().text().split('\n').length == 1) - title = S(h1s.first().text()).stripTags().s; + return callback(null, note.id) } - if (!title) title = "Untitled"; - return title; - }, - generateDescription: function (markdown) { - return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' '); - }, - decodeTitle: function (title) { - return title ? title : 'Untitled'; - }, - generateWebTitle: function (title) { - title = !title || title == "Untitled" ? "HackMD - Collaborative markdown notes" : title + " - HackMD"; - return title; - }, - extractNoteTags: function (meta, $) { - var tags = []; - var rawtags = []; - if (meta.tags && (typeof meta.tags == "string" || typeof meta.tags == "number")) { - var metaTags = ('' + meta.tags).split(','); - for (var i = 0; i < metaTags.length; i++) { - var text = metaTags[i].trim(); - if (text) rawtags.push(text); - } + } else { + var filePath = path.join(config.docspath, noteId + '.md') + if (Note.checkFileExist(filePath)) { + Note.create({ + alias: noteId, + owner: null, + permission: 'locked' + }).then(function (note) { + return callback(null, note.id) + }).catch(function (err) { + return _callback(err, null) + }) } else { - var h6s = $("h6"); - h6s.each(function (key, value) { - if (/^tags/gmi.test($(value).text())) { - var codes = $(value).find("code"); - for (var i = 0; i < codes.length; i++) { - var text = S($(codes[i]).text().trim()).stripTags().s; - if (text) rawtags.push(text); - } - } - }); + return _callback(null, null) } - for (var i = 0; i < rawtags.length; i++) { - var found = false; - for (var j = 0; j < tags.length; j++) { - if (tags[j] == rawtags[i]) { - found = true; - break; - } - } - if (!found) - tags.push(rawtags[i]); - } - return tags; - }, - extractMeta: function (content) { - try { - var obj = metaMarked(content); - if (!obj.markdown) obj.markdown = ""; - if (!obj.meta) obj.meta = {}; - } catch (err) { - var obj = { - markdown: content, - meta: {} - }; - } - return obj; - }, - parseMeta: function (meta) { - var _meta = {}; - if (meta) { - if (meta.title && (typeof meta.title == "string" || typeof meta.title == "number")) - _meta.title = meta.title; - if (meta.description && (typeof meta.description == "string" || typeof meta.description == "number")) - _meta.description = meta.description; - if (meta.robots && (typeof meta.robots == "string" || typeof meta.robots == "number")) - _meta.robots = meta.robots; - if (meta.GA && (typeof meta.GA == "string" || typeof meta.GA == "number")) - _meta.GA = meta.GA; - if (meta.disqus && (typeof meta.disqus == "string" || typeof meta.disqus == "number")) - _meta.disqus = meta.disqus; - if (meta.slideOptions && (typeof meta.slideOptions == "object")) - _meta.slideOptions = meta.slideOptions; - } - return _meta; - }, - updateAuthorshipByOperation: function (operation, userId, authorships) { - var index = 0; - var timestamp = Date.now(); - for (var i = 0; i < operation.length; i++) { - var op = operation[i]; - if (ot.TextOperation.isRetain(op)) { - index += op; - } else if (ot.TextOperation.isInsert(op)) { - var opStart = index; - var opEnd = index + op.length; - var inserted = false; - // authorship format: [userId, startPos, endPos, createdAt, updatedAt] - if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]); - else { - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - if (!inserted) { - var nextAuthorship = authorships[j + 1] || -1; - if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) { - if (authorship[1] < opStart && authorship[2] > opStart) { - // divide - var postLength = authorship[2] - opStart; - authorship[2] = opStart; - authorship[4] = timestamp; - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]); - authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]); - j += 2; - inserted = true; - } else if (authorship[1] >= opStart) { - authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]); - j += 1; - inserted = true; - } else if (authorship[2] <= opStart) { - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]); - j += 1; - inserted = true; - } - } - } - if (authorship[1] >= opStart) { - authorship[1] += op.length; - authorship[2] += op.length; - } - } - } - index += op.length; - } else if (ot.TextOperation.isDelete(op)) { - var opStart = index; - var opEnd = index - op; - if (operation.length == 1) { - authorships = []; - } else if (authorships.length > 0) { - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) { - authorships.splice(j, 1); - j -= 1; - } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) { - authorship[2] += op; - authorship[4] = timestamp; - } else if (authorship[2] >= opStart && authorship[2] <= opEnd) { - authorship[2] = opStart; - authorship[4] = timestamp; - } else if (authorship[1] >= opStart && authorship[1] <= opEnd) { - authorship[1] = opEnd; - authorship[4] = timestamp; - } - if (authorship[1] >= opEnd) { - authorship[1] += op; - authorship[2] += op; - } - } - } - index += op; - } - } - // merge - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - for (var k = j + 1; k < authorships.length; k++) { - var nextAuthorship = authorships[k]; - if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) { - var minTimestamp = Math.min(authorship[3], nextAuthorship[3]); - var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]); - authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]); - authorships.splice(k, 1); - j -= 1; - break; - } - } - } - // clear - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - if (!authorship[0]) { - authorships.splice(j, 1); - j -= 1; - } - } - return authorships; - }, - transformPatchToOperations: function (patch, contentLength) { - var operations = []; - if (patch.length > 0) { - // calculate original content length - for (var j = patch.length - 1; j >= 0; j--) { - var p = patch[j]; - for (var i = 0; i < p.diffs.length; i++) { - var diff = p.diffs[i]; - switch(diff[0]) { - case 1: // insert - contentLength -= diff[1].length; - break; - case -1: // delete - contentLength += diff[1].length; - break; - } - } - } - // generate operations - var bias = 0; - var lengthBias = 0; - for (var j = 0; j < patch.length; j++) { - var operation = []; - var p = patch[j]; - var currIndex = p.start1; - var currLength = contentLength - bias; - for (var i = 0; i < p.diffs.length; i++) { - var diff = p.diffs[i]; - switch(diff[0]) { - case 0: // retain - if (i == 0) // first - operation.push(currIndex + diff[1].length); - else if (i != p.diffs.length - 1) // mid - operation.push(diff[1].length); - else // last - operation.push(currLength + lengthBias - currIndex); - currIndex += diff[1].length; - break; - case 1: // insert - operation.push(diff[1]); - lengthBias += diff[1].length; - currIndex += diff[1].length; - break; - case -1: // delete - operation.push(-diff[1].length); - bias += diff[1].length; - currIndex += diff[1].length; - break; - } - } - operations.push(operation); - } - } - return operations; + } + }).catch(function (err) { + return _callback(err, null) + }) + }, + parseNoteIdByLZString: function (_callback) { + // try to parse note id by LZString Base64 + try { + var id = LZString.decompressFromBase64(noteId) + if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) } + } catch (err) { + return _callback(err, null) } - }, - hooks: { - beforeCreate: function (note, options, callback) { - // if no content specified then use default note - if (!note.content) { - var body = null; - var filePath = null; - if (!note.alias) { - filePath = config.defaultnotepath; - } else { - filePath = path.join(config.docspath, note.alias + '.md'); - } - if (Note.checkFileExist(filePath)) { - var fsCreatedTime = moment(fs.statSync(filePath).ctime); - body = fs.readFileSync(filePath, 'utf8'); - note.title = Note.parseNoteTitle(body); - note.content = body; - if (filePath !== config.defaultnotepath) { - note.createdAt = fsCreatedTime; - } - } - } - // if no permission specified and have owner then give default permission in config, else default permission is freely - if (!note.permission) { - if (note.ownerId) { - note.permission = config.defaultpermission; - } else { - note.permission = "freely"; - } - } - return callback(null, note); - }, - afterCreate: function (note, options, callback) { - sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { - callback(err, note); - }); + }, + parseNoteIdByShortId: function (_callback) { + // try to parse note id by shortId + try { + if (shortId.isValid(noteId)) { + Note.findOne({ + where: { + shortid: noteId + } + }).then(function (note) { + if (!note) return _callback(null, null) + return callback(null, note.id) + }).catch(function (err) { + return _callback(err, null) + }) + } else { + return _callback(null, null) + } + } catch (err) { + return _callback(err, null) } + } + }, function (err, result) { + if (err) { + logger.error(err) + return callback(err, null) + } + return callback(null, null) + }) + }, + parseNoteInfo: function (body) { + var parsed = Note.extractMeta(body) + var $ = cheerio.load(md.render(parsed.markdown)) + return { + title: Note.extractNoteTitle(parsed.meta, $), + tags: Note.extractNoteTags(parsed.meta, $) } - }); + }, + parseNoteTitle: function (body) { + var parsed = Note.extractMeta(body) + var $ = cheerio.load(md.render(parsed.markdown)) + return Note.extractNoteTitle(parsed.meta, $) + }, + extractNoteTitle: function (meta, $) { + var title = '' + if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { + title = meta.title + } else { + var h1s = $('h1') + if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = S(h1s.first().text()).stripTags().s } + } + if (!title) title = 'Untitled' + return title + }, + generateDescription: function (markdown) { + return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ') + }, + decodeTitle: function (title) { + return title || 'Untitled' + }, + generateWebTitle: function (title) { + title = !title || title === 'Untitled' ? 'HackMD - Collaborative markdown notes' : title + ' - HackMD' + return title + }, + extractNoteTags: function (meta, $) { + var tags = [] + var rawtags = [] + if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) { + var metaTags = ('' + meta.tags).split(',') + for (let i = 0; i < metaTags.length; i++) { + var text = metaTags[i].trim() + if (text) rawtags.push(text) + } + } else { + var h6s = $('h6') + h6s.each(function (key, value) { + if (/^tags/gmi.test($(value).text())) { + var codes = $(value).find('code') + for (let i = 0; i < codes.length; i++) { + var text = S($(codes[i]).text().trim()).stripTags().s + if (text) rawtags.push(text) + } + } + }) + } + for (let i = 0; i < rawtags.length; i++) { + var found = false + for (let j = 0; j < tags.length; j++) { + if (tags[j] === rawtags[i]) { + found = true + break + } + } + if (!found) { tags.push(rawtags[i]) } + } + return tags + }, + extractMeta: function (content) { + var obj = null + try { + obj = metaMarked(content) + if (!obj.markdown) obj.markdown = '' + if (!obj.meta) obj.meta = {} + } catch (err) { + obj = { + markdown: content, + meta: {} + } + } + return obj + }, + parseMeta: function (meta) { + var _meta = {} + if (meta) { + if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title } + if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description } + if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots } + if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA } + if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus } + if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions } + } + return _meta + }, + updateAuthorshipByOperation: function (operation, userId, authorships) { + var index = 0 + var timestamp = Date.now() + for (let i = 0; i < operation.length; i++) { + var op = operation[i] + if (ot.TextOperation.isRetain(op)) { + index += op + } else if (ot.TextOperation.isInsert(op)) { + let opStart = index + let opEnd = index + op.length + var inserted = false + // authorship format: [userId, startPos, endPos, createdAt, updatedAt] + if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]) + else { + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (!inserted) { + let nextAuthorship = authorships[j + 1] || -1 + if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) { + if (authorship[1] < opStart && authorship[2] > opStart) { + // divide + let postLength = authorship[2] - opStart + authorship[2] = opStart + authorship[4] = timestamp + authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) + authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]) + j += 2 + inserted = true + } else if (authorship[1] >= opStart) { + authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]) + j += 1 + inserted = true + } else if (authorship[2] <= opStart) { + authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) + j += 1 + inserted = true + } + } + } + if (authorship[1] >= opStart) { + authorship[1] += op.length + authorship[2] += op.length + } + } + } + index += op.length + } else if (ot.TextOperation.isDelete(op)) { + let opStart = index + let opEnd = index - op + if (operation.length === 1) { + authorships = [] + } else if (authorships.length > 0) { + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) { + authorships.splice(j, 1) + j -= 1 + } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) { + authorship[2] += op + authorship[4] = timestamp + } else if (authorship[2] >= opStart && authorship[2] <= opEnd) { + authorship[2] = opStart + authorship[4] = timestamp + } else if (authorship[1] >= opStart && authorship[1] <= opEnd) { + authorship[1] = opEnd + authorship[4] = timestamp + } + if (authorship[1] >= opEnd) { + authorship[1] += op + authorship[2] += op + } + } + } + index += op + } + } + // merge + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + for (let k = j + 1; k < authorships.length; k++) { + let nextAuthorship = authorships[k] + if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) { + let minTimestamp = Math.min(authorship[3], nextAuthorship[3]) + let maxTimestamp = Math.max(authorship[3], nextAuthorship[3]) + authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]) + authorships.splice(k, 1) + j -= 1 + break + } + } + } + // clear + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (!authorship[0]) { + authorships.splice(j, 1) + j -= 1 + } + } + return authorships + }, + transformPatchToOperations: function (patch, contentLength) { + var operations = [] + if (patch.length > 0) { + // calculate original content length + for (let j = patch.length - 1; j >= 0; j--) { + var p = patch[j] + for (let i = 0; i < p.diffs.length; i++) { + var diff = p.diffs[i] + switch (diff[0]) { + case 1: // insert + contentLength -= diff[1].length + break + case -1: // delete + contentLength += diff[1].length + break + } + } + } + // generate operations + var bias = 0 + var lengthBias = 0 + for (let j = 0; j < patch.length; j++) { + var operation = [] + let p = patch[j] + var currIndex = p.start1 + var currLength = contentLength - bias + for (let i = 0; i < p.diffs.length; i++) { + let diff = p.diffs[i] + switch (diff[0]) { + case 0: // retain + if (i === 0) { + // first + operation.push(currIndex + diff[1].length) + } else if (i !== p.diffs.length - 1) { + // mid + operation.push(diff[1].length) + } else { + // last + operation.push(currLength + lengthBias - currIndex) + } + currIndex += diff[1].length + break + case 1: // insert + operation.push(diff[1]) + lengthBias += diff[1].length + currIndex += diff[1].length + break + case -1: // delete + operation.push(-diff[1].length) + bias += diff[1].length + currIndex += diff[1].length + break + } + } + operations.push(operation) + } + } + return operations + } + }, + hooks: { + beforeCreate: function (note, options, callback) { + // if no content specified then use default note + if (!note.content) { + var body = null + let filePath = null + if (!note.alias) { + filePath = config.defaultnotepath + } else { + filePath = path.join(config.docspath, note.alias + '.md') + } + if (Note.checkFileExist(filePath)) { + var fsCreatedTime = moment(fs.statSync(filePath).ctime) + body = fs.readFileSync(filePath, 'utf8') + note.title = Note.parseNoteTitle(body) + note.content = body + if (filePath !== config.defaultnotepath) { + note.createdAt = fsCreatedTime + } + } + } + // if no permission specified and have owner then give default permission in config, else default permission is freely + if (!note.permission) { + if (note.ownerId) { + note.permission = config.defaultpermission + } else { + note.permission = 'freely' + } + } + return callback(null, note) + }, + afterCreate: function (note, options, callback) { + sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { + callback(err, note) + }) + } + } + }) - return Note; -}; + return Note +} diff --git a/lib/models/revision.js b/lib/models/revision.js index c7360fe..d8dab30 100644 --- a/lib/models/revision.js +++ b/lib/models/revision.js @@ -1,306 +1,306 @@ -"use strict"; - // external modules -var Sequelize = require("sequelize"); -var async = require('async'); -var moment = require('moment'); -var childProcess = require('child_process'); -var shortId = require('shortid'); +var Sequelize = require('sequelize') +var async = require('async') +var moment = require('moment') +var childProcess = require('child_process') +var shortId = require('shortid') // core -var config = require("../config.js"); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -var dmpWorker = createDmpWorker(); -var dmpCallbackCache = {}; +var dmpWorker = createDmpWorker() +var dmpCallbackCache = {} -function createDmpWorker() { - var worker = childProcess.fork("./lib/workers/dmpWorker.js", { - stdio: 'ignore' - }); - if (config.debug) logger.info('dmp worker process started'); - worker.on('message', function (data) { - if (!data || !data.msg || !data.cacheKey) { - return logger.error('dmp worker error: not enough data on message'); - } - var cacheKey = data.cacheKey; - switch(data.msg) { - case 'error': - dmpCallbackCache[cacheKey](data.error, null); - break; - case 'check': - dmpCallbackCache[cacheKey](null, data.result); - break; - } - delete dmpCallbackCache[cacheKey]; - }); - worker.on('close', function (code) { - dmpWorker = null; - if (config.debug) logger.info('dmp worker process exited with code ' + code); - }); - return worker; +function createDmpWorker () { + var worker = childProcess.fork('./lib/workers/dmpWorker.js', { + stdio: 'ignore' + }) + if (config.debug) logger.info('dmp worker process started') + worker.on('message', function (data) { + if (!data || !data.msg || !data.cacheKey) { + return logger.error('dmp worker error: not enough data on message') + } + var cacheKey = data.cacheKey + switch (data.msg) { + case 'error': + dmpCallbackCache[cacheKey](data.error, null) + break + case 'check': + dmpCallbackCache[cacheKey](null, data.result) + break + } + delete dmpCallbackCache[cacheKey] + }) + worker.on('close', function (code) { + dmpWorker = null + if (config.debug) logger.info('dmp worker process exited with code ' + code) + }) + return worker } -function sendDmpWorker(data, callback) { - if (!dmpWorker) dmpWorker = createDmpWorker(); - var cacheKey = Date.now() + '_' + shortId.generate(); - dmpCallbackCache[cacheKey] = callback; - data = Object.assign(data, { - cacheKey: cacheKey - }); - dmpWorker.send(data); +function sendDmpWorker (data, callback) { + if (!dmpWorker) dmpWorker = createDmpWorker() + var cacheKey = Date.now() + '_' + shortId.generate() + dmpCallbackCache[cacheKey] = callback + data = Object.assign(data, { + cacheKey: cacheKey + }) + dmpWorker.send(data) } module.exports = function (sequelize, DataTypes) { - var Revision = sequelize.define("Revision", { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - patch: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('patch'), ""); + var Revision = sequelize.define('Revision', { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + patch: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('patch'), '') + }, + set: function (value) { + this.setDataValue('patch', sequelize.stripNullByte(value)) + } + }, + lastContent: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('lastContent'), '') + }, + set: function (value) { + this.setDataValue('lastContent', sequelize.stripNullByte(value)) + } + }, + content: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('content'), '') + }, + set: function (value) { + this.setDataValue('content', sequelize.stripNullByte(value)) + } + }, + length: { + type: DataTypes.INTEGER + }, + authorship: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse) + }, + set: function (value) { + this.setDataValue('authorship', value ? JSON.stringify(value) : value) + } + } + }, { + classMethods: { + associate: function (models) { + Revision.belongsTo(models.Note, { + foreignKey: 'noteId', + as: 'note', + constraints: false + }) + }, + getNoteRevisions: function (note, callback) { + Revision.findAll({ + where: { + noteId: note.id + }, + order: '"createdAt" DESC' + }).then(function (revisions) { + var data = [] + for (var i = 0, l = revisions.length; i < l; i++) { + var revision = revisions[i] + data.push({ + time: moment(revision.createdAt).valueOf(), + length: revision.length + }) + } + callback(null, data) + }).catch(function (err) { + callback(err, null) + }) + }, + getPatchedNoteRevisionByTime: function (note, time, callback) { + // find all revisions to prepare for all possible calculation + Revision.findAll({ + where: { + noteId: note.id + }, + order: '"createdAt" DESC' + }).then(function (revisions) { + if (revisions.length <= 0) return callback(null, null) + // measure target revision position + Revision.count({ + where: { + noteId: note.id, + createdAt: { + $gte: time + } }, - set: function (value) { - this.setDataValue('patch', sequelize.stripNullByte(value)); - } - }, - lastContent: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('lastContent'), ""); - }, - set: function (value) { - this.setDataValue('lastContent', sequelize.stripNullByte(value)); - } - }, - content: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('content'), ""); - }, - set: function (value) { - this.setDataValue('content', sequelize.stripNullByte(value)); - } - }, - length: { - type: DataTypes.INTEGER - }, - authorship: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse); - }, - set: function (value) { - this.setDataValue('authorship', value ? JSON.stringify(value) : value); - } - } - }, { - classMethods: { - associate: function (models) { - Revision.belongsTo(models.Note, { - foreignKey: "noteId", - as: "note", - constraints: false - }); - }, - getNoteRevisions: function (note, callback) { - Revision.findAll({ - where: { - noteId: note.id - }, - order: '"createdAt" DESC' - }).then(function (revisions) { - var data = []; - for (var i = 0, l = revisions.length; i < l; i++) { - var revision = revisions[i]; - data.push({ - time: moment(revision.createdAt).valueOf(), - length: revision.length - }); + order: '"createdAt" DESC' + }).then(function (count) { + if (count <= 0) return callback(null, null) + sendDmpWorker({ + msg: 'get revision', + revisions: revisions, + count: count + }, callback) + }).catch(function (err) { + return callback(err, null) + }) + }).catch(function (err) { + return callback(err, null) + }) + }, + checkAllNotesRevision: function (callback) { + Revision.saveAllNotesRevision(function (err, notes) { + if (err) return callback(err, null) + if (!notes || notes.length <= 0) { + return callback(null, notes) + } else { + Revision.checkAllNotesRevision(callback) + } + }) + }, + saveAllNotesRevision: function (callback) { + sequelize.models.Note.findAll({ + // query all notes that need to save for revision + where: { + $and: [ + { + lastchangeAt: { + $or: { + $eq: null, + $and: { + $ne: null, + $gt: sequelize.col('createdAt') } - callback(null, data); - }).catch(function (err) { - callback(err, null); - }); - }, - getPatchedNoteRevisionByTime: function (note, time, callback) { - // find all revisions to prepare for all possible calculation - Revision.findAll({ - where: { - noteId: note.id - }, - order: '"createdAt" DESC' - }).then(function (revisions) { - if (revisions.length <= 0) return callback(null, null); - // measure target revision position - Revision.count({ - where: { - noteId: note.id, - createdAt: { - $gte: time - } - }, - order: '"createdAt" DESC' - }).then(function (count) { - if (count <= 0) return callback(null, null); - sendDmpWorker({ - msg: 'get revision', - revisions: revisions, - count: count - }, callback); - }).catch(function (err) { - return callback(err, null); - }); - }).catch(function (err) { - return callback(err, null); - }); - }, - checkAllNotesRevision: function (callback) { - Revision.saveAllNotesRevision(function (err, notes) { - if (err) return callback(err, null); - if (!notes || notes.length <= 0) { - return callback(null, notes); - } else { - Revision.checkAllNotesRevision(callback); - } - }); - }, - saveAllNotesRevision: function (callback) { - sequelize.models.Note.findAll({ - // query all notes that need to save for revision - where: { - $and: [ - { - lastchangeAt: { - $or: { - $eq: null, - $and: { - $ne: null, - $gt: sequelize.col('createdAt') - } - } - } - }, - { - savedAt: { - $or: { - $eq: null, - $lt: sequelize.col('lastchangeAt') - } - } - } - ] - } - }).then(function (notes) { - if (notes.length <= 0) return callback(null, notes); - var savedNotes = []; - async.each(notes, function (note, _callback) { - // revision saving policy: note not been modified for 5 mins or not save for 10 mins - if (note.lastchangeAt && note.savedAt) { - var lastchangeAt = moment(note.lastchangeAt); - var savedAt = moment(note.savedAt); - if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) { - savedNotes.push(note); - Revision.saveNoteRevision(note, _callback); - } else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) { - savedNotes.push(note); - Revision.saveNoteRevision(note, _callback); - } else { - return _callback(null, null); - } - } else { - savedNotes.push(note); - Revision.saveNoteRevision(note, _callback); - } - }, function (err) { - if (err) return callback(err, null); - // return null when no notes need saving at this moment but have delayed tasks to be done - var result = ((savedNotes.length == 0) && (notes.length > savedNotes.length)) ? null : savedNotes; - return callback(null, result); - }); - }).catch(function (err) { - return callback(err, null); - }); - }, - saveNoteRevision: function (note, callback) { - Revision.findAll({ - where: { - noteId: note.id - }, - order: '"createdAt" DESC' - }).then(function (revisions) { - if (revisions.length <= 0) { - // if no revision available - Revision.create({ - noteId: note.id, - lastContent: note.content, - length: note.content.length, - authorship: note.authorship - }).then(function (revision) { - Revision.finishSaveNoteRevision(note, revision, callback); - }).catch(function (err) { - return callback(err, null); - }); - } else { - var latestRevision = revisions[0]; - var lastContent = latestRevision.content || latestRevision.lastContent; - var content = note.content; - sendDmpWorker({ - msg: 'create patch', - lastDoc: lastContent, - currDoc: content, - }, function (err, patch) { - if (err) logger.error('save note revision error', err); - if (!patch) { - // if patch is empty (means no difference) then just update the latest revision updated time - latestRevision.changed('updatedAt', true); - latestRevision.update({ - updatedAt: Date.now() - }).then(function (revision) { - Revision.finishSaveNoteRevision(note, revision, callback); - }).catch(function (err) { - return callback(err, null); - }); - } else { - Revision.create({ - noteId: note.id, - patch: patch, - content: note.content, - length: note.content.length, - authorship: note.authorship - }).then(function (revision) { - // clear last revision content to reduce db size - latestRevision.update({ - content: null - }).then(function () { - Revision.finishSaveNoteRevision(note, revision, callback); - }).catch(function (err) { - return callback(err, null); - }); - }).catch(function (err) { - return callback(err, null); - }); - } - }); - } - }).catch(function (err) { - return callback(err, null); - }); - }, - finishSaveNoteRevision: function (note, revision, callback) { - note.update({ - savedAt: revision.updatedAt - }).then(function () { - return callback(null, revision); - }).catch(function (err) { - return callback(err, null); - }); + } + } + }, + { + savedAt: { + $or: { + $eq: null, + $lt: sequelize.col('lastchangeAt') + } + } + } + ] + } + }).then(function (notes) { + if (notes.length <= 0) return callback(null, notes) + var savedNotes = [] + async.each(notes, function (note, _callback) { + // revision saving policy: note not been modified for 5 mins or not save for 10 mins + if (note.lastchangeAt && note.savedAt) { + var lastchangeAt = moment(note.lastchangeAt) + var savedAt = moment(note.savedAt) + if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) + } else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) + } else { + return _callback(null, null) + } + } else { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) } - } - }); + }, function (err) { + if (err) { + return callback(err, null) + } + // return null when no notes need saving at this moment but have delayed tasks to be done + var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes + return callback(null, result) + }) + }).catch(function (err) { + return callback(err, null) + }) + }, + saveNoteRevision: function (note, callback) { + Revision.findAll({ + where: { + noteId: note.id + }, + order: '"createdAt" DESC' + }).then(function (revisions) { + if (revisions.length <= 0) { + // if no revision available + Revision.create({ + noteId: note.id, + lastContent: note.content, + length: note.content.length, + authorship: note.authorship + }).then(function (revision) { + Revision.finishSaveNoteRevision(note, revision, callback) + }).catch(function (err) { + return callback(err, null) + }) + } else { + var latestRevision = revisions[0] + var lastContent = latestRevision.content || latestRevision.lastContent + var content = note.content + sendDmpWorker({ + msg: 'create patch', + lastDoc: lastContent, + currDoc: content + }, function (err, patch) { + if (err) logger.error('save note revision error', err) + if (!patch) { + // if patch is empty (means no difference) then just update the latest revision updated time + latestRevision.changed('updatedAt', true) + latestRevision.update({ + updatedAt: Date.now() + }).then(function (revision) { + Revision.finishSaveNoteRevision(note, revision, callback) + }).catch(function (err) { + return callback(err, null) + }) + } else { + Revision.create({ + noteId: note.id, + patch: patch, + content: note.content, + length: note.content.length, + authorship: note.authorship + }).then(function (revision) { + // clear last revision content to reduce db size + latestRevision.update({ + content: null + }).then(function () { + Revision.finishSaveNoteRevision(note, revision, callback) + }).catch(function (err) { + return callback(err, null) + }) + }).catch(function (err) { + return callback(err, null) + }) + } + }) + } + }).catch(function (err) { + return callback(err, null) + }) + }, + finishSaveNoteRevision: function (note, revision, callback) { + note.update({ + savedAt: revision.updatedAt + }).then(function () { + return callback(null, revision) + }).catch(function (err) { + return callback(err, null) + }) + } + } + }) - return Revision; -}; \ No newline at end of file + return Revision +} diff --git a/lib/models/temp.js b/lib/models/temp.js index 6eeff15..e770bb3 100644 --- a/lib/models/temp.js +++ b/lib/models/temp.js @@ -1,19 +1,17 @@ -"use strict"; - -//external modules -var shortId = require('shortid'); +// external modules +var shortId = require('shortid') module.exports = function (sequelize, DataTypes) { - var Temp = sequelize.define("Temp", { - id: { - type: DataTypes.STRING, - primaryKey: true, - defaultValue: shortId.generate - }, - data: { - type: DataTypes.TEXT - } - }); - - return Temp; -}; \ No newline at end of file + var Temp = sequelize.define('Temp', { + id: { + type: DataTypes.STRING, + primaryKey: true, + defaultValue: shortId.generate + }, + data: { + type: DataTypes.TEXT + } + }) + + return Temp +} diff --git a/lib/models/user.js b/lib/models/user.js index dd93bf7..f7e533b 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -1,149 +1,147 @@ -"use strict"; - // external modules -var md5 = require("blueimp-md5"); -var Sequelize = require("sequelize"); -var scrypt = require('scrypt'); +var md5 = require('blueimp-md5') +var Sequelize = require('sequelize') +var scrypt = require('scrypt') // core -var logger = require("../logger.js"); -var letterAvatars = require('../letter-avatars.js'); +var logger = require('../logger.js') +var letterAvatars = require('../letter-avatars.js') module.exports = function (sequelize, DataTypes) { - var User = sequelize.define("User", { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - profileid: { - type: DataTypes.STRING, - unique: true - }, - profile: { - type: DataTypes.TEXT - }, - history: { - type: DataTypes.TEXT - }, - accessToken: { - type: DataTypes.STRING - }, - refreshToken: { - type: DataTypes.STRING - }, - email: { - type: Sequelize.TEXT, - validate: { - isEmail: true - } - }, - password: { - type: Sequelize.TEXT, - set: function(value) { - var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString("hex"); - this.setDataValue('password', hash); - } + var User = sequelize.define('User', { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + profileid: { + type: DataTypes.STRING, + unique: true + }, + profile: { + type: DataTypes.TEXT + }, + history: { + type: DataTypes.TEXT + }, + accessToken: { + type: DataTypes.STRING + }, + refreshToken: { + type: DataTypes.STRING + }, + email: { + type: Sequelize.TEXT, + validate: { + isEmail: true + } + }, + password: { + type: Sequelize.TEXT, + set: function (value) { + var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString('hex') + this.setDataValue('password', hash) + } + } + }, { + instanceMethods: { + verifyPassword: function (attempt) { + if (scrypt.verifyKdfSync(new Buffer(this.password, 'hex'), attempt)) { + return this + } else { + return false } - }, { - instanceMethods: { - verifyPassword: function(attempt) { - if (scrypt.verifyKdfSync(new Buffer(this.password, "hex"), attempt)) { - return this; - } else { - return false; - } - } - }, - classMethods: { - associate: function (models) { - User.hasMany(models.Note, { - foreignKey: "ownerId", - constraints: false - }); - User.hasMany(models.Note, { - foreignKey: "lastchangeuserId", - constraints: false - }); - }, - getProfile: function (user) { - return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null); - }, - parseProfile: function (profile) { - try { - var profile = JSON.parse(profile); - } catch (err) { - logger.error(err); - profile = null; - } - if (profile) { - profile = { - name: profile.displayName || profile.username, - photo: User.parsePhotoByProfile(profile), - biggerphoto: User.parsePhotoByProfile(profile, true) - } - } - return profile; - }, - parsePhotoByProfile: function (profile, bigger) { - var photo = null; - switch (profile.provider) { - case "facebook": - photo = 'https://graph.facebook.com/' + profile.id + '/picture'; - if (bigger) photo += '?width=400'; - else photo += '?width=96'; - break; - case "twitter": - photo = 'https://twitter.com/' + profile.username + '/profile_image'; - if (bigger) photo += '?size=original'; - else photo += '?size=bigger'; - break; - case "github": - photo = 'https://avatars.githubusercontent.com/u/' + profile.id; - if (bigger) photo += '?s=400'; - else photo += '?s=96'; - break; - case "gitlab": - photo = profile.avatarUrl; - if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400'); - else photo = photo.replace(/(\?s=)\d*$/i, '$196'); - break; - case "dropbox": - //no image api provided, use gravatar - photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value); - if (bigger) photo += '?s=400'; - else photo += '?s=96'; - break; - case "google": - photo = profile.photos[0].value; - if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400'); - else photo = photo.replace(/(\?sz=)\d*$/i, '$196'); - break; - case "ldap": - //no image api provided, - //use gravatar if email exists, - //otherwise generate a letter avatar - if (profile.emails[0]) { - photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]); - if (bigger) photo += '?s=400'; - else photo += '?s=96'; - } else { - photo = letterAvatars(profile.username); - } - break; - } - return photo; - }, - parseProfileByEmail: function (email) { - var photoUrl = 'https://www.gravatar.com/avatar/' + md5(email); - return { - name: email.substring(0, email.lastIndexOf("@")), - photo: photoUrl += '?s=96', - biggerphoto: photoUrl += '?s=400' - }; - } + } + }, + classMethods: { + associate: function (models) { + User.hasMany(models.Note, { + foreignKey: 'ownerId', + constraints: false + }) + User.hasMany(models.Note, { + foreignKey: 'lastchangeuserId', + constraints: false + }) + }, + getProfile: function (user) { + return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null) + }, + parseProfile: function (profile) { + try { + profile = JSON.parse(profile) + } catch (err) { + logger.error(err) + profile = null } - }); + if (profile) { + profile = { + name: profile.displayName || profile.username, + photo: User.parsePhotoByProfile(profile), + biggerphoto: User.parsePhotoByProfile(profile, true) + } + } + return profile + }, + parsePhotoByProfile: function (profile, bigger) { + var photo = null + switch (profile.provider) { + case 'facebook': + photo = 'https://graph.facebook.com/' + profile.id + '/picture' + if (bigger) photo += '?width=400' + else photo += '?width=96' + break + case 'twitter': + photo = 'https://twitter.com/' + profile.username + '/profile_image' + if (bigger) photo += '?size=original' + else photo += '?size=bigger' + break + case 'github': + photo = 'https://avatars.githubusercontent.com/u/' + profile.id + if (bigger) photo += '?s=400' + else photo += '?s=96' + break + case 'gitlab': + photo = profile.avatarUrl + if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') + else photo = photo.replace(/(\?s=)\d*$/i, '$196') + break + case 'dropbox': + // no image api provided, use gravatar + photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value) + if (bigger) photo += '?s=400' + else photo += '?s=96' + break + case 'google': + photo = profile.photos[0].value + if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400') + else photo = photo.replace(/(\?sz=)\d*$/i, '$196') + break + case 'ldap': + // no image api provided, + // use gravatar if email exists, + // otherwise generate a letter avatar + if (profile.emails[0]) { + photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]) + if (bigger) photo += '?s=400' + else photo += '?s=96' + } else { + photo = letterAvatars(profile.username) + } + break + } + return photo + }, + parseProfileByEmail: function (email) { + var photoUrl = 'https://www.gravatar.com/avatar/' + md5(email) + return { + name: email.substring(0, email.lastIndexOf('@')), + photo: photoUrl + '?s=96', + biggerphoto: photoUrl + '?s=400' + } + } + } + }) - return User; -}; \ No newline at end of file + return User +} diff --git a/lib/realtime.js b/lib/realtime.js index c1db688..cff795c 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -1,937 +1,924 @@ -//realtime -//external modules -var cookie = require('cookie'); -var cookieParser = require('cookie-parser'); -var url = require('url'); -var async = require('async'); -var LZString = require('lz-string'); -var randomcolor = require("randomcolor"); -var Chance = require('chance'), - chance = new Chance(); -var moment = require('moment'); +// realtime +// external modules +var cookie = require('cookie') +var cookieParser = require('cookie-parser') +var url = require('url') +var async = require('async') +var LZString = require('lz-string') +var randomcolor = require('randomcolor') +var Chance = require('chance') +var chance = new Chance() +var moment = require('moment') -//core -var config = require("./config.js"); -var logger = require("./logger.js"); -var history = require("./history.js"); -var models = require("./models"); +// core +var config = require('./config.js') +var logger = require('./logger.js') +var history = require('./history.js') +var models = require('./models') -//ot -var ot = require("./ot/index.js"); +// ot +var ot = require('./ot/index.js') -//public +// public var realtime = { - io: null, - onAuthorizeSuccess: onAuthorizeSuccess, - onAuthorizeFail: onAuthorizeFail, - secure: secure, - connection: connection, - getStatus: getStatus, - isReady: isReady -}; - -function onAuthorizeSuccess(data, accept) { - accept(); + io: null, + onAuthorizeSuccess: onAuthorizeSuccess, + onAuthorizeFail: onAuthorizeFail, + secure: secure, + connection: connection, + getStatus: getStatus, + isReady: isReady } -function onAuthorizeFail(data, message, error, accept) { - accept(); //accept whether authorize or not to allow anonymous usage +function onAuthorizeSuccess (data, accept) { + accept() } -//secure the origin by the cookie -function secure(socket, next) { - try { - var handshakeData = socket.request; - if (handshakeData.headers.cookie) { - handshakeData.cookie = cookie.parse(handshakeData.headers.cookie); - handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionname], config.sessionsecret); - if (handshakeData.sessionID && +function onAuthorizeFail (data, message, error, accept) { + accept() // accept whether authorize or not to allow anonymous usage +} + +// secure the origin by the cookie +function secure (socket, next) { + try { + var handshakeData = socket.request + if (handshakeData.headers.cookie) { + handshakeData.cookie = cookie.parse(handshakeData.headers.cookie) + handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionname], config.sessionsecret) + if (handshakeData.sessionID && handshakeData.cookie[config.sessionname] && - handshakeData.cookie[config.sessionname] != handshakeData.sessionID) { - if (config.debug) - logger.info("AUTH success cookie: " + handshakeData.sessionID); - return next(); - } else { - next(new Error('AUTH failed: Cookie is invalid.')); - } - } else { - next(new Error('AUTH failed: No cookie transmitted.')); - } - } catch (ex) { - next(new Error("AUTH failed:" + JSON.stringify(ex))); - } -} - -function emitCheck(note) { - var out = { - title: note.title, - updatetime: note.updatetime, - lastchangeuser: note.lastchangeuser, - lastchangeuserprofile: note.lastchangeuserprofile, - authors: note.authors, - authorship: note.authorship - }; - realtime.io.to(note.id).emit('check', out); -} - -//actions -var users = {}; -var notes = {}; -//update when the note is dirty -var updater = setInterval(function () { - async.each(Object.keys(notes), function (key, callback) { - var note = notes[key]; - if (note.server.isDirty) { - if (config.debug) logger.info("updater found dirty note: " + key); - note.server.isDirty = false; - updateNote(note, function(err, _note) { - // handle when note already been clean up - if (!notes[key] || !notes[key].server) return callback(null, null); - if (!_note) { - realtime.io.to(note.id).emit('info', { - code: 404 - }); - logger.error('note not found: ', note.id); - } - if (err || !_note) { - for (var i = 0, l = note.socks.length; i < l; i++) { - var sock = note.socks[i]; - if (typeof sock !== 'undefined' && sock) { - setTimeout(function () { - sock.disconnect(true); - }, 0); - } - } - return callback(err, null); - } - note.updatetime = moment(_note.lastchangeAt).valueOf(); - emitCheck(note); - return callback(null, null); - }); - } else { - return callback(null, null); - } - }, function (err) { - if (err) return logger.error('updater error', err); - }); -}, 1000); -function updateNote(note, callback) { - models.Note.findOne({ - where: { - id: note.id - } - }).then(function (_note) { - if (!_note) return callback(null, null); - // update user note history - var tempUsers = Object.assign({}, note.tempUsers); - note.tempUsers = {}; - Object.keys(tempUsers).forEach(function (key) { - updateHistory(key, note, tempUsers[key]); - }); - if (note.lastchangeuser) { - if (_note.lastchangeuserId != note.lastchangeuser) { - models.User.findOne({ - where: { - id: note.lastchangeuser - } - }).then(function (user) { - if (!user) return callback(null, null); - note.lastchangeuserprofile = models.User.getProfile(user); - return finishUpdateNote(note, _note, callback); - }).catch(function (err) { - logger.error(err); - return callback(err, null); - }); - } else { - return finishUpdateNote(note, _note, callback); - } - } else { - note.lastchangeuserprofile = null; - return finishUpdateNote(note, _note, callback); - } - }).catch(function (err) { - logger.error(err); - return callback(err, null); - }); -} -function finishUpdateNote(note, _note, callback) { - if (!note || !note.server) return callback(null, null); - var body = note.server.document; - var title = note.title = models.Note.parseNoteTitle(body); - var values = { - title: title, - content: body, - authorship: note.authorship, - lastchangeuserId: note.lastchangeuser, - lastchangeAt: Date.now() - }; - _note.update(values).then(function (_note) { - saverSleep = false; - return callback(null, _note); - }).catch(function (err) { - logger.error(err); - return callback(err, null); - }); -} -//clean when user not in any rooms or user not in connected list -var cleaner = setInterval(function () { - async.each(Object.keys(users), function (key, callback) { - var socket = realtime.io.sockets.connected[key]; - if ((!socket && users[key]) || - (socket && (!socket.rooms || socket.rooms.length <= 0))) { - if (config.debug) - logger.info("cleaner found redundant user: " + key); - if (!socket) { - socket = { - id: key - }; - } - disconnectSocketQueue.push(socket); - disconnect(socket); - } - return callback(null, null); - }, function (err) { - if (err) return logger.error('cleaner error', err); - }); -}, 60000); -var saverSleep = false; -// save note revision in interval -var saver = setInterval(function () { - if (saverSleep) return; - models.Revision.saveAllNotesRevision(function (err, notes) { - if (err) return logger.error('revision saver failed: ' + err); - if (notes && notes.length <= 0) { - saverSleep = true; - return; - } - }); -}, 60000 * 5); - -function getStatus(callback) { - models.Note.count().then(function (notecount) { - var distinctaddresses = []; - var regaddresses = []; - var distinctregaddresses = []; - Object.keys(users).forEach(function (key) { - var user = users[key]; - if (!user) return; - var found = false; - for (var i = 0; i < distinctaddresses.length; i++) { - if (user.address == distinctaddresses[i]) { - found = true; - break; - } - } - if (!found) { - distinctaddresses.push(user.address); - } - if (user.login) { - regaddresses.push(user.address); - var found = false; - for (var i = 0; i < distinctregaddresses.length; i++) { - if (user.address == distinctregaddresses[i]) { - found = true; - break; - } - } - if (!found) { - distinctregaddresses.push(user.address); - } - } - }); - models.User.count().then(function (regcount) { - return callback ? callback({ - onlineNotes: Object.keys(notes).length, - onlineUsers: Object.keys(users).length, - distinctOnlineUsers: distinctaddresses.length, - notesCount: notecount, - registeredUsers: regcount, - onlineRegisteredUsers: regaddresses.length, - distinctOnlineRegisteredUsers: distinctregaddresses.length, - isConnectionBusy: isConnectionBusy, - connectionSocketQueueLength: connectionSocketQueue.length, - isDisconnectBusy: isDisconnectBusy, - disconnectSocketQueueLength: disconnectSocketQueue.length - }) : null; - }).catch(function (err) { - return logger.error('count user failed: ' + err); - }); - }).catch(function (err) { - return logger.error('count note failed: ' + err); - }); -} - -function isReady() { - return realtime.io - && Object.keys(notes).length == 0 && Object.keys(users).length == 0 - && connectionSocketQueue.length == 0 && !isConnectionBusy - && disconnectSocketQueue.length == 0 && !isDisconnectBusy; -} - -function extractNoteIdFromSocket(socket) { - if (!socket || !socket.handshake || !socket.handshake.headers) { - return false; - } - var referer = socket.handshake.headers.referer; - if (!referer) { - return false; - } - var hostUrl = url.parse(referer); - var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1]; - return noteId; -} - -function parseNoteIdFromSocket(socket, callback) { - var noteId = extractNoteIdFromSocket(socket); - if (!noteId) { - return callback(null, null); - } - models.Note.parseNoteId(noteId, function (err, id) { - if (err || !id) return callback(err, id); - return callback(null, id); - }); -} - -function emitOnlineUsers(socket) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var users = []; - Object.keys(notes[noteId].users).forEach(function (key) { - var user = notes[noteId].users[key]; - if (user) - users.push(buildUserOutData(user)); - }); - var out = { - users: users - }; - realtime.io.to(noteId).emit('online users', out); -} - -function emitUserStatus(socket) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - var out = buildUserOutData(user); - socket.broadcast.to(noteId).emit('user status', out); -} - -function emitRefresh(socket) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - var out = { - title: note.title, - docmaxlength: config.documentmaxlength, - owner: note.owner, - ownerprofile: note.ownerprofile, - lastchangeuser: note.lastchangeuser, - lastchangeuserprofile: note.lastchangeuserprofile, - authors: note.authors, - authorship: note.authorship, - permission: note.permission, - createtime: note.createtime, - updatetime: note.updatetime - }; - socket.emit('refresh', out); -} - -function isDuplicatedInSocketQueue(queue, socket) { - for (var i = 0; i < queue.length; i++) { - if (queue[i] && queue[i].id == socket.id) { - return true; - } - } - return false; -} - -function clearSocketQueue(queue, socket) { - for (var i = 0; i < queue.length; i++) { - if (!queue[i] || queue[i].id == socket.id) { - queue.splice(i, 1); - i--; - } - } -} - -function connectNextSocket() { - setTimeout(function () { - isConnectionBusy = false; - if (connectionSocketQueue.length > 0) { - startConnection(connectionSocketQueue[0]); - } - }, 1); -} - -function interruptConnection(socket, note, user) { - if (note) delete note; - if (user) delete user; - if (socket) - clearSocketQueue(connectionSocketQueue, socket); - else - connectionSocketQueue.shift(); - connectNextSocket(); -} - -function checkViewPermission(req, note) { - if (note.permission == 'private') { - if (req.user && req.user.logged_in && req.user.id == note.owner) - return true; - else - return false; - } else if (note.permission == 'limited' || note.permission == 'protected') { - if(req.user && req.user.logged_in) - return true; - else - return false; + handshakeData.cookie[config.sessionname] !== handshakeData.sessionID) { + if (config.debug) { logger.info('AUTH success cookie: ' + handshakeData.sessionID) } + return next() + } else { + next(new Error('AUTH failed: Cookie is invalid.')) + } } else { - return true; + next(new Error('AUTH failed: No cookie transmitted.')) } + } catch (ex) { + next(new Error('AUTH failed:' + JSON.stringify(ex))) + } } -var isConnectionBusy = false; -var connectionSocketQueue = []; -var isDisconnectBusy = false; -var disconnectSocketQueue = []; +function emitCheck (note) { + var out = { + title: note.title, + updatetime: note.updatetime, + lastchangeuser: note.lastchangeuser, + lastchangeuserprofile: note.lastchangeuserprofile, + authors: note.authors, + authorship: note.authorship + } + realtime.io.to(note.id).emit('check', out) +} -function finishConnection(socket, note, user) { - // if no valid info provided will drop the client - if (!socket || !note || !user) { - return interruptConnection(socket, note, user); +// actions +var users = {} +var notes = {} +// update when the note is dirty +setInterval(function () { + async.each(Object.keys(notes), function (key, callback) { + var note = notes[key] + if (note.server.isDirty) { + if (config.debug) logger.info('updater found dirty note: ' + key) + note.server.isDirty = false + updateNote(note, function (err, _note) { + // handle when note already been clean up + if (!notes[key] || !notes[key].server) return callback(null, null) + if (!_note) { + realtime.io.to(note.id).emit('info', { + code: 404 + }) + logger.error('note not found: ', note.id) + } + if (err || !_note) { + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i] + if (typeof sock !== 'undefined' && sock) { + setTimeout(function () { + sock.disconnect(true) + }, 0) + } + } + return callback(err, null) + } + note.updatetime = moment(_note.lastchangeAt).valueOf() + emitCheck(note) + return callback(null, null) + }) + } else { + return callback(null, null) } - // check view permission - if (!checkViewPermission(socket.request, note)) { - interruptConnection(socket, note, user); - return failConnection(403, 'connection forbidden', socket); - } - // update user color to author color - if (note.authors[user.userid]) { - user.color = users[socket.id].color = note.authors[user.userid].color; - } - note.users[socket.id] = user; - note.socks.push(socket); - note.server.addClient(socket); - note.server.setName(socket, user.name); - note.server.setColor(socket, user.color); + }, function (err) { + if (err) return logger.error('updater error', err) + }) +}, 1000) +function updateNote (note, callback) { + models.Note.findOne({ + where: { + id: note.id + } + }).then(function (_note) { + if (!_note) return callback(null, null) // update user note history - updateHistory(user.userid, note); - - emitOnlineUsers(socket); - emitRefresh(socket); - - //clear finished socket in queue - clearSocketQueue(connectionSocketQueue, socket); - //seek for next socket - connectNextSocket(); - - if (config.debug) { - var noteId = socket.noteId; - logger.info('SERVER connected a client to [' + noteId + ']:'); - logger.info(JSON.stringify(user)); - //logger.info(notes); - getStatus(function (data) { - logger.info(JSON.stringify(data)); - }); - } -} - -function startConnection(socket) { - if (isConnectionBusy) return; - isConnectionBusy = true; - - var noteId = socket.noteId; - if (!noteId) { - return failConnection(404, 'note id not found', socket); - } - - if (!notes[noteId]) { - var include = [{ - model: models.User, - as: "owner" - }, { - model: models.User, - as: "lastchangeuser" - }, { - model: models.Author, - as: "authors", - include: [{ - model: models.User, - as: "user" - }] - }]; - - models.Note.findOne({ - where: { - id: noteId - }, - include: include - }).then(function (note) { - if (!note) { - return failConnection(404, 'note not found', socket); - } - var owner = note.ownerId; - var ownerprofile = note.owner ? models.User.getProfile(note.owner) : null; - - var lastchangeuser = note.lastchangeuserId; - var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null; - - var body = note.content; - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback); - - var authors = {}; - for (var i = 0; i < note.authors.length; i++) { - var author = note.authors[i]; - var profile = models.User.getProfile(author.user); - authors[author.userId] = { - userid: author.userId, - color: author.color, - photo: profile.photo, - name: profile.name - }; - } - - notes[noteId] = { - id: noteId, - alias: note.alias, - title: note.title, - owner: owner, - ownerprofile: ownerprofile, - permission: note.permission, - lastchangeuser: lastchangeuser, - lastchangeuserprofile: lastchangeuserprofile, - socks: [], - users: {}, - tempUsers: {}, - createtime: moment(createtime).valueOf(), - updatetime: moment(updatetime).valueOf(), - server: server, - authors: authors, - authorship: note.authorship - }; - - return finishConnection(socket, notes[noteId], users[socket.id]); + var tempUsers = Object.assign({}, note.tempUsers) + note.tempUsers = {} + Object.keys(tempUsers).forEach(function (key) { + updateHistory(key, note, tempUsers[key]) + }) + if (note.lastchangeuser) { + if (_note.lastchangeuserId !== note.lastchangeuser) { + models.User.findOne({ + where: { + id: note.lastchangeuser + } + }).then(function (user) { + if (!user) return callback(null, null) + note.lastchangeuserprofile = models.User.getProfile(user) + return finishUpdateNote(note, _note, callback) }).catch(function (err) { - return failConnection(500, err, socket); - }); + logger.error(err) + return callback(err, null) + }) + } else { + return finishUpdateNote(note, _note, callback) + } } else { - return finishConnection(socket, notes[noteId], users[socket.id]); + note.lastchangeuserprofile = null + return finishUpdateNote(note, _note, callback) } + }).catch(function (err) { + logger.error(err) + return callback(err, null) + }) } -function failConnection(code, err, socket) { - logger.error(err); - // clear error socket in queue - clearSocketQueue(connectionSocketQueue, socket); - connectNextSocket(); - // emit error info - socket.emit('info', { - code: code - }); - return socket.disconnect(true); +function finishUpdateNote (note, _note, callback) { + if (!note || !note.server) return callback(null, null) + var body = note.server.document + var title = note.title = models.Note.parseNoteTitle(body) + var values = { + title: title, + content: body, + authorship: note.authorship, + lastchangeuserId: note.lastchangeuser, + lastchangeAt: Date.now() + } + _note.update(values).then(function (_note) { + saverSleep = false + return callback(null, _note) + }).catch(function (err) { + logger.error(err) + return callback(err, null) + }) } -function disconnect(socket) { - if (isDisconnectBusy) return; - isDisconnectBusy = true; - - if (config.debug) { - logger.info("SERVER disconnected a client"); - logger.info(JSON.stringify(users[socket.id])); - } - - if (users[socket.id]) { - delete users[socket.id]; - } - var noteId = socket.noteId; - var note = notes[noteId]; - if (note) { - // delete user in users - if (note.users[socket.id]) { - delete note.users[socket.id]; +// clean when user not in any rooms or user not in connected list +setInterval(function () { + async.each(Object.keys(users), function (key, callback) { + var socket = realtime.io.sockets.connected[key] + if ((!socket && users[key]) || + (socket && (!socket.rooms || socket.rooms.length <= 0))) { + if (config.debug) { logger.info('cleaner found redundant user: ' + key) } + if (!socket) { + socket = { + id: key } - // remove sockets in the note socks - do { - var index = note.socks.indexOf(socket); - if (index != -1) { - note.socks.splice(index, 1); - } - } while (index != -1); - // remove note in notes if no user inside - if (Object.keys(note.users).length <= 0) { - if (note.server.isDirty) { - updateNote(note, function (err, _note) { - if (err) return logger.error('disconnect note failed: ' + err); - // clear server before delete to avoid memory leaks - note.server.document = ""; - note.server.operations = []; - delete note.server; - delete notes[noteId]; - if (config.debug) { - //logger.info(notes); - getStatus(function (data) { - logger.info(JSON.stringify(data)); - }); - } - }); - } else { - delete note.server; - delete notes[noteId]; - } + } + disconnectSocketQueue.push(socket) + disconnect(socket) + } + return callback(null, null) + }, function (err) { + if (err) return logger.error('cleaner error', err) + }) +}, 60000) + +var saverSleep = false +// save note revision in interval +setInterval(function () { + if (saverSleep) return + models.Revision.saveAllNotesRevision(function (err, notes) { + if (err) return logger.error('revision saver failed: ' + err) + if (notes && notes.length <= 0) { + saverSleep = true + } + }) +}, 60000 * 5) + +function getStatus (callback) { + models.Note.count().then(function (notecount) { + var distinctaddresses = [] + var regaddresses = [] + var distinctregaddresses = [] + Object.keys(users).forEach(function (key) { + var user = users[key] + if (!user) return + let found = false + for (let i = 0; i < distinctaddresses.length; i++) { + if (user.address === distinctaddresses[i]) { + found = true + break } - } - emitOnlineUsers(socket); - - //clear finished socket in queue - clearSocketQueue(disconnectSocketQueue, socket); - //seek for next socket - isDisconnectBusy = false; - if (disconnectSocketQueue.length > 0) - disconnect(disconnectSocketQueue[0]); - - if (config.debug) { - //logger.info(notes); - getStatus(function (data) { - logger.info(JSON.stringify(data)); - }); - } + } + if (!found) { + distinctaddresses.push(user.address) + } + if (user.login) { + regaddresses.push(user.address) + let found = false + for (let i = 0; i < distinctregaddresses.length; i++) { + if (user.address === distinctregaddresses[i]) { + found = true + break + } + } + if (!found) { + distinctregaddresses.push(user.address) + } + } + }) + models.User.count().then(function (regcount) { + return callback ? callback({ + onlineNotes: Object.keys(notes).length, + onlineUsers: Object.keys(users).length, + distinctOnlineUsers: distinctaddresses.length, + notesCount: notecount, + registeredUsers: regcount, + onlineRegisteredUsers: regaddresses.length, + distinctOnlineRegisteredUsers: distinctregaddresses.length, + isConnectionBusy: isConnectionBusy, + connectionSocketQueueLength: connectionSocketQueue.length, + isDisconnectBusy: isDisconnectBusy, + disconnectSocketQueueLength: disconnectSocketQueue.length + }) : null + }).catch(function (err) { + return logger.error('count user failed: ' + err) + }) + }).catch(function (err) { + return logger.error('count note failed: ' + err) + }) } -function buildUserOutData(user) { +function isReady () { + return realtime.io && + Object.keys(notes).length === 0 && Object.keys(users).length === 0 && + connectionSocketQueue.length === 0 && !isConnectionBusy && + disconnectSocketQueue.length === 0 && !isDisconnectBusy +} + +function extractNoteIdFromSocket (socket) { + if (!socket || !socket.handshake || !socket.handshake.headers) { + return false + } + var referer = socket.handshake.headers.referer + if (!referer) { + return false + } + var hostUrl = url.parse(referer) + var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1] + return noteId +} + +function parseNoteIdFromSocket (socket, callback) { + var noteId = extractNoteIdFromSocket(socket) + if (!noteId) { + return callback(null, null) + } + models.Note.parseNoteId(noteId, function (err, id) { + if (err || !id) return callback(err, id) + return callback(null, id) + }) +} + +function emitOnlineUsers (socket) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var users = [] + Object.keys(notes[noteId].users).forEach(function (key) { + var user = notes[noteId].users[key] + if (user) { users.push(buildUserOutData(user)) } + }) + var out = { + users: users + } + realtime.io.to(noteId).emit('online users', out) +} + +function emitUserStatus (socket) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + var out = buildUserOutData(user) + socket.broadcast.to(noteId).emit('user status', out) +} + +function emitRefresh (socket) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + var out = { + title: note.title, + docmaxlength: config.documentmaxlength, + owner: note.owner, + ownerprofile: note.ownerprofile, + lastchangeuser: note.lastchangeuser, + lastchangeuserprofile: note.lastchangeuserprofile, + authors: note.authors, + authorship: note.authorship, + permission: note.permission, + createtime: note.createtime, + updatetime: note.updatetime + } + socket.emit('refresh', out) +} + +function isDuplicatedInSocketQueue (queue, socket) { + for (var i = 0; i < queue.length; i++) { + if (queue[i] && queue[i].id === socket.id) { + return true + } + } + return false +} + +function clearSocketQueue (queue, socket) { + for (var i = 0; i < queue.length; i++) { + if (!queue[i] || queue[i].id === socket.id) { + queue.splice(i, 1) + i-- + } + } +} + +function connectNextSocket () { + setTimeout(function () { + isConnectionBusy = false + if (connectionSocketQueue.length > 0) { + startConnection(connectionSocketQueue[0]) + } + }, 1) +} + +function interruptConnection (socket, noteId, socketId) { + if (notes[noteId]) delete notes[noteId] + if (users[socketId]) delete users[socketId] + if (socket) { clearSocketQueue(connectionSocketQueue, socket) } else { connectionSocketQueue.shift() } + connectNextSocket() +} + +function checkViewPermission (req, note) { + if (note.permission === 'private') { + if (req.user && req.user.logged_in && req.user.id === note.owner) { return true } else { return false } + } else if (note.permission === 'limited' || note.permission === 'protected') { + if (req.user && req.user.logged_in) { return true } else { return false } + } else { + return true + } +} + +var isConnectionBusy = false +var connectionSocketQueue = [] +var isDisconnectBusy = false +var disconnectSocketQueue = [] + +function finishConnection (socket, noteId, socketId) { + // if no valid info provided will drop the client + if (!socket || !notes[noteId] || !users[socketId]) { + return interruptConnection(socket, noteId, socketId) + } + // check view permission + if (!checkViewPermission(socket.request, notes[noteId])) { + interruptConnection(socket, noteId, socketId) + return failConnection(403, 'connection forbidden', socket) + } + let note = notes[noteId] + let user = users[socketId] + // update user color to author color + if (note.authors[user.userid]) { + user.color = users[socket.id].color = note.authors[user.userid].color + } + note.users[socket.id] = user + note.socks.push(socket) + note.server.addClient(socket) + note.server.setName(socket, user.name) + note.server.setColor(socket, user.color) + + // update user note history + updateHistory(user.userid, note) + + emitOnlineUsers(socket) + emitRefresh(socket) + + // clear finished socket in queue + clearSocketQueue(connectionSocketQueue, socket) + // seek for next socket + connectNextSocket() + + if (config.debug) { + let noteId = socket.noteId + logger.info('SERVER connected a client to [' + noteId + ']:') + logger.info(JSON.stringify(user)) + // logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)) + }) + } +} + +function startConnection (socket) { + if (isConnectionBusy) return + isConnectionBusy = true + + var noteId = socket.noteId + if (!noteId) { + return failConnection(404, 'note id not found', socket) + } + + if (!notes[noteId]) { + var include = [{ + model: models.User, + as: 'owner' + }, { + model: models.User, + as: 'lastchangeuser' + }, { + model: models.Author, + as: 'authors', + include: [{ + model: models.User, + as: 'user' + }] + }] + + models.Note.findOne({ + where: { + id: noteId + }, + include: include + }).then(function (note) { + if (!note) { + return failConnection(404, 'note not found', socket) + } + var owner = note.ownerId + var ownerprofile = note.owner ? models.User.getProfile(note.owner) : null + + var lastchangeuser = note.lastchangeuserId + var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null + + var body = note.content + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback) + + var authors = {} + for (var i = 0; i < note.authors.length; i++) { + var author = note.authors[i] + var profile = models.User.getProfile(author.user) + authors[author.userId] = { + userid: author.userId, + color: author.color, + photo: profile.photo, + name: profile.name + } + } + + notes[noteId] = { + id: noteId, + alias: note.alias, + title: note.title, + owner: owner, + ownerprofile: ownerprofile, + permission: note.permission, + lastchangeuser: lastchangeuser, + lastchangeuserprofile: lastchangeuserprofile, + socks: [], + users: {}, + tempUsers: {}, + createtime: moment(createtime).valueOf(), + updatetime: moment(updatetime).valueOf(), + server: server, + authors: authors, + authorship: note.authorship + } + + return finishConnection(socket, noteId, socket.id) + }).catch(function (err) { + return failConnection(500, err, socket) + }) + } else { + return finishConnection(socket, noteId, socket.id) + } +} + +function failConnection (code, err, socket) { + logger.error(err) + // clear error socket in queue + clearSocketQueue(connectionSocketQueue, socket) + connectNextSocket() + // emit error info + socket.emit('info', { + code: code + }) + return socket.disconnect(true) +} + +function disconnect (socket) { + if (isDisconnectBusy) return + isDisconnectBusy = true + + if (config.debug) { + logger.info('SERVER disconnected a client') + logger.info(JSON.stringify(users[socket.id])) + } + + if (users[socket.id]) { + delete users[socket.id] + } + var noteId = socket.noteId + var note = notes[noteId] + if (note) { + // delete user in users + if (note.users[socket.id]) { + delete note.users[socket.id] + } + // remove sockets in the note socks + do { + var index = note.socks.indexOf(socket) + if (index !== -1) { + note.socks.splice(index, 1) + } + } while (index !== -1) + // remove note in notes if no user inside + if (Object.keys(note.users).length <= 0) { + if (note.server.isDirty) { + updateNote(note, function (err, _note) { + if (err) return logger.error('disconnect note failed: ' + err) + // clear server before delete to avoid memory leaks + note.server.document = '' + note.server.operations = [] + delete note.server + delete notes[noteId] + if (config.debug) { + // logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)) + }) + } + }) + } else { + delete note.server + delete notes[noteId] + } + } + } + emitOnlineUsers(socket) + + // clear finished socket in queue + clearSocketQueue(disconnectSocketQueue, socket) + // seek for next socket + isDisconnectBusy = false + if (disconnectSocketQueue.length > 0) { disconnect(disconnectSocketQueue[0]) } + + if (config.debug) { + // logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)) + }) + } +} + +function buildUserOutData (user) { + var out = { + id: user.id, + login: user.login, + userid: user.userid, + photo: user.photo, + color: user.color, + cursor: user.cursor, + name: user.name, + idle: user.idle, + type: user.type + } + return out +} + +function updateUserData (socket, user) { + // retrieve user data from passport + if (socket.request.user && socket.request.user.logged_in) { + var profile = models.User.getProfile(socket.request.user) + user.photo = profile.photo + user.name = profile.name + user.userid = socket.request.user.id + user.login = true + } else { + user.userid = null + user.name = 'Guest ' + chance.last() + user.login = false + } +} + +function ifMayEdit (socket, callback) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + var mayEdit = true + switch (note.permission) { + case 'freely': + // not blocking anyone + break + case 'editable': case 'limited': + // only login user can change + if (!socket.request.user || !socket.request.user.logged_in) { mayEdit = false } + break + case 'locked': case 'private': case 'protected': + // only owner can change + if (!note.owner || note.owner !== socket.request.user.id) { mayEdit = false } + break + } + // if user may edit and this is a text operation + if (socket.origin === 'operation' && mayEdit) { + // save for the last change user id + if (socket.request.user && socket.request.user.logged_in) { + note.lastchangeuser = socket.request.user.id + } else { + note.lastchangeuser = null + } + } + return callback(mayEdit) +} + +function operationCallback (socket, operation) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + var userId = null + // save authors + if (socket.request.user && socket.request.user.logged_in) { + var user = users[socket.id] + if (!user) return + userId = socket.request.user.id + if (!note.authors[userId]) { + models.Author.findOrCreate({ + where: { + noteId: noteId, + userId: userId + }, + defaults: { + noteId: noteId, + userId: userId, + color: user.color + } + }).spread(function (author, created) { + if (author) { + note.authors[author.userId] = { + userid: author.userId, + color: author.color, + photo: user.photo, + name: user.name + } + } + }).catch(function (err) { + return logger.error('operation callback failed: ' + err) + }) + } + note.tempUsers[userId] = Date.now() + } + // save authorship - use timer here because it's an O(n) complexity algorithm + setImmediate(function () { + note.authorship = models.Note.updateAuthorshipByOperation(operation, userId, note.authorship) + }) +} + +function updateHistory (userId, note, time) { + var noteId = note.alias ? note.alias : LZString.compressToBase64(note.id) + if (note.server) history.updateHistory(userId, noteId, note.server.document, time) +} + +function connection (socket) { + if (config.maintenance) return + parseNoteIdFromSocket(socket, function (err, noteId) { + if (err) { + return failConnection(500, err, socket) + } + if (!noteId) { + return failConnection(404, 'note id not found', socket) + } + + if (isDuplicatedInSocketQueue(socket, connectionSocketQueue)) return + + // store noteId in this socket session + socket.noteId = noteId + + // initialize user data + // random color + var color = randomcolor() + // make sure color not duplicated or reach max random count + if (notes[noteId]) { + var randomcount = 0 + var maxrandomcount = 10 + var found = false + do { + Object.keys(notes[noteId].users).forEach(function (user) { + if (user.color === color) { + found = true + } + }) + if (found) { + color = randomcolor() + randomcount++ + } + } while (found && randomcount < maxrandomcount) + } + // create user data + users[socket.id] = { + id: socket.id, + address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, + 'user-agent': socket.handshake.headers['user-agent'], + color: color, + cursor: null, + login: false, + userid: null, + name: null, + idle: false, + type: null + } + updateUserData(socket, users[socket.id]) + + // start connection + connectionSocketQueue.push(socket) + startConnection(socket) + }) + + // received client refresh request + socket.on('refresh', function () { + emitRefresh(socket) + }) + + // received user status + socket.on('user status', function (data) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + if (config.debug) { logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)) } + if (data) { + user.idle = data.idle + user.type = data.type + } + emitUserStatus(socket) + }) + + // received note permission change request + socket.on('permission', function (permission) { + // need login to do more actions + if (socket.request.user && socket.request.user.logged_in) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + // Only owner can change permission + if (note.owner && note.owner === socket.request.user.id) { + if (permission === 'freely' && !config.allowanonymous) return + note.permission = permission + models.Note.update({ + permission: permission + }, { + where: { + id: noteId + } + }).then(function (count) { + if (!count) { + return + } + var out = { + permission: permission + } + realtime.io.to(note.id).emit('permission', out) + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i] + if (typeof sock !== 'undefined' && sock) { + // check view permission + if (!checkViewPermission(sock.request, note)) { + sock.emit('info', { + code: 403 + }) + setTimeout(function () { + sock.disconnect(true) + }, 0) + } + } + } + }).catch(function (err) { + return logger.error('update note permission failed: ' + err) + }) + } + } + }) + + // delete a note + socket.on('delete', function () { + // need login to do more actions + if (socket.request.user && socket.request.user.logged_in) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + // Only owner can delete note + if (note.owner && note.owner === socket.request.user.id) { + models.Note.destroy({ + where: { + id: noteId + } + }).then(function (count) { + if (!count) return + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i] + if (typeof sock !== 'undefined' && sock) { + sock.emit('delete') + setTimeout(function () { + sock.disconnect(true) + }, 0) + } + } + }).catch(function (err) { + return logger.error('delete note failed: ' + err) + }) + } + } + }) + + // reveiced when user logout or changed + socket.on('user changed', function () { + logger.info('user changed') + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var user = notes[noteId].users[socket.id] + if (!user) return + updateUserData(socket, user) + emitOnlineUsers(socket) + }) + + // received sync of online users request + socket.on('online users', function () { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var users = [] + Object.keys(notes[noteId].users).forEach(function (key) { + var user = notes[noteId].users[key] + if (user) { users.push(buildUserOutData(user)) } + }) var out = { - id: user.id, - login: user.login, - userid: user.userid, - photo: user.photo, - color: user.color, - cursor: user.cursor, - name: user.name, - idle: user.idle, - type: user.type - }; - return out; -} - -function updateUserData(socket, user) { - //retrieve user data from passport - if (socket.request.user && socket.request.user.logged_in) { - var profile = models.User.getProfile(socket.request.user); - user.photo = profile.photo; - user.name = profile.name; - user.userid = socket.request.user.id; - user.login = true; - } else { - user.userid = null; - user.name = 'Guest ' + chance.last(); - user.login = false; + users: users } -} + socket.emit('online users', out) + }) -function ifMayEdit(socket, callback) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - var mayEdit = true; - switch (note.permission) { - case "freely": - //not blocking anyone - break; - case "editable": case "limited": - //only login user can change - if (!socket.request.user || !socket.request.user.logged_in) - mayEdit = false; - break; - case "locked": case "private": case "protected": - //only owner can change - if (!note.owner || note.owner != socket.request.user.id) - mayEdit = false; - break; + // check version + socket.on('version', function () { + socket.emit('version', { + version: config.version, + minimumCompatibleVersion: config.minimumCompatibleVersion + }) + }) + + // received cursor focus + socket.on('cursor focus', function (data) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + user.cursor = data + var out = buildUserOutData(user) + socket.broadcast.to(noteId).emit('cursor focus', out) + }) + + // received cursor activity + socket.on('cursor activity', function (data) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + user.cursor = data + var out = buildUserOutData(user) + socket.broadcast.to(noteId).emit('cursor activity', out) + }) + + // received cursor blur + socket.on('cursor blur', function () { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + user.cursor = null + var out = { + id: socket.id } - //if user may edit and this is a text operation - if (socket.origin == 'operation' && mayEdit) { - //save for the last change user id - if (socket.request.user && socket.request.user.logged_in) { - note.lastchangeuser = socket.request.user.id; - } else { - note.lastchangeuser = null; - } - } - return callback(mayEdit); + socket.broadcast.to(noteId).emit('cursor blur', out) + }) + + // when a new client disconnect + socket.on('disconnect', function () { + if (isDuplicatedInSocketQueue(socket, disconnectSocketQueue)) return + disconnectSocketQueue.push(socket) + disconnect(socket) + }) } -function operationCallback(socket, operation) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - var userId = null; - // save authors - if (socket.request.user && socket.request.user.logged_in) { - var user = users[socket.id]; - if (!user) return; - userId = socket.request.user.id; - if (!note.authors[userId]) { - models.Author.findOrCreate({ - where: { - noteId: noteId, - userId: userId - }, - defaults: { - noteId: noteId, - userId: userId, - color: user.color - } - }).spread(function (author, created) { - if (author) { - note.authors[author.userId] = { - userid: author.userId, - color: author.color, - photo: user.photo, - name: user.name - }; - } - }).catch(function (err) { - return logger.error('operation callback failed: ' + err); - }); - } - note.tempUsers[userId] = Date.now(); - } - // save authorship - use timer here because it's an O(n) complexity algorithm - setImmediate(function () { - note.authorship = models.Note.updateAuthorshipByOperation(operation, userId, note.authorship); - }); -} - -function updateHistory(userId, note, time) { - var noteId = note.alias ? note.alias : LZString.compressToBase64(note.id); - if (note.server) history.updateHistory(userId, noteId, note.server.document, time); -} - -function connection(socket) { - if (config.maintenance) return; - parseNoteIdFromSocket(socket, function (err, noteId) { - if (err) { - return failConnection(500, err, socket); - } - if (!noteId) { - return failConnection(404, 'note id not found', socket); - } - - if (isDuplicatedInSocketQueue(socket, connectionSocketQueue)) return; - - // store noteId in this socket session - socket.noteId = noteId; - - //initialize user data - //random color - var color = randomcolor(); - //make sure color not duplicated or reach max random count - if (notes[noteId]) { - var randomcount = 0; - var maxrandomcount = 10; - var found = false; - do { - Object.keys(notes[noteId].users).forEach(function (user) { - if (user.color == color) { - found = true; - return; - } - }); - if (found) { - color = randomcolor(); - randomcount++; - } - } while (found && randomcount < maxrandomcount); - } - //create user data - users[socket.id] = { - id: socket.id, - address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, - 'user-agent': socket.handshake.headers['user-agent'], - color: color, - cursor: null, - login: false, - userid: null, - name: null, - idle: false, - type: null - }; - updateUserData(socket, users[socket.id]); - - //start connection - connectionSocketQueue.push(socket); - startConnection(socket); - }); - - //received client refresh request - socket.on('refresh', function () { - emitRefresh(socket); - }); - - //received user status - socket.on('user status', function (data) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - if (config.debug) - logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)); - if (data) { - user.idle = data.idle; - user.type = data.type; - } - emitUserStatus(socket); - }); - - //received note permission change request - socket.on('permission', function (permission) { - //need login to do more actions - if (socket.request.user && socket.request.user.logged_in) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - //Only owner can change permission - if (note.owner && note.owner == socket.request.user.id) { - if (permission == 'freely' && !config.allowanonymous) return; - note.permission = permission; - models.Note.update({ - permission: permission - }, { - where: { - id: noteId - } - }).then(function (count) { - if (!count) { - return; - } - var out = { - permission: permission - }; - realtime.io.to(note.id).emit('permission', out); - for (var i = 0, l = note.socks.length; i < l; i++) { - var sock = note.socks[i]; - if (typeof sock !== 'undefined' && sock) { - // check view permission - if (!checkViewPermission(sock.request, note)) { - sock.emit('info', { - code: 403 - }); - setTimeout(function () { - sock.disconnect(true); - }, 0); - } - } - } - }).catch(function (err) { - return logger.error('update note permission failed: ' + err); - }); - } - } - }); - - // delete a note - socket.on('delete', function () { - //need login to do more actions - if (socket.request.user && socket.request.user.logged_in) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - //Only owner can delete note - if (note.owner && note.owner == socket.request.user.id) { - models.Note.destroy({ - where: { - id: noteId - } - }).then(function (count) { - if (!count) return; - for (var i = 0, l = note.socks.length; i < l; i++) { - var sock = note.socks[i]; - if (typeof sock !== 'undefined' && sock) { - sock.emit('delete'); - setTimeout(function () { - sock.disconnect(true); - }, 0); - } - } - }).catch(function (err) { - return logger.error('delete note failed: ' + err); - }); - } - } - }); - - //reveiced when user logout or changed - socket.on('user changed', function () { - logger.info('user changed'); - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var user = notes[noteId].users[socket.id]; - if (!user) return; - updateUserData(socket, user); - emitOnlineUsers(socket); - }); - - //received sync of online users request - socket.on('online users', function () { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var users = []; - Object.keys(notes[noteId].users).forEach(function (key) { - var user = notes[noteId].users[key]; - if (user) - users.push(buildUserOutData(user)); - }); - var out = { - users: users - }; - socket.emit('online users', out); - }); - - //check version - socket.on('version', function () { - socket.emit('version', { - version: config.version, - minimumCompatibleVersion: config.minimumCompatibleVersion - }); - }); - - //received cursor focus - socket.on('cursor focus', function (data) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - user.cursor = data; - var out = buildUserOutData(user); - socket.broadcast.to(noteId).emit('cursor focus', out); - }); - - //received cursor activity - socket.on('cursor activity', function (data) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - user.cursor = data; - var out = buildUserOutData(user); - socket.broadcast.to(noteId).emit('cursor activity', out); - }); - - //received cursor blur - socket.on('cursor blur', function () { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - user.cursor = null; - var out = { - id: socket.id - }; - socket.broadcast.to(noteId).emit('cursor blur', out); - }); - - //when a new client disconnect - socket.on('disconnect', function () { - if (isDuplicatedInSocketQueue(socket, disconnectSocketQueue)) return; - disconnectSocketQueue.push(socket); - disconnect(socket); - }); -} - -module.exports = realtime; \ No newline at end of file +module.exports = realtime diff --git a/lib/response.js b/lib/response.js index 585d1d5..31fa18b 100755 --- a/lib/response.js +++ b/lib/response.js @@ -1,609 +1,601 @@ -//response -//external modules -var fs = require('fs'); -var path = require('path'); -var markdownpdf = require("markdown-pdf"); -var LZString = require('lz-string'); -var S = require('string'); -var shortId = require('shortid'); -var querystring = require('querystring'); -var request = require('request'); -var moment = require('moment'); +// response +// external modules +var fs = require('fs') +var markdownpdf = require('markdown-pdf') +var LZString = require('lz-string') +var shortId = require('shortid') +var querystring = require('querystring') +var request = require('request') +var moment = require('moment') -//core -var config = require("./config.js"); -var logger = require("./logger.js"); -var models = require("./models"); +// core +var config = require('./config.js') +var logger = require('./logger.js') +var models = require('./models') -//public +// public var response = { - errorForbidden: function (res) { - responseError(res, "403", "Forbidden", "oh no."); - }, - errorNotFound: function (res) { - responseError(res, "404", "Not Found", "oops."); - }, - errorBadRequest: function (res) { - responseError(res, "400", "Bad Request", "something not right."); - }, - errorInternalError: function (res) { - responseError(res, "500", "Internal Error", "wtf."); - }, - errorServiceUnavailable: function (res) { - res.status(503).send("I'm busy right now, try again later."); - }, - newNote: newNote, - showNote: showNote, - showPublishNote: showPublishNote, - showPublishSlide: showPublishSlide, - showIndex: showIndex, - noteActions: noteActions, - publishNoteActions: publishNoteActions, - publishSlideActions: publishSlideActions, - githubActions: githubActions, - gitlabActions: gitlabActions -}; - -function responseError(res, code, detail, msg) { - res.status(code).render(config.errorpath, { - url: config.serverurl, - title: code + ' ' + detail + ' ' + msg, - code: code, - detail: detail, - msg: msg, - useCDN: config.usecdn - }); + errorForbidden: function (res) { + responseError(res, '403', 'Forbidden', 'oh no.') + }, + errorNotFound: function (res) { + responseError(res, '404', 'Not Found', 'oops.') + }, + errorBadRequest: function (res) { + responseError(res, '400', 'Bad Request', 'something not right.') + }, + errorInternalError: function (res) { + responseError(res, '500', 'Internal Error', 'wtf.') + }, + errorServiceUnavailable: function (res) { + res.status(503).send("I'm busy right now, try again later.") + }, + newNote: newNote, + showNote: showNote, + showPublishNote: showPublishNote, + showPublishSlide: showPublishSlide, + showIndex: showIndex, + noteActions: noteActions, + publishNoteActions: publishNoteActions, + publishSlideActions: publishSlideActions, + githubActions: githubActions, + gitlabActions: gitlabActions } -function showIndex(req, res, next) { - res.render(config.indexpath, { - url: config.serverurl, - useCDN: config.usecdn, - allowAnonymous: config.allowanonymous, - facebook: config.facebook, - twitter: config.twitter, - github: config.github, - gitlab: config.gitlab, - dropbox: config.dropbox, - google: config.google, - ldap: config.ldap, - email: config.email, - allowemailregister: config.allowemailregister, - signin: req.isAuthenticated(), - infoMessage: req.flash('info'), - errorMessage: req.flash('error') - }); +function responseError (res, code, detail, msg) { + res.status(code).render(config.errorpath, { + url: config.serverurl, + title: code + ' ' + detail + ' ' + msg, + code: code, + detail: detail, + msg: msg, + useCDN: config.usecdn + }) } -function responseHackMD(res, note) { - var body = note.content; - var extracted = models.Note.extractMeta(body); - var meta = models.Note.parseMeta(extracted.meta); - var title = models.Note.decodeTitle(note.title); - title = models.Note.generateWebTitle(meta.title || title); - res.set({ - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.render(config.hackmdpath, { - url: config.serverurl, - title: title, - useCDN: config.usecdn, - allowAnonymous: config.allowanonymous, - facebook: config.facebook, - twitter: config.twitter, - github: config.github, - gitlab: config.gitlab, - dropbox: config.dropbox, - google: config.google, - ldap: config.ldap, - email: config.email, - allowemailregister: config.allowemailregister - }); +function showIndex (req, res, next) { + res.render(config.indexpath, { + url: config.serverurl, + useCDN: config.usecdn, + allowAnonymous: config.allowanonymous, + facebook: config.facebook, + twitter: config.twitter, + github: config.github, + gitlab: config.gitlab, + dropbox: config.dropbox, + google: config.google, + ldap: config.ldap, + email: config.email, + allowemailregister: config.allowemailregister, + signin: req.isAuthenticated(), + infoMessage: req.flash('info'), + errorMessage: req.flash('error') + }) } -function newNote(req, res, next) { - var owner = null; - if (req.isAuthenticated()) { - owner = req.user.id; - } else if (!config.allowanonymous) { - return response.errorForbidden(res); +function responseHackMD (res, note) { + var body = note.content + var extracted = models.Note.extractMeta(body) + var meta = models.Note.parseMeta(extracted.meta) + var title = models.Note.decodeTitle(note.title) + title = models.Note.generateWebTitle(meta.title || title) + res.set({ + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.render(config.hackmdpath, { + url: config.serverurl, + title: title, + useCDN: config.usecdn, + allowAnonymous: config.allowanonymous, + facebook: config.facebook, + twitter: config.twitter, + github: config.github, + gitlab: config.gitlab, + dropbox: config.dropbox, + google: config.google, + ldap: config.ldap, + email: config.email, + allowemailregister: config.allowemailregister + }) +} + +function newNote (req, res, next) { + var owner = null + if (req.isAuthenticated()) { + owner = req.user.id + } else if (!config.allowanonymous) { + return response.errorForbidden(res) + } + models.Note.create({ + ownerId: owner, + alias: req.alias ? req.alias : null + }).then(function (note) { + return res.redirect(config.serverurl + '/' + LZString.compressToBase64(note.id)) + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) +} + +function checkViewPermission (req, note) { + if (note.permission === 'private') { + if (!req.isAuthenticated() || note.ownerId !== req.user.id) { return false } else { return true } + } else if (note.permission === 'limited' || note.permission === 'protected') { + if (!req.isAuthenticated()) { return false } else { return true } + } else { + return true + } +} + +function findNote (req, res, callback, include) { + var noteId = req.params.noteId + var id = req.params.noteId || req.params.shortid + models.Note.parseNoteId(id, function (err, _id) { + if (err) { + logger.log(err) } - models.Note.create({ - ownerId: owner, - alias: req.alias ? req.alias : null + models.Note.findOne({ + where: { + id: _id + }, + include: include || null }).then(function (note) { - return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id)); + if (!note) { + if (config.allowfreeurl && noteId) { + req.alias = noteId + return newNote(req, res) + } else { + return response.errorNotFound(res) + } + } + if (!checkViewPermission(req, note)) { + return response.errorForbidden(res) + } else { + return callback(note) + } }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); + logger.error(err) + return response.errorInternalError(res) + }) + }) } -function checkViewPermission(req, note) { - if (note.permission == 'private') { - if (!req.isAuthenticated() || note.ownerId != req.user.id) - return false; - else - return true; - } else if (note.permission == 'limited' || note.permission == 'protected') { - if(!req.isAuthenticated()) - return false; - else - return true; - } else { - return true; +function showNote (req, res, next) { + findNote(req, res, function (note) { + // force to use note id + var noteId = req.params.noteId + var id = LZString.compressToBase64(note.id) + if ((note.alias && noteId !== note.alias) || (!note.alias && noteId !== id)) { return res.redirect(config.serverurl + '/' + (note.alias || id)) } + return responseHackMD(res, note) + }) +} + +function showPublishNote (req, res, next) { + var include = [{ + model: models.User, + as: 'owner' + }, { + model: models.User, + as: 'lastchangeuser' + }] + findNote(req, res, function (note) { + // force to use short id + var shortid = req.params.shortid + if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { + return res.redirect(config.serverurl + '/s/' + (note.alias || note.shortid)) } -} - -function findNote(req, res, callback, include) { - var noteId = req.params.noteId; - var id = req.params.noteId || req.params.shortid; - models.Note.parseNoteId(id, function (err, _id) { - models.Note.findOne({ - where: { - id: _id - }, - include: include || null - }).then(function (note) { - if (!note) { - if (config.allowfreeurl && noteId) { - req.alias = noteId; - return newNote(req, res); - } else { - return response.errorNotFound(res); - } - } - if (!checkViewPermission(req, note)) { - return response.errorForbidden(res); - } else { - return callback(note); - } - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); - }); -} - -function showNote(req, res, next) { - findNote(req, res, function (note) { - // force to use note id - var noteId = req.params.noteId; - var id = LZString.compressToBase64(note.id); - if ((note.alias && noteId != note.alias) || (!note.alias && noteId != id)) - return res.redirect(config.serverurl + "/" + (note.alias || id)); - return responseHackMD(res, note); - }); -} - -function showPublishNote(req, res, next) { - var include = [{ - model: models.User, - as: "owner" - }, { - model: models.User, - as: "lastchangeuser" - }]; - findNote(req, res, function (note) { - // force to use short id - var shortid = req.params.shortid; - if ((note.alias && shortid != note.alias) || (!note.alias && shortid != note.shortid)) - return res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid)); - note.increment('viewcount').then(function (note) { - if (!note) { - return response.errorNotFound(res); - } - var body = note.content; - var extracted = models.Note.extractMeta(body); - markdown = extracted.markdown; - meta = models.Note.parseMeta(extracted.meta); - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var title = models.Note.decodeTitle(note.title); - title = models.Note.generateWebTitle(meta.title || title); - var origin = config.serverurl; - var data = { - title: title, - description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), - viewcount: note.viewcount, - createtime: createtime, - updatetime: updatetime, - url: origin, - body: body, - useCDN: config.usecdn, - owner: note.owner ? note.owner.id : null, - ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, - lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, - lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, - robots: meta.robots || false, //default allow robots - GA: meta.GA, - disqus: meta.disqus - }; - return renderPublish(data, res); - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); - }, include); -} - -function renderPublish(data, res) { - res.set({ - 'Cache-Control': 'private' // only cache by client - }); - res.render(config.prettypath, data); -} - -function actionPublish(req, res, note) { - res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid)); -} - -function actionSlide(req, res, note) { - res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid)); -} - -function actionDownload(req, res, note) { - var body = note.content; - var title = models.Note.decodeTitle(note.title); - var filename = title; - filename = encodeURIComponent(filename); - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Content-Type': 'text/markdown; charset=UTF-8', - 'Cache-Control': 'private', - 'Content-disposition': 'attachment; filename=' + filename + '.md', - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(body); -} - -function actionInfo(req, res, note) { - var body = note.content; - var extracted = models.Note.extractMeta(body); - var markdown = extracted.markdown; - var meta = models.Note.parseMeta(extracted.meta); - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var title = models.Note.decodeTitle(note.title); - var data = { - title: meta.title || title, + note.increment('viewcount').then(function (note) { + if (!note) { + return response.errorNotFound(res) + } + var body = note.content + var extracted = models.Note.extractMeta(body) + var markdown = extracted.markdown + var meta = models.Note.parseMeta(extracted.meta) + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var title = models.Note.decodeTitle(note.title) + title = models.Note.generateWebTitle(meta.title || title) + var origin = config.serverurl + var data = { + title: title, description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), viewcount: note.viewcount, createtime: createtime, - updatetime: updatetime - }; - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API + updatetime: updatetime, + url: origin, + body: body, + useCDN: config.usecdn, + owner: note.owner ? note.owner.id : null, + ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, + lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, + lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, + robots: meta.robots || false, // default allow robots + GA: meta.GA, + disqus: meta.disqus + } + return renderPublish(data, res) + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + }, include) +} + +function renderPublish (data, res) { + res.set({ + 'Cache-Control': 'private' // only cache by client + }) + res.render(config.prettypath, data) +} + +function actionPublish (req, res, note) { + res.redirect(config.serverurl + '/s/' + (note.alias || note.shortid)) +} + +function actionSlide (req, res, note) { + res.redirect(config.serverurl + '/p/' + (note.alias || note.shortid)) +} + +function actionDownload (req, res, note) { + var body = note.content + var title = models.Note.decodeTitle(note.title) + var filename = title + filename = encodeURIComponent(filename) + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API + 'Access-Control-Allow-Headers': 'Range', + 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', + 'Content-Type': 'text/markdown; charset=UTF-8', + 'Cache-Control': 'private', + 'Content-disposition': 'attachment; filename=' + filename + '.md', + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(body) +} + +function actionInfo (req, res, note) { + var body = note.content + var extracted = models.Note.extractMeta(body) + var markdown = extracted.markdown + var meta = models.Note.parseMeta(extracted.meta) + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var title = models.Note.decodeTitle(note.title) + var data = { + title: meta.title || title, + description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), + viewcount: note.viewcount, + createtime: createtime, + updatetime: updatetime + } + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API + 'Access-Control-Allow-Headers': 'Range', + 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(data) +} + +function actionPDF (req, res, note) { + var body = note.content + var extracted = models.Note.extractMeta(body) + var title = models.Note.decodeTitle(note.title) + + if (!fs.existsSync(config.tmppath)) { + fs.mkdirSync(config.tmppath) + } + var path = config.tmppath + '/' + Date.now() + '.pdf' + markdownpdf().from.string(extracted.markdown).to(path, function () { + var stream = fs.createReadStream(path) + var filename = title + // Be careful of special characters + filename = encodeURIComponent(filename) + // Ideally this should strip them + res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"') + res.setHeader('Cache-Control', 'private') + res.setHeader('Content-Type', 'application/pdf; charset=UTF-8') + res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling + stream.pipe(res) + fs.unlink(path) + }) +} + +function actionGist (req, res, note) { + var data = { + client_id: config.github.clientID, + redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist', + scope: 'gist', + state: shortId.generate() + } + var query = querystring.stringify(data) + res.redirect('https://github.com/login/oauth/authorize?' + query) +} + +function actionRevision (req, res, note) { + var actionId = req.params.actionId + if (actionId) { + var time = moment(parseInt(actionId)) + if (time.isValid()) { + models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) { + if (err) { + logger.error(err) + return response.errorInternalError(res) + } + if (!content) { + return response.errorNotFound(res) + } + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API + 'Access-Control-Allow-Headers': 'Range', + 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(content) + }) + } else { + return response.errorNotFound(res) + } + } else { + models.Revision.getNoteRevisions(note, function (err, data) { + if (err) { + logger.error(err) + return response.errorInternalError(res) + } + var out = { + revision: data + } + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API 'Access-Control-Allow-Headers': 'Range', 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', 'Cache-Control': 'private', // only cache by client 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(data); + }) + res.send(out) + }) + } } -function actionPDF(req, res, note) { - var body = note.content; - var extracted = models.Note.extractMeta(body); - var title = models.Note.decodeTitle(note.title); - - if (!fs.existsSync(config.tmppath)) { - fs.mkdirSync(config.tmppath); +function noteActions (req, res, next) { + var noteId = req.params.noteId + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'publish': + case 'pretty': // pretty deprecated + actionPublish(req, res, note) + break + case 'slide': + actionSlide(req, res, note) + break + case 'download': + actionDownload(req, res, note) + break + case 'info': + actionInfo(req, res, note) + break + case 'pdf': + actionPDF(req, res, note) + break + case 'gist': + actionGist(req, res, note) + break + case 'revision': + actionRevision(req, res, note) + break + default: + return res.redirect(config.serverurl + '/' + noteId) } - var path = config.tmppath + '/' + Date.now() + '.pdf'; - markdownpdf().from.string(extracted.markdown).to(path, function () { - var stream = fs.createReadStream(path); - var filename = title; - // Be careful of special characters - filename = encodeURIComponent(filename); - // Ideally this should strip them - res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"'); - res.setHeader('Cache-Control', 'private'); - res.setHeader('Content-Type', 'application/pdf; charset=UTF-8'); - res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling - stream.pipe(res); - fs.unlink(path); - }); + }) } -function actionGist(req, res, note) { +function publishNoteActions (req, res, next) { + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'edit': + res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))) + break + default: + res.redirect(config.serverurl + '/s/' + note.shortid) + break + } + }) +} + +function publishSlideActions (req, res, next) { + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'edit': + res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))) + break + default: + res.redirect(config.serverurl + '/p/' + note.shortid) + break + } + }) +} + +function githubActions (req, res, next) { + var noteId = req.params.noteId + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'gist': + githubActionGist(req, res, note) + break + default: + res.redirect(config.serverurl + '/' + noteId) + break + } + }) +} + +function githubActionGist (req, res, note) { + var code = req.query.code + var state = req.query.state + if (!code || !state) { + return response.errorForbidden(res) + } else { var data = { - client_id: config.github.clientID, - redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist', - scope: "gist", - state: shortId.generate() - }; - var query = querystring.stringify(data); - res.redirect("https://github.com/login/oauth/authorize?" + query); -} - -function actionRevision(req, res, note) { - var actionId = req.params.actionId; - if (actionId) { - var time = moment(parseInt(actionId)); - if (time.isValid()) { - models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) { - if (err) { - logger.error(err); - return response.errorInternalError(res); - } - if (!content) { - return response.errorNotFound(res); - } - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(content); - }); - } else { - return response.errorNotFound(res); - } - } else { - models.Revision.getNoteRevisions(note, function (err, data) { - if (err) { - logger.error(err); - return response.errorInternalError(res); - } - var out = { - revision: data - }; - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(out); - }); + client_id: config.github.clientID, + client_secret: config.github.clientSecret, + code: code, + state: state } -} - -function noteActions(req, res, next) { - var noteId = req.params.noteId; - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "publish": - case "pretty": //pretty deprecated - actionPublish(req, res, note); - break; - case "slide": - actionSlide(req, res, note); - break; - case "download": - actionDownload(req, res, note); - break; - case "info": - actionInfo(req, res, note); - break; - case "pdf": - actionPDF(req, res, note); - break; - case "gist": - actionGist(req, res, note); - break; - case "revision": - actionRevision(req, res, note); - break; - default: - return res.redirect(config.serverurl + '/' + noteId); - break; - } - }); -} - -function publishNoteActions(req, res, next) { - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "edit": - res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))); - break; - default: - res.redirect(config.serverurl + '/s/' + note.shortid); - break; - } - }); -} - -function publishSlideActions(req, res, next) { - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "edit": - res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))); - break; - default: - res.redirect(config.serverurl + '/p/' + note.shortid); - break; - } - }); -} - -function githubActions(req, res, next) { - var noteId = req.params.noteId; - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "gist": - githubActionGist(req, res, note); - break; - default: - res.redirect(config.serverurl + '/' + noteId); - break; - } - }); -} - -function githubActionGist(req, res, note) { - var code = req.query.code; - var state = req.query.state; - if (!code || !state) { - return response.errorForbidden(res); - } else { - var data = { - client_id: config.github.clientID, - client_secret: config.github.clientSecret, - code: code, - state: state - } - var auth_url = 'https://github.com/login/oauth/access_token'; - request({ - url: auth_url, - method: "POST", - json: data - }, function (error, httpResponse, body) { - if (!error && httpResponse.statusCode == 200) { - var access_token = body.access_token; - if (access_token) { - var content = note.content; - var title = models.Note.decodeTitle(note.title); - var filename = title.replace('/', ' ') + '.md'; - var gist = { - "files": {} - }; - gist.files[filename] = { - "content": content - }; - var gist_url = "https://api.github.com/gists"; - request({ - url: gist_url, - headers: { - 'User-Agent': 'HackMD', - 'Authorization': 'token ' + access_token - }, - method: "POST", - json: gist - }, function (error, httpResponse, body) { - if (!error && httpResponse.statusCode == 201) { - res.setHeader('referer', ''); - res.redirect(body.html_url); - } else { - return response.errorForbidden(res); - } - }); - } else { - return response.errorForbidden(res); - } + var authUrl = 'https://github.com/login/oauth/access_token' + request({ + url: authUrl, + method: 'POST', + json: data + }, function (error, httpResponse, body) { + if (!error && httpResponse.statusCode === 200) { + var accessToken = body.access_token + if (accessToken) { + var content = note.content + var title = models.Note.decodeTitle(note.title) + var filename = title.replace('/', ' ') + '.md' + var gist = { + 'files': {} + } + gist.files[filename] = { + 'content': content + } + var gistUrl = 'https://api.github.com/gists' + request({ + url: gistUrl, + headers: { + 'User-Agent': 'HackMD', + 'Authorization': 'token ' + accessToken + }, + method: 'POST', + json: gist + }, function (error, httpResponse, body) { + if (!error && httpResponse.statusCode === 201) { + res.setHeader('referer', '') + res.redirect(body.html_url) } else { - return response.errorForbidden(res); + return response.errorForbidden(res) } - }) - } -} - -function gitlabActions(req, res, next) { - var noteId = req.params.noteId; - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "projects": - gitlabActionProjects(req, res, note); - break; - default: - res.redirect(config.serverurl + '/' + noteId); - break; + }) + } else { + return response.errorForbidden(res) } - }); + } else { + return response.errorForbidden(res) + } + }) + } } -function gitlabActionProjects(req, res, note) { - if (req.isAuthenticated()) { - models.User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (!user) - return response.errorNotFound(res); - var ret = { baseURL: config.gitlab.baseURL }; - ret.accesstoken = user.accessToken; - ret.profileid = user.profileid; - request( - config.gitlab.baseURL + '/api/v3/projects?access_token=' + user.accessToken, - function(error, httpResponse, body) { - if (!error && httpResponse.statusCode == 200) { - ret.projects = JSON.parse(body); - return res.send(ret); - } else { - return res.send(ret); - } - } - ); - }).catch(function (err) { - logger.error('gitlab action projects failed: ' + err); - return response.errorInternalError(res); - }); - } else { - return response.errorForbidden(res); +function gitlabActions (req, res, next) { + var noteId = req.params.noteId + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'projects': + gitlabActionProjects(req, res, note) + break + default: + res.redirect(config.serverurl + '/' + noteId) + break } + }) } -function showPublishSlide(req, res, next) { - var include = [{ - model: models.User, - as: "owner" - }, { - model: models.User, - as: "lastchangeuser" - }]; - findNote(req, res, function (note) { - // force to use short id - var shortid = req.params.shortid; - if ((note.alias && shortid != note.alias) || (!note.alias && shortid != note.shortid)) - return res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid)); - note.increment('viewcount').then(function (note) { - if (!note) { - return response.errorNotFound(res); - } - var body = note.content; - var extracted = models.Note.extractMeta(body); - markdown = extracted.markdown; - meta = models.Note.parseMeta(extracted.meta); - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var title = models.Note.decodeTitle(note.title); - title = models.Note.generateWebTitle(meta.title || title); - var origin = config.serverurl; - var data = { - title: title, - description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), - viewcount: note.viewcount, - createtime: createtime, - updatetime: updatetime, - url: origin, - body: markdown, - meta: JSON.stringify(extracted.meta), - useCDN: config.usecdn, - owner: note.owner ? note.owner.id : null, - ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, - lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, - lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, - robots: meta.robots || false, //default allow robots - GA: meta.GA, - disqus: meta.disqus - }; - return renderPublishSlide(data, res); - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); - }, include); +function gitlabActionProjects (req, res, note) { + if (req.isAuthenticated()) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (!user) { return response.errorNotFound(res) } + var ret = { baseURL: config.gitlab.baseURL } + ret.accesstoken = user.accessToken + ret.profileid = user.profileid + request( + config.gitlab.baseURL + '/api/v3/projects?access_token=' + user.accessToken, + function (error, httpResponse, body) { + if (!error && httpResponse.statusCode === 200) { + ret.projects = JSON.parse(body) + return res.send(ret) + } else { + return res.send(ret) + } + } + ) + }).catch(function (err) { + logger.error('gitlab action projects failed: ' + err) + return response.errorInternalError(res) + }) + } else { + return response.errorForbidden(res) + } } -function renderPublishSlide(data, res) { - res.set({ - 'Cache-Control': 'private' // only cache by client - }); - res.render(config.slidepath, data); +function showPublishSlide (req, res, next) { + var include = [{ + model: models.User, + as: 'owner' + }, { + model: models.User, + as: 'lastchangeuser' + }] + findNote(req, res, function (note) { + // force to use short id + var shortid = req.params.shortid + if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { return res.redirect(config.serverurl + '/p/' + (note.alias || note.shortid)) } + note.increment('viewcount').then(function (note) { + if (!note) { + return response.errorNotFound(res) + } + var body = note.content + var extracted = models.Note.extractMeta(body) + var markdown = extracted.markdown + var meta = models.Note.parseMeta(extracted.meta) + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var title = models.Note.decodeTitle(note.title) + title = models.Note.generateWebTitle(meta.title || title) + var origin = config.serverurl + var data = { + title: title, + description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), + viewcount: note.viewcount, + createtime: createtime, + updatetime: updatetime, + url: origin, + body: markdown, + meta: JSON.stringify(extracted.meta), + useCDN: config.usecdn, + owner: note.owner ? note.owner.id : null, + ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, + lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, + lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, + robots: meta.robots || false, // default allow robots + GA: meta.GA, + disqus: meta.disqus + } + return renderPublishSlide(data, res) + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + }, include) } -module.exports = response; +function renderPublishSlide (data, res) { + res.set({ + 'Cache-Control': 'private' // only cache by client + }) + res.render(config.slidepath, data) +} + +module.exports = response diff --git a/lib/workers/dmpWorker.js b/lib/workers/dmpWorker.js index 8e69636..6a1da98 100644 --- a/lib/workers/dmpWorker.js +++ b/lib/workers/dmpWorker.js @@ -1,140 +1,137 @@ // external modules -var DiffMatchPatch = require('diff-match-patch'); -var dmp = new DiffMatchPatch(); +var DiffMatchPatch = require('diff-match-patch') +var dmp = new DiffMatchPatch() // core -var config = require("../config.js"); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -process.on('message', function(data) { - if (!data || !data.msg || !data.cacheKey) { - return logger.error('dmp worker error: not enough data'); - } - switch (data.msg) { - case 'create patch': - if (!data.hasOwnProperty('lastDoc') || !data.hasOwnProperty('currDoc')) { - return logger.error('dmp worker error: not enough data on create patch'); - } - try { - var patch = createPatch(data.lastDoc, data.currDoc); - process.send({ - msg: 'check', - result: patch, - cacheKey: data.cacheKey - }); - } catch (err) { - logger.error('dmp worker error', err); - process.send({ - msg: 'error', - error: err, - cacheKey: data.cacheKey - }); - } - break; - case 'get revision': - if (!data.hasOwnProperty('revisions') || !data.hasOwnProperty('count')) { - return logger.error('dmp worker error: not enough data on get revision'); - } - try { - var result = getRevision(data.revisions, data.count); - process.send({ - msg: 'check', - result: result, - cacheKey: data.cacheKey - }); - } catch (err) { - logger.error('dmp worker error', err); - process.send({ - msg: 'error', - error: err, - cacheKey: data.cacheKey - }); - } - break; - } -}); +process.on('message', function (data) { + if (!data || !data.msg || !data.cacheKey) { + return logger.error('dmp worker error: not enough data') + } + switch (data.msg) { + case 'create patch': + if (!data.hasOwnProperty('lastDoc') || !data.hasOwnProperty('currDoc')) { + return logger.error('dmp worker error: not enough data on create patch') + } + try { + var patch = createPatch(data.lastDoc, data.currDoc) + process.send({ + msg: 'check', + result: patch, + cacheKey: data.cacheKey + }) + } catch (err) { + logger.error('dmp worker error', err) + process.send({ + msg: 'error', + error: err, + cacheKey: data.cacheKey + }) + } + break + case 'get revision': + if (!data.hasOwnProperty('revisions') || !data.hasOwnProperty('count')) { + return logger.error('dmp worker error: not enough data on get revision') + } + try { + var result = getRevision(data.revisions, data.count) + process.send({ + msg: 'check', + result: result, + cacheKey: data.cacheKey + }) + } catch (err) { + logger.error('dmp worker error', err) + process.send({ + msg: 'error', + error: err, + cacheKey: data.cacheKey + }) + } + break + } +}) -function createPatch(lastDoc, currDoc) { - var ms_start = (new Date()).getTime(); - var diff = dmp.diff_main(lastDoc, currDoc); - var patch = dmp.patch_make(lastDoc, diff); - patch = dmp.patch_toText(patch); - var ms_end = (new Date()).getTime(); - if (config.debug) { - logger.info(patch); - logger.info((ms_end - ms_start) + 'ms'); - } - return patch; +function createPatch (lastDoc, currDoc) { + var msStart = (new Date()).getTime() + var diff = dmp.diff_main(lastDoc, currDoc) + var patch = dmp.patch_make(lastDoc, diff) + patch = dmp.patch_toText(patch) + var msEnd = (new Date()).getTime() + if (config.debug) { + logger.info(patch) + logger.info((msEnd - msStart) + 'ms') + } + return patch } -function getRevision(revisions, count) { - var ms_start = (new Date()).getTime(); - var startContent = null; - var lastPatch = []; - var applyPatches = []; - var authorship = []; - if (count <= Math.round(revisions.length / 2)) { - // start from top to target - for (var i = 0; i < count; i++) { - var revision = revisions[i]; - if (i == 0) { - startContent = revision.content || revision.lastContent; - } - if (i != count - 1) { - var patch = dmp.patch_fromText(revision.patch); - applyPatches = applyPatches.concat(patch); - } - lastPatch = revision.patch; - authorship = revision.authorship; - } - // swap DIFF_INSERT and DIFF_DELETE to achieve unpatching - for (var i = 0, l = applyPatches.length; i < l; i++) { - for (var j = 0, m = applyPatches[i].diffs.length; j < m; j++) { - var diff = applyPatches[i].diffs[j]; - if (diff[0] == DiffMatchPatch.DIFF_INSERT) - diff[0] = DiffMatchPatch.DIFF_DELETE; - else if (diff[0] == DiffMatchPatch.DIFF_DELETE) - diff[0] = DiffMatchPatch.DIFF_INSERT; - } - } - } else { - // start from bottom to target - var l = revisions.length - 1; - for (var i = l; i >= count - 1; i--) { - var revision = revisions[i]; - if (i == l) { - startContent = revision.lastContent; - authorship = revision.authorship; - } - if (revision.patch) { - var patch = dmp.patch_fromText(revision.patch); - applyPatches = applyPatches.concat(patch); - } - lastPatch = revision.patch; - authorship = revision.authorship; - } +function getRevision (revisions, count) { + var msStart = (new Date()).getTime() + var startContent = null + var lastPatch = [] + var applyPatches = [] + var authorship = [] + if (count <= Math.round(revisions.length / 2)) { + // start from top to target + for (let i = 0; i < count; i++) { + let revision = revisions[i] + if (i === 0) { + startContent = revision.content || revision.lastContent + } + if (i !== count - 1) { + let patch = dmp.patch_fromText(revision.patch) + applyPatches = applyPatches.concat(patch) + } + lastPatch = revision.patch + authorship = revision.authorship } - try { - var finalContent = dmp.patch_apply(applyPatches, startContent)[0]; - } catch (err) { - throw new Error(err); + // swap DIFF_INSERT and DIFF_DELETE to achieve unpatching + for (let i = 0, l = applyPatches.length; i < l; i++) { + for (let j = 0, m = applyPatches[i].diffs.length; j < m; j++) { + var diff = applyPatches[i].diffs[j] + if (diff[0] === DiffMatchPatch.DIFF_INSERT) { diff[0] = DiffMatchPatch.DIFF_DELETE } else if (diff[0] === DiffMatchPatch.DIFF_DELETE) { diff[0] = DiffMatchPatch.DIFF_INSERT } + } } - var data = { - content: finalContent, - patch: dmp.patch_fromText(lastPatch), - authorship: authorship - }; - var ms_end = (new Date()).getTime(); - if (config.debug) { - logger.info((ms_end - ms_start) + 'ms'); + } else { + // start from bottom to target + var l = revisions.length - 1 + for (var i = l; i >= count - 1; i--) { + let revision = revisions[i] + if (i === l) { + startContent = revision.lastContent + authorship = revision.authorship + } + if (revision.patch) { + let patch = dmp.patch_fromText(revision.patch) + applyPatches = applyPatches.concat(patch) + } + lastPatch = revision.patch + authorship = revision.authorship } - return data; + } + try { + var finalContent = dmp.patch_apply(applyPatches, startContent)[0] + } catch (err) { + throw new Error(err) + } + var data = { + content: finalContent, + patch: dmp.patch_fromText(lastPatch), + authorship: authorship + } + var msEnd = (new Date()).getTime() + if (config.debug) { + logger.info((msEnd - msStart) + 'ms') + } + return data } // log uncaught exception process.on('uncaughtException', function (err) { - logger.error('An uncaught exception has occured.'); - logger.error(err); - logger.error('Process will exit now.'); - process.exit(1); -}); \ No newline at end of file + logger.error('An uncaught exception has occured.') + logger.error(err) + logger.error('Process will exit now.') + process.exit(1) +}) diff --git a/package.json b/package.json index a179d93..2012dbd 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "app.js", "license": "MIT", "scripts": { - "test": "npm run-script lint", + "test": "node ./node_modules/standard/bin/cmd.js && npm run-script lint", "lint": "eslint .", "dev": "webpack --config webpack.config.js --progress --colors --watch", "build": "webpack --config webpack.production.js --progress --colors", @@ -165,8 +165,15 @@ "optimize-css-assets-webpack-plugin": "^1.3.0", "script-loader": "^0.7.0", "style-loader": "^0.13.1", + "standard": "^9.0.1", "url-loader": "^0.5.7", "webpack": "^1.14.0", "webpack-parallel-uglify-plugin": "^0.2.0" + }, + "standard": { + "ignore": [ + "lib/ot", + "public/vendor" + ] } }