\
- visited \
-
\
- \
- \
-
diff --git a/.travis.yml b/.travis.yml index ed8ab42f..235f84b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: node_js node_js: - 6 - 7 - - stable + - lts/boron env: - CXX=g++-4.8 addons: diff --git a/README.md b/README.md index 2afeba26..77aff69a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ HackMD === +[![Standard - JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) + [![Join the chat at https://gitter.im/hackmdio/hackmd][gitter-image]][gitter-url] [![build status][travis-image]][travis-url] diff --git a/app.js b/app.js index 67a62540..c68652bb 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 4b14e42c..ef1d6464 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 4d2fbf74..af4c22cd 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 e7fb3087..69337dc5 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 3afa03fe..92bd36ee 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 += ''; + var 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 61299c10..23e302da 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 3af490a9..20c0e03c 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 fa647d93..618bb4d7 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 239327ec..98381d4e 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 92ff6f7b..984920b8 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 b5aaf777..a97d3be5 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 0b0f149d..5e39c347 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 e83956e5..96babc2a 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 8b38d3f9..bef9ee21 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 c7360fed..d8dab30a 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 6eeff153..e770bb3a 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 dd93bf78..f7e533b7 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 c1db6886..cff795c7 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 585d1d54..31fa18b2 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 8e69636e..6a1da981 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/locales/ru.json b/locales/ru.json index a1a95aaf..f87f7c69 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,7 +1,7 @@ { "Collaborative markdown notes": "Совместные markdown заметки", "Realtime collaborative markdown notes on all platforms.": "Совместные markdown заметки в режиме реального времени на всех платформах.", - "Best way to write and share your knowledge in markdown.": "Лучший способ, чтобы записывать и делиться своими знаниями markdown.", + "Best way to write and share your knowledge in markdown.": "Лучший способ записывать свои знания и делиться ими в формате markdown.", "Intro": "Введение", "History": "История", "New guest note": "Новая гостевая заметка", @@ -101,4 +101,4 @@ "OR": "ИЛИ", "Export to Snippet": "Экспорт фрагмента кода", "Select Visibility Level": "Выберите уровень видимости" -} \ No newline at end of file +} diff --git a/package.json b/package.json index a179d93e..05789321 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "app.js", "license": "MIT", "scripts": { - "test": "npm run-script lint", - "lint": "eslint .", + "test": "npm run-script standard", + "standard": "node ./node_modules/standard/bin/cmd.js", "dev": "webpack --config webpack.config.js --progress --colors --watch", "build": "webpack --config webpack.production.js --progress --colors", "postinstall": "bin/heroku", @@ -152,7 +152,6 @@ "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.26.1", "ejs-loader": "^0.3.0", - "eslint": "^3.15.0", "exports-loader": "^0.6.3", "expose-loader": "^0.7.1", "extract-text-webpack-plugin": "^1.0.1", @@ -165,8 +164,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" + ] } } diff --git a/public/js/cover.js b/public/js/cover.js index bc6e73f9..a45a1c13 100644 --- a/public/js/cover.js +++ b/public/js/cover.js @@ -1,7 +1,10 @@ -require('./locale'); +/* eslint-env browser, jquery */ +/* global moment, serverurl */ -require('../css/cover.css'); -require('../css/site.css'); +require('./locale') + +require('../css/cover.css') +require('../css/site.css') import { checkIfAuth, @@ -9,7 +12,7 @@ import { getLoginState, resetCheckAuth, setloginStateChangeEvent -} from './lib/common/login'; +} from './lib/common/login' import { clearDuplicatedHistory, @@ -23,411 +26,403 @@ import { removeHistory, saveHistory, saveStorageHistoryToServer -} from './history'; +} from './history' -import { saveAs } from 'file-saver'; -import List from 'list.js'; -import S from 'string'; +import { saveAs } from 'file-saver' +import List from 'list.js' +import S from 'string' const options = { - valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'], - item: '
\
- visited \
-
\
- \
- \
-
' +
+ ' visited ' +
+ '
' +
+ '' +
+ '' +
+ '
${highlighted}
\n`;
-};
+ return `${highlighted}
\n`
+}
/* Defined regex markdown it plugins */
-import Plugin from 'markdown-it-regexp';
+import Plugin from 'markdown-it-regexp'
-//youtube
+// youtube
const youtubePlugin = new Plugin(
// regexp to match
/{%youtube\s*([\d\D]*?)\s*%}/,
(match, utils) => {
- const videoid = match[1];
- if (!videoid) return;
- const div = $('');
- div.attr('data-videoid', videoid);
- const thumbnail_src = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`;
- const image = ``;
- div.append(image);
- const icon = '';
- div.append(icon);
- return div[0].outerHTML;
+ const videoid = match[1]
+ if (!videoid) return
+ const div = $('')
+ div.attr('data-videoid', videoid)
+ const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`
+ const image = ``
+ div.append(image)
+ const icon = ''
+ div.append(icon)
+ return div[0].outerHTML
}
-);
-//vimeo
+)
+// vimeo
const vimeoPlugin = new Plugin(
// regexp to match
/{%vimeo\s*([\d\D]*?)\s*%}/,
(match, utils) => {
- const videoid = match[1];
- if (!videoid) return;
- const div = $('');
- div.attr('data-videoid', videoid);
- const icon = '';
- div.append(icon);
- return div[0].outerHTML;
+ const videoid = match[1]
+ if (!videoid) return
+ const div = $('')
+ div.attr('data-videoid', videoid)
+ const icon = ''
+ div.append(icon)
+ return div[0].outerHTML
}
-);
-//gist
+)
+// gist
const gistPlugin = new Plugin(
// regexp to match
/{%gist\s*([\d\D]*?)\s*%}/,
(match, utils) => {
- const gistid = match[1];
- const code = `
`;
- return code;
+ const gistid = match[1]
+ const code = `
`
+ return code
}
-);
-//TOC
+)
+// TOC
const tocPlugin = new Plugin(
// regexp to match
/^\[TOC\]$/i,
(match, utils) => ''
-);
-//slideshare
+)
+// slideshare
const slidesharePlugin = new Plugin(
// regexp to match
/{%slideshare\s*([\d\D]*?)\s*%}/,
(match, utils) => {
- const slideshareid = match[1];
- const div = $('');
- div.attr('data-slideshareid', slideshareid);
- return div[0].outerHTML;
+ const slideshareid = match[1]
+ const div = $('')
+ div.attr('data-slideshareid', slideshareid)
+ return div[0].outerHTML
}
-);
-//speakerdeck
+)
+// speakerdeck
const speakerdeckPlugin = new Plugin(
// regexp to match
/{%speakerdeck\s*([\d\D]*?)\s*%}/,
(match, utils) => {
- const speakerdeckid = match[1];
- const div = $('');
- div.attr('data-speakerdeckid', speakerdeckid);
- return div[0].outerHTML;
+ const speakerdeckid = match[1]
+ const div = $('')
+ div.attr('data-speakerdeckid', speakerdeckid)
+ return div[0].outerHTML
}
-);
-//pdf
+)
+// pdf
const pdfPlugin = new Plugin(
// regexp to match
/{%pdf\s*([\d\D]*?)\s*%}/,
(match, utils) => {
- const pdfurl = match[1];
- if (!isValidURL(pdfurl)) return match[0];
- const div = $('');
- div.attr('data-pdfurl', pdfurl);
- return div[0].outerHTML;
+ const pdfurl = match[1]
+ if (!isValidURL(pdfurl)) return match[0]
+ const div = $('')
+ div.attr('data-pdfurl', pdfurl)
+ return div[0].outerHTML
}
-);
+)
-//yaml meta, from https://github.com/eugeneware/remarkable-meta
-function get(state, line) {
- const pos = state.bMarks[line];
- const max = state.eMarks[line];
- return state.src.substr(pos, max - pos);
+// yaml meta, from https://github.com/eugeneware/remarkable-meta
+function get (state, line) {
+ const pos = state.bMarks[line]
+ const max = state.eMarks[line]
+ return state.src.substr(pos, max - pos)
}
-function meta(state, start, end, silent) {
- if (start !== 0 || state.blkIndent !== 0) return false;
- if (state.tShift[start] < 0) return false;
- if (!get(state, start).match(/^---$/)) return false;
+function meta (state, start, end, silent) {
+ if (start !== 0 || state.blkIndent !== 0) return false
+ if (state.tShift[start] < 0) return false
+ if (!get(state, start).match(/^---$/)) return false
- const data = [];
- for (var line = start + 1; line < end; line++) {
- const str = get(state, line);
- if (str.match(/^(\.{3}|-{3})$/)) break;
- if (state.tShift[line] < 0) break;
- data.push(str);
- }
+ const data = []
+ for (var line = start + 1; line < end; line++) {
+ const str = get(state, line)
+ if (str.match(/^(\.{3}|-{3})$/)) break
+ if (state.tShift[line] < 0) break
+ data.push(str)
+ }
- if (line >= end) return false;
+ if (line >= end) return false
- try {
- md.meta = jsyaml.safeLoad(data.join('\n')) || {};
- delete md.metaError;
- } catch(err) {
- md.metaError = err;
- console.warn(err);
- return false;
- }
+ try {
+ md.meta = window.jsyaml.safeLoad(data.join('\n')) || {}
+ delete md.metaError
+ } catch (err) {
+ md.metaError = err
+ console.warn(err)
+ return false
+ }
- state.line = line + 1;
+ state.line = line + 1
- return true;
+ return true
}
-function metaPlugin(md) {
- md.meta = md.meta || {};
- md.block.ruler.before('code', 'meta', meta, {
- alt: []
- });
+function metaPlugin (md) {
+ md.meta = md.meta || {}
+ md.block.ruler.before('code', 'meta', meta, {
+ alt: []
+ })
}
-md.use(metaPlugin);
-md.use(youtubePlugin);
-md.use(vimeoPlugin);
-md.use(gistPlugin);
-md.use(tocPlugin);
-md.use(slidesharePlugin);
-md.use(speakerdeckPlugin);
-md.use(pdfPlugin);
+md.use(metaPlugin)
+md.use(youtubePlugin)
+md.use(vimeoPlugin)
+md.use(gistPlugin)
+md.use(tocPlugin)
+md.use(slidesharePlugin)
+md.use(speakerdeckPlugin)
+md.use(pdfPlugin)
export default {
md
-};
+}
diff --git a/public/js/google-drive-picker.js b/public/js/google-drive-picker.js
index 94aa77ff..5006cd25 100644
--- a/public/js/google-drive-picker.js
+++ b/public/js/google-drive-picker.js
@@ -1,119 +1,118 @@
-/**!
+/** !
* Google Drive File Picker Example
* By Daniel Lo Nigro (http://dan.cx/)
*/
-(function() {
- /**
- * Initialise a Google Driver file picker
- */
- var FilePicker = window.FilePicker = function(options) {
- // Config
- this.apiKey = options.apiKey;
- this.clientId = options.clientId;
-
- // Elements
- this.buttonEl = options.buttonEl;
-
- // Events
- this.onSelect = options.onSelect;
- this.buttonEl.on('click', this.open.bind(this));
-
- // Disable the button until the API loads, as it won't work properly until then.
- this.buttonEl.prop('disabled', true);
+(function () {
+ /**
+ * Initialise a Google Driver file picker
+ */
+ var FilePicker = window.FilePicker = function (options) {
+ // Config
+ this.apiKey = options.apiKey
+ this.clientId = options.clientId
- // Load the drive API
- gapi.client.setApiKey(this.apiKey);
- gapi.client.load('drive', 'v2', this._driveApiLoaded.bind(this));
- google.load('picker', '1', { callback: this._pickerApiLoaded.bind(this) });
- }
+ // Elements
+ this.buttonEl = options.buttonEl
- FilePicker.prototype = {
- /**
- * Open the file picker.
- */
- open: function() {
- // Check if the user has already authenticated
- var token = gapi.auth.getToken();
- if (token) {
- this._showPicker();
- } else {
- // The user has not yet authenticated with Google
- // We need to do the authentication before displaying the Drive picker.
- this._doAuth(false, function() { this._showPicker(); }.bind(this));
- }
- },
-
- /**
- * Show the file picker once authentication has been done.
- * @private
- */
- _showPicker: function() {
- var accessToken = gapi.auth.getToken().access_token;
- var view = new google.picker.DocsView();
- view.setMimeTypes("text/markdown,text/html");
- view.setIncludeFolders(true);
- view.setOwnedByMe(true);
- this.picker = new google.picker.PickerBuilder().
- enableFeature(google.picker.Feature.NAV_HIDDEN).
- addView(view).
- setAppId(this.clientId).
- setOAuthToken(accessToken).
- setCallback(this._pickerCallback.bind(this)).
- build().
- setVisible(true);
- },
-
- /**
- * Called when a file has been selected in the Google Drive file picker.
- * @private
- */
- _pickerCallback: function(data) {
- if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
- var file = data[google.picker.Response.DOCUMENTS][0],
- id = file[google.picker.Document.ID],
- request = gapi.client.drive.files.get({
- fileId: id
- });
-
- request.execute(this._fileGetCallback.bind(this));
- }
- },
- /**
- * Called when file details have been retrieved from Google Drive.
- * @private
- */
- _fileGetCallback: function(file) {
- if (this.onSelect) {
- this.onSelect(file);
- }
- },
-
- /**
- * Called when the Google Drive file picker API has finished loading.
- * @private
- */
- _pickerApiLoaded: function() {
- this.buttonEl.prop('disabled', false);
- },
-
- /**
- * Called when the Google Drive API has finished loading.
- * @private
- */
- _driveApiLoaded: function() {
- this._doAuth(true);
- },
-
- /**
- * Authenticate with Google Drive via the Google JavaScript API.
- * @private
- */
- _doAuth: function(immediate, callback) {
- gapi.auth.authorize({
- client_id: this.clientId,
- scope: 'https://www.googleapis.com/auth/drive.readonly',
- immediate: immediate
- }, callback ? callback : function() {});
- }
- };
-}());
+ // Events
+ this.onSelect = options.onSelect
+ this.buttonEl.on('click', this.open.bind(this))
+
+ // Disable the button until the API loads, as it won't work properly until then.
+ this.buttonEl.prop('disabled', true)
+
+ // Load the drive API
+ window.gapi.client.setApiKey(this.apiKey)
+ window.gapi.client.load('drive', 'v2', this._driveApiLoaded.bind(this))
+ window.google.load('picker', '1', { callback: this._pickerApiLoaded.bind(this) })
+ }
+
+ FilePicker.prototype = {
+ /**
+ * Open the file picker.
+ */
+ open: function () {
+ // Check if the user has already authenticated
+ var token = window.gapi.auth.getToken()
+ if (token) {
+ this._showPicker()
+ } else {
+ // The user has not yet authenticated with Google
+ // We need to do the authentication before displaying the Drive picker.
+ this._doAuth(false, function () { this._showPicker() }.bind(this))
+ }
+ },
+
+ /**
+ * Show the file picker once authentication has been done.
+ * @private
+ */
+ _showPicker: function () {
+ var accessToken = window.gapi.auth.getToken().access_token
+ var view = new window.google.picker.DocsView()
+ view.setMimeTypes('text/markdown,text/html')
+ view.setIncludeFolders(true)
+ view.setOwnedByMe(true)
+ this.picker = new window.google.picker.PickerBuilder()
+ .enableFeature(window.google.picker.Feature.NAV_HIDDEN)
+ .addView(view)
+ .setAppId(this.clientId)
+ .setOAuthToken(accessToken)
+ .setCallback(this._pickerCallback.bind(this))
+ .build()
+ .setVisible(true)
+ },
+
+ /**
+ * Called when a file has been selected in the Google Drive file picker.
+ * @private
+ */
+ _pickerCallback: function (data) {
+ if (data[window.google.picker.Response.ACTION] === window.google.picker.Action.PICKED) {
+ var file = data[window.google.picker.Response.DOCUMENTS][0]
+ var id = file[window.google.picker.Document.ID]
+ var request = window.gapi.client.drive.files.get({
+ fileId: id
+ })
+ request.execute(this._fileGetCallback.bind(this))
+ }
+ },
+ /**
+ * Called when file details have been retrieved from Google Drive.
+ * @private
+ */
+ _fileGetCallback: function (file) {
+ if (this.onSelect) {
+ this.onSelect(file)
+ }
+ },
+
+ /**
+ * Called when the Google Drive file picker API has finished loading.
+ * @private
+ */
+ _pickerApiLoaded: function () {
+ this.buttonEl.prop('disabled', false)
+ },
+
+ /**
+ * Called when the Google Drive API has finished loading.
+ * @private
+ */
+ _driveApiLoaded: function () {
+ this._doAuth(true)
+ },
+
+ /**
+ * Authenticate with Google Drive via the Google JavaScript API.
+ * @private
+ */
+ _doAuth: function (immediate, callback) {
+ window.gapi.auth.authorize({
+ client_id: this.clientId,
+ scope: 'https://www.googleapis.com/auth/drive.readonly',
+ immediate: immediate
+ }, callback || function () {})
+ }
+ }
+}())
diff --git a/public/js/google-drive-upload.js b/public/js/google-drive-upload.js
index eabc5b7f..6c0e8a62 100644
--- a/public/js/google-drive-upload.js
+++ b/public/js/google-drive-upload.js
@@ -1,30 +1,31 @@
+/* eslint-env browser, jquery */
/**
* Helper for implementing retries with backoff. Initial retry
* delay is 1 second, increasing by 2x (+jitter) for subsequent retries
*
* @constructor
*/
-var RetryHandler = function() {
- this.interval = 1000; // Start at one second
- this.maxInterval = 60 * 1000; // Don't wait longer than a minute
-};
+var RetryHandler = function () {
+ this.interval = 1000 // Start at one second
+ this.maxInterval = 60 * 1000 // Don't wait longer than a minute
+}
/**
* Invoke the function after waiting
*
* @param {function} fn Function to invoke
*/
-RetryHandler.prototype.retry = function(fn) {
- setTimeout(fn, this.interval);
- this.interval = this.nextInterval_();
-};
+RetryHandler.prototype.retry = function (fn) {
+ setTimeout(fn, this.interval)
+ this.interval = this.nextInterval_()
+}
/**
* Reset the counter (e.g. after successful request.)
*/
-RetryHandler.prototype.reset = function() {
- this.interval = 1000;
-};
+RetryHandler.prototype.reset = function () {
+ this.interval = 1000
+}
/**
* Calculate the next wait time.
@@ -32,10 +33,10 @@ RetryHandler.prototype.reset = function() {
*
* @private
*/
-RetryHandler.prototype.nextInterval_ = function() {
- var interval = this.interval * 2 + this.getRandomInt_(0, 1000);
- return Math.min(interval, this.maxInterval);
-};
+RetryHandler.prototype.nextInterval_ = function () {
+ var interval = this.interval * 2 + this.getRandomInt_(0, 1000)
+ return Math.min(interval, this.maxInterval)
+}
/**
* Get a random int in the range of min to max. Used to add jitter to wait times.
@@ -44,10 +45,9 @@ RetryHandler.prototype.nextInterval_ = function() {
* @param {number} max Upper bounds
* @private
*/
-RetryHandler.prototype.getRandomInt_ = function(min, max) {
- return Math.floor(Math.random() * (max - min + 1) + min);
-};
-
+RetryHandler.prototype.getRandomInt_ = function (min, max) {
+ return Math.floor(Math.random() * (max - min + 1) + min)
+}
/**
* Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether
@@ -75,116 +75,115 @@ RetryHandler.prototype.getRandomInt_ = function(min, max) {
* @param {function} [options.onProgress] Callback for status for the in-progress upload
* @param {function} [options.onError] Callback if upload fails
*/
-var MediaUploader = function(options) {
- var noop = function() {};
- this.file = options.file;
- this.contentType = options.contentType || this.file.type || 'application/octet-stream';
+var MediaUploader = function (options) {
+ var noop = function () {}
+ this.file = options.file
+ this.contentType = options.contentType || this.file.type || 'application/octet-stream'
this.metadata = options.metadata || {
'title': this.file.name,
'mimeType': this.contentType
- };
- this.token = options.token;
- this.onComplete = options.onComplete || noop;
- this.onProgress = options.onProgress || noop;
- this.onError = options.onError || noop;
- this.offset = options.offset || 0;
- this.chunkSize = options.chunkSize || 0;
- this.retryHandler = new RetryHandler();
-
- this.url = options.url;
- if (!this.url) {
- var params = options.params || {};
- params.uploadType = 'resumable';
- this.url = this.buildUrl_(options.fileId, params, options.baseUrl);
}
- this.httpMethod = options.fileId ? 'PUT' : 'POST';
-};
+ this.token = options.token
+ this.onComplete = options.onComplete || noop
+ this.onProgress = options.onProgress || noop
+ this.onError = options.onError || noop
+ this.offset = options.offset || 0
+ this.chunkSize = options.chunkSize || 0
+ this.retryHandler = new RetryHandler()
+
+ this.url = options.url
+ if (!this.url) {
+ var params = options.params || {}
+ params.uploadType = 'resumable'
+ this.url = this.buildUrl_(options.fileId, params, options.baseUrl)
+ }
+ this.httpMethod = options.fileId ? 'PUT' : 'POST'
+}
/**
* Initiate the upload.
*/
-MediaUploader.prototype.upload = function() {
- var self = this;
- var xhr = new XMLHttpRequest();
+MediaUploader.prototype.upload = function () {
+ var xhr = new XMLHttpRequest()
- xhr.open(this.httpMethod, this.url, true);
- xhr.setRequestHeader('Authorization', 'Bearer ' + this.token);
- xhr.setRequestHeader('Content-Type', 'application/json');
- xhr.setRequestHeader('X-Upload-Content-Length', this.file.size);
- xhr.setRequestHeader('X-Upload-Content-Type', this.contentType);
+ xhr.open(this.httpMethod, this.url, true)
+ xhr.setRequestHeader('Authorization', 'Bearer ' + this.token)
+ xhr.setRequestHeader('Content-Type', 'application/json')
+ xhr.setRequestHeader('X-Upload-Content-Length', this.file.size)
+ xhr.setRequestHeader('X-Upload-Content-Type', this.contentType)
- xhr.onload = function(e) {
+ xhr.onload = function (e) {
if (e.target.status < 400) {
- var location = e.target.getResponseHeader('Location');
- this.url = location;
- this.sendFile_();
+ var location = e.target.getResponseHeader('Location')
+ this.url = location
+ this.sendFile_()
} else {
- this.onUploadError_(e);
+ this.onUploadError_(e)
}
- }.bind(this);
- xhr.onerror = this.onUploadError_.bind(this);
- xhr.send(JSON.stringify(this.metadata));
-};
+ }.bind(this)
+ xhr.onerror = this.onUploadError_.bind(this)
+ xhr.send(JSON.stringify(this.metadata))
+}
/**
* Send the actual file content.
*
* @private
*/
-MediaUploader.prototype.sendFile_ = function() {
- var content = this.file;
- var end = this.file.size;
+MediaUploader.prototype.sendFile_ = function () {
+ var content = this.file
+ var end = this.file.size
if (this.offset || this.chunkSize) {
// Only bother to slice the file if we're either resuming or uploading in chunks
if (this.chunkSize) {
- end = Math.min(this.offset + this.chunkSize, this.file.size);
+ end = Math.min(this.offset + this.chunkSize, this.file.size)
}
- content = content.slice(this.offset, end);
+ content = content.slice(this.offset, end)
}
- var xhr = new XMLHttpRequest();
- xhr.open('PUT', this.url, true);
- xhr.setRequestHeader('Content-Type', this.contentType);
- xhr.setRequestHeader('Content-Range', "bytes " + this.offset + "-" + (end - 1) + "/" + this.file.size);
- xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
+ var xhr = new XMLHttpRequest()
+ xhr.open('PUT', this.url, true)
+ xhr.setRequestHeader('Content-Type', this.contentType)
+ xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size)
+ xhr.setRequestHeader('X-Upload-Content-Type', this.file.type)
if (xhr.upload) {
- xhr.upload.addEventListener('progress', this.onProgress);
+ xhr.upload.addEventListener('progress', this.onProgress)
}
- xhr.onload = this.onContentUploadSuccess_.bind(this);
- xhr.onerror = this.onContentUploadError_.bind(this);
- xhr.send(content);
-};
+ xhr.onload = this.onContentUploadSuccess_.bind(this)
+ xhr.onerror = this.onContentUploadError_.bind(this)
+ xhr.send(content)
+}
/**
* Query for the state of the file for resumption.
*
* @private
*/
-MediaUploader.prototype.resume_ = function() {
- var xhr = new XMLHttpRequest();
- xhr.open('PUT', this.url, true);
- xhr.setRequestHeader('Content-Range', "bytes */" + this.file.size);
- xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
+MediaUploader.prototype.resume_ = function () {
+ var xhr = new XMLHttpRequest()
+ xhr.open('PUT', this.url, true)
+ xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size)
+ xhr.setRequestHeader('X-Upload-Content-Type', this.file.type)
if (xhr.upload) {
- xhr.upload.addEventListener('progress', this.onProgress);
+ xhr.upload.addEventListener('progress', this.onProgress)
}
- xhr.onload = this.onContentUploadSuccess_.bind(this);
- xhr.onerror = this.onContentUploadError_.bind(this);
- xhr.send();
-};
+ xhr.onload = this.onContentUploadSuccess_.bind(this)
+ xhr.onerror = this.onContentUploadError_.bind(this)
+ xhr.send()
+}
/**
* Extract the last saved range if available in the request.
*
* @param {XMLHttpRequest} xhr Request object
*/
-MediaUploader.prototype.extractRange_ = function(xhr) {
- var range = xhr.getResponseHeader('Range');
+MediaUploader.prototype.extractRange_ = function (xhr) {
+ var range = xhr.getResponseHeader('Range')
if (range) {
- this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1;
+ this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1
}
-};
+}
/**
* Handle successful responses for uploads. Depending on the context,
@@ -194,17 +193,17 @@ MediaUploader.prototype.extractRange_ = function(xhr) {
* @private
* @param {object} e XHR event
*/
-MediaUploader.prototype.onContentUploadSuccess_ = function(e) {
- if (e.target.status == 200 || e.target.status == 201) {
- this.onComplete(e.target.response);
- } else if (e.target.status == 308) {
- this.extractRange_(e.target);
- this.retryHandler.reset();
- this.sendFile_();
+MediaUploader.prototype.onContentUploadSuccess_ = function (e) {
+ if (e.target.status === 200 || e.target.status === 201) {
+ this.onComplete(e.target.response)
+ } else if (e.target.status === 308) {
+ this.extractRange_(e.target)
+ this.retryHandler.reset()
+ this.sendFile_()
} else {
- this.onContentUploadError_(e);
+ this.onContentUploadError_(e)
}
-};
+}
/**
* Handles errors for uploads. Either retries or aborts depending
@@ -213,13 +212,13 @@ MediaUploader.prototype.onContentUploadSuccess_ = function(e) {
* @private
* @param {object} e XHR event
*/
-MediaUploader.prototype.onContentUploadError_ = function(e) {
+MediaUploader.prototype.onContentUploadError_ = function (e) {
if (e.target.status && e.target.status < 500) {
- this.onError(e.target.response);
+ this.onError(e.target.response)
} else {
- this.retryHandler.retry(this.resume_.bind(this));
+ this.retryHandler.retry(this.resume_.bind(this))
}
-};
+}
/**
* Handles errors for the initial request.
@@ -227,9 +226,9 @@ MediaUploader.prototype.onContentUploadError_ = function(e) {
* @private
* @param {object} e XHR event
*/
-MediaUploader.prototype.onUploadError_ = function(e) {
- this.onError(e.target.response); // TODO - Retries for initial upload
-};
+MediaUploader.prototype.onUploadError_ = function (e) {
+ this.onError(e.target.response) // TODO - Retries for initial upload
+}
/**
* Construct a query string from a hash/object
@@ -238,12 +237,12 @@ MediaUploader.prototype.onUploadError_ = function(e) {
* @param {object} [params] Key/value pairs for query string
* @return {string} query string
*/
-MediaUploader.prototype.buildQuery_ = function(params) {
- params = params || {};
- return Object.keys(params).map(function(key) {
- return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
- }).join('&');
-};
+MediaUploader.prototype.buildQuery_ = function (params) {
+ params = params || {}
+ return Object.keys(params).map(function (key) {
+ return encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
+ }).join('&')
+}
/**
* Build the drive upload URL
@@ -253,16 +252,16 @@ MediaUploader.prototype.buildQuery_ = function(params) {
* @param {object} [params] Query parameters
* @return {string} URL
*/
-MediaUploader.prototype.buildUrl_ = function(id, params, baseUrl) {
- var url = baseUrl || 'https://www.googleapis.com/upload/drive/v2/files/';
+MediaUploader.prototype.buildUrl_ = function (id, params, baseUrl) {
+ var url = baseUrl || 'https://www.googleapis.com/upload/drive/v2/files/'
if (id) {
- url += id;
+ url += id
}
- var query = this.buildQuery_(params);
+ var query = this.buildQuery_(params)
if (query) {
- url += '?' + query;
+ url += '?' + query
}
- return url;
-};
+ return url
+}
-window.MediaUploader = MediaUploader;
+window.MediaUploader = MediaUploader
diff --git a/public/js/history.js b/public/js/history.js
index 34b2cba7..e14b80d8 100644
--- a/public/js/history.js
+++ b/public/js/history.js
@@ -1,372 +1,328 @@
-import store from 'store';
-import S from 'string';
+/* eslint-env browser, jquery */
+/* global serverurl, Cookies, moment */
+
+import store from 'store'
+import S from 'string'
import {
checkIfAuth
-} from './lib/common/login';
+} from './lib/common/login'
import {
urlpath
-} from './lib/config';
+} from './lib/config'
-window.migrateHistoryFromTempCallback = null;
+window.migrateHistoryFromTempCallback = null
-migrateHistoryFromTemp();
+migrateHistoryFromTemp()
-function migrateHistoryFromTemp() {
- if (url('#tempid')) {
- $.get(`${serverurl}/temp`, {
- tempid: url('#tempid')
- })
- .done(data => {
- if (data && data.temp) {
- getStorageHistory(olddata => {
- if (!olddata || olddata.length == 0) {
- saveHistoryToStorage(JSON.parse(data.temp));
- }
- });
- }
- })
- .always(() => {
- let hash = location.hash.split('#')[1];
- hash = hash.split('&');
- for (let i = 0; i < hash.length; i++)
- if (hash[i].indexOf('tempid') == 0) {
- hash.splice(i, 1);
- i--;
- }
- hash = hash.join('&');
- location.hash = hash;
- if (migrateHistoryFromTempCallback)
- migrateHistoryFromTempCallback();
- });
- }
+function migrateHistoryFromTemp () {
+ if (window.url('#tempid')) {
+ $.get(`${serverurl}/temp`, {
+ tempid: window.url('#tempid')
+ })
+ .done(data => {
+ if (data && data.temp) {
+ getStorageHistory(olddata => {
+ if (!olddata || olddata.length === 0) {
+ saveHistoryToStorage(JSON.parse(data.temp))
+ }
+ })
+ }
+ })
+ .always(() => {
+ let hash = location.hash.split('#')[1]
+ hash = hash.split('&')
+ for (let i = 0; i < hash.length; i++) {
+ if (hash[i].indexOf('tempid') === 0) {
+ hash.splice(i, 1)
+ i--
+ }
+ }
+ hash = hash.join('&')
+ location.hash = hash
+ if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
+ })
+ }
}
-export function saveHistory(notehistory) {
- checkIfAuth(
+export function saveHistory (notehistory) {
+ checkIfAuth(
() => {
- saveHistoryToServer(notehistory);
+ saveHistoryToServer(notehistory)
},
() => {
- saveHistoryToStorage(notehistory);
+ saveHistoryToStorage(notehistory)
}
- );
+ )
}
-function saveHistoryToStorage(notehistory) {
- if (store.enabled)
- store.set('notehistory', JSON.stringify(notehistory));
- else
- saveHistoryToCookie(notehistory);
+function saveHistoryToStorage (notehistory) {
+ if (store.enabled) { store.set('notehistory', JSON.stringify(notehistory)) } else { saveHistoryToCookie(notehistory) }
}
-function saveHistoryToCookie(notehistory) {
- Cookies.set('notehistory', notehistory, {
- expires: 365
- });
+function saveHistoryToCookie (notehistory) {
+ Cookies.set('notehistory', notehistory, {
+ expires: 365
+ })
}
-function saveHistoryToServer(notehistory) {
+function saveHistoryToServer (notehistory) {
+ $.post(`${serverurl}/history`, {
+ history: JSON.stringify(notehistory)
+ })
+}
+
+export function saveStorageHistoryToServer (callback) {
+ const data = store.get('notehistory')
+ if (data) {
$.post(`${serverurl}/history`, {
- history: JSON.stringify(notehistory)
- });
-}
-
-function saveCookieHistoryToStorage(callback) {
- store.set('notehistory', Cookies.get('notehistory'));
- callback();
-}
-
-export function saveStorageHistoryToServer(callback) {
- const data = store.get('notehistory');
- if (data) {
- $.post(`${serverurl}/history`, {
- history: data
- })
+ history: data
+ })
.done(data => {
- callback(data);
- });
- }
+ callback(data)
+ })
+ }
}
-function saveCookieHistoryToServer(callback) {
- $.post(`${serverurl}/history`, {
- history: Cookies.get('notehistory')
- })
- .done(data => {
- callback(data);
- });
-}
-
-export function clearDuplicatedHistory(notehistory) {
- const newnotehistory = [];
- for (let i = 0; i < notehistory.length; i++) {
- let found = false;
- for (let j = 0; j < newnotehistory.length; j++) {
- const id = notehistory[i].id.replace(/\=+$/, '');
- const newId = newnotehistory[j].id.replace(/\=+$/, '');
- if (id == newId || notehistory[i].id == newnotehistory[j].id || !notehistory[i].id || !newnotehistory[j].id) {
- const time = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
- const newTime = (typeof newnotehistory[i].time === 'number' ? moment(newnotehistory[i].time) : moment(newnotehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
- if(time >= newTime) {
- newnotehistory[j] = notehistory[i];
- }
- found = true;
- break;
- }
+export function clearDuplicatedHistory (notehistory) {
+ const newnotehistory = []
+ for (let i = 0; i < notehistory.length; i++) {
+ let found = false
+ for (let j = 0; j < newnotehistory.length; j++) {
+ const id = notehistory[i].id.replace(/=+$/, '')
+ const newId = newnotehistory[j].id.replace(/=+$/, '')
+ if (id === newId || notehistory[i].id === newnotehistory[j].id || !notehistory[i].id || !newnotehistory[j].id) {
+ const time = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'))
+ const newTime = (typeof newnotehistory[i].time === 'number' ? moment(newnotehistory[i].time) : moment(newnotehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'))
+ if (time >= newTime) {
+ newnotehistory[j] = notehistory[i]
}
- if (!found)
- newnotehistory.push(notehistory[i]);
+ found = true
+ break
+ }
}
- return newnotehistory;
+ if (!found) { newnotehistory.push(notehistory[i]) }
+ }
+ return newnotehistory
}
-function addHistory(id, text, time, tags, pinned, notehistory) {
+function addHistory (id, text, time, tags, pinned, notehistory) {
// only add when note id exists
- if (id) {
- notehistory.push({
- id,
- text,
- time,
- tags,
- pinned
- });
- }
- return notehistory;
+ if (id) {
+ notehistory.push({
+ id,
+ text,
+ time,
+ tags,
+ pinned
+ })
+ }
+ return notehistory
}
-export function removeHistory(id, notehistory) {
- for (let i = 0; i < notehistory.length; i++) {
- if (notehistory[i].id == id) {
- notehistory.splice(i, 1);
- i -= 1;
- }
+export function removeHistory (id, notehistory) {
+ for (let i = 0; i < notehistory.length; i++) {
+ if (notehistory[i].id === id) {
+ notehistory.splice(i, 1)
+ i -= 1
}
- return notehistory;
+ }
+ return notehistory
}
-//used for inner
-export function writeHistory(title, tags) {
- checkIfAuth(
+// used for inner
+export function writeHistory (title, tags) {
+ checkIfAuth(
() => {
// no need to do this anymore, this will count from server-side
// writeHistoryToServer(title, tags);
},
() => {
- writeHistoryToStorage(title, tags);
+ writeHistoryToStorage(title, tags)
}
- );
+ )
}
-function writeHistoryToServer(title, tags) {
- $.get(`${serverurl}/history`)
- .done(data => {
- try {
- if (data.history) {
- var notehistory = data.history;
- } else {
- var notehistory = [];
- }
- } catch (err) {
- var notehistory = [];
- }
- if (!notehistory)
- notehistory = [];
-
- const newnotehistory = generateHistory(title, tags, notehistory);
- saveHistoryToServer(newnotehistory);
- })
- .fail((xhr, status, error) => {
- console.error(xhr.responseText);
- });
+function writeHistoryToCookie (title, tags) {
+ var notehistory
+ try {
+ notehistory = Cookies.getJSON('notehistory')
+ } catch (err) {
+ notehistory = []
+ }
+ if (!notehistory) { notehistory = [] }
+ const newnotehistory = generateHistory(title, tags, notehistory)
+ saveHistoryToCookie(newnotehistory)
}
-function writeHistoryToCookie(title, tags) {
- try {
- var notehistory = Cookies.getJSON('notehistory');
- } catch (err) {
- var notehistory = [];
- }
- if (!notehistory)
- notehistory = [];
-
- const newnotehistory = generateHistory(title, tags, notehistory);
- saveHistoryToCookie(newnotehistory);
-}
-
-function writeHistoryToStorage(title, tags) {
- if (store.enabled) {
- let data = store.get('notehistory');
- if (data) {
- if (typeof data == "string")
- data = JSON.parse(data);
- var notehistory = data;
- } else
- var notehistory = [];
- if (!notehistory)
- notehistory = [];
-
- const newnotehistory = generateHistory(title, tags, notehistory);
- saveHistoryToStorage(newnotehistory);
+function writeHistoryToStorage (title, tags) {
+ if (store.enabled) {
+ let data = store.get('notehistory')
+ var notehistory
+ if (data) {
+ if (typeof data === 'string') { data = JSON.parse(data) }
+ notehistory = data
} else {
- writeHistoryToCookie(title, tags);
+ notehistory = []
}
+ if (!notehistory) { notehistory = [] }
+
+ const newnotehistory = generateHistory(title, tags, notehistory)
+ saveHistoryToStorage(newnotehistory)
+ } else {
+ writeHistoryToCookie(title, tags)
+ }
}
if (!Array.isArray) {
- Array.isArray = arg => Object.prototype.toString.call(arg) === '[object Array]';
+ Array.isArray = arg => Object.prototype.toString.call(arg) === '[object Array]'
}
-function renderHistory(title, tags) {
- //console.debug(tags);
- const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1];
- return {
- id,
- text: title,
- time: moment().valueOf(),
- tags
- };
+function renderHistory (title, tags) {
+ // console.debug(tags);
+ const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1]
+ return {
+ id,
+ text: title,
+ time: moment().valueOf(),
+ tags
+ }
}
-function generateHistory(title, tags, notehistory) {
- const info = renderHistory(title, tags);
- //keep any pinned data
- let pinned = false;
+function generateHistory (title, tags, notehistory) {
+ const info = renderHistory(title, tags)
+ // keep any pinned data
+ let pinned = false
+ for (let i = 0; i < notehistory.length; i++) {
+ if (notehistory[i].id === info.id && notehistory[i].pinned) {
+ pinned = true
+ break
+ }
+ }
+ notehistory = removeHistory(info.id, notehistory)
+ notehistory = addHistory(info.id, info.text, info.time, info.tags, pinned, notehistory)
+ notehistory = clearDuplicatedHistory(notehistory)
+ return notehistory
+}
+
+// used for outer
+export function getHistory (callback) {
+ checkIfAuth(
+ () => {
+ getServerHistory(callback)
+ },
+ () => {
+ getStorageHistory(callback)
+ }
+ )
+}
+
+function getServerHistory (callback) {
+ $.get(`${serverurl}/history`)
+ .done(data => {
+ if (data.history) {
+ callback(data.history)
+ }
+ })
+ .fail((xhr, status, error) => {
+ console.error(xhr.responseText)
+ })
+}
+
+function getCookieHistory (callback) {
+ callback(Cookies.getJSON('notehistory'))
+}
+
+export function getStorageHistory (callback) {
+ if (store.enabled) {
+ let data = store.get('notehistory')
+ if (data) {
+ if (typeof data === 'string') { data = JSON.parse(data) }
+ callback(data)
+ } else { getCookieHistory(callback) }
+ } else {
+ getCookieHistory(callback)
+ }
+}
+
+export function parseHistory (list, callback) {
+ checkIfAuth(
+ () => {
+ parseServerToHistory(list, callback)
+ },
+ () => {
+ parseStorageToHistory(list, callback)
+ }
+ )
+}
+
+export function parseServerToHistory (list, callback) {
+ $.get(`${serverurl}/history`)
+ .done(data => {
+ if (data.history) {
+ parseToHistory(list, data.history, callback)
+ }
+ })
+ .fail((xhr, status, error) => {
+ console.error(xhr.responseText)
+ })
+}
+
+function parseCookieToHistory (list, callback) {
+ const notehistory = Cookies.getJSON('notehistory')
+ parseToHistory(list, notehistory, callback)
+}
+
+export function parseStorageToHistory (list, callback) {
+ if (store.enabled) {
+ let data = store.get('notehistory')
+ if (data) {
+ if (typeof data === 'string') { data = JSON.parse(data) }
+ parseToHistory(list, data, callback)
+ } else { parseCookieToHistory(list, callback) }
+ } else {
+ parseCookieToHistory(list, callback)
+ }
+}
+
+function parseToHistory (list, notehistory, callback) {
+ if (!callback) return
+ else if (!list || !notehistory) callback(list, notehistory)
+ else if (notehistory && notehistory.length > 0) {
for (let i = 0; i < notehistory.length; i++) {
- if (notehistory[i].id == info.id && notehistory[i].pinned) {
- pinned = true;
- break;
- }
- }
- notehistory = removeHistory(info.id, notehistory);
- notehistory = addHistory(info.id, info.text, info.time, info.tags, pinned, notehistory);
- notehistory = clearDuplicatedHistory(notehistory);
- return notehistory;
-}
-
-//used for outer
-export function getHistory(callback) {
- checkIfAuth(
- () => {
- getServerHistory(callback);
- },
- () => {
- getStorageHistory(callback);
- }
- );
-}
-
-function getServerHistory(callback) {
- $.get(`${serverurl}/history`)
- .done(data => {
- if (data.history) {
- callback(data.history);
- }
- })
- .fail((xhr, status, error) => {
- console.error(xhr.responseText);
- });
-}
-
-function getCookieHistory(callback) {
- callback(Cookies.getJSON('notehistory'));
-}
-
-export function getStorageHistory(callback) {
- if (store.enabled) {
- let data = store.get('notehistory');
- if (data) {
- if (typeof data == "string")
- data = JSON.parse(data);
- callback(data);
- } else
- getCookieHistory(callback);
- } else {
- getCookieHistory(callback);
- }
-}
-
-export function parseHistory(list, callback) {
- checkIfAuth(
- () => {
- parseServerToHistory(list, callback);
- },
- () => {
- parseStorageToHistory(list, callback);
- }
- );
-}
-
-export function parseServerToHistory(list, callback) {
- $.get(`${serverurl}/history`)
- .done(data => {
- if (data.history) {
- parseToHistory(list, data.history, callback);
- }
- })
- .fail((xhr, status, error) => {
- console.error(xhr.responseText);
- });
-}
-
-function parseCookieToHistory(list, callback) {
- const notehistory = Cookies.getJSON('notehistory');
- parseToHistory(list, notehistory, callback);
-}
-
-export function parseStorageToHistory(list, callback) {
- if (store.enabled) {
- let data = store.get('notehistory');
- if (data) {
- if (typeof data == "string")
- data = JSON.parse(data);
- parseToHistory(list, data, callback);
- } else
- parseCookieToHistory(list, callback);
- } else {
- parseCookieToHistory(list, callback);
- }
-}
-
-function parseToHistory(list, notehistory, callback) {
- if (!callback) return;
- else if (!list || !notehistory) callback(list, notehistory);
- else if (notehistory && notehistory.length > 0) {
- for (let i = 0; i < notehistory.length; i++) {
- //parse time to timestamp and fromNow
- const timestamp = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
- notehistory[i].timestamp = timestamp.valueOf();
- notehistory[i].fromNow = timestamp.fromNow();
- notehistory[i].time = timestamp.format('llll');
+ // parse time to timestamp and fromNow
+ const timestamp = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'))
+ notehistory[i].timestamp = timestamp.valueOf()
+ notehistory[i].fromNow = timestamp.fromNow()
+ notehistory[i].time = timestamp.format('llll')
// prevent XSS
- notehistory[i].text = S(notehistory[i].text).escapeHTML().s;
- notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : [];
+ notehistory[i].text = S(notehistory[i].text).escapeHTML().s
+ notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : []
// add to list
- if (notehistory[i].id && list.get('id', notehistory[i].id).length == 0)
- list.add(notehistory[i]);
- }
+ if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) }
}
- callback(list, notehistory);
+ }
+ callback(list, notehistory)
}
-export function postHistoryToServer(noteId, data, callback) {
- $.post(`${serverurl}/history/${noteId}`, data)
+export function postHistoryToServer (noteId, data, callback) {
+ $.post(`${serverurl}/history/${noteId}`, data)
.done(result => callback(null, result))
.fail((xhr, status, error) => {
- console.error(xhr.responseText);
- return callback(error, null);
- });
-}
-
-export function deleteServerHistory(noteId, callback) {
- $.ajax({
- url: `${serverurl}/history${noteId ? '/' + noteId : ""}`,
- type: 'DELETE'
+ console.error(xhr.responseText)
+ return callback(error, null)
})
+}
+
+export function deleteServerHistory (noteId, callback) {
+ $.ajax({
+ url: `${serverurl}/history${noteId ? '/' + noteId : ''}`,
+ type: 'DELETE'
+ })
.done(result => callback(null, result))
.fail((xhr, status, error) => {
- console.error(xhr.responseText);
- return callback(error, null);
- });
+ console.error(xhr.responseText)
+ return callback(error, null)
+ })
}
diff --git a/public/js/htmlExport.js b/public/js/htmlExport.js
index 1c2c5eb9..1a873aca 100644
--- a/public/js/htmlExport.js
+++ b/public/js/htmlExport.js
@@ -1,6 +1,6 @@
-require('../css/github-extract.css');
-require('../css/markdown.css');
-require('../css/extra.css');
-require('../css/slide-preview.css');
-require('../css/google-font.css');
-require('../css/site.css');
+require('../css/github-extract.css')
+require('../css/markdown.css')
+require('../css/extra.css')
+require('../css/slide-preview.css')
+require('../css/google-font.css')
+require('../css/site.css')
diff --git a/public/js/index.js b/public/js/index.js
index f0c476ef..e672a68d 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1,26 +1,30 @@
-/* jquery and jquery plugins */
-require('../vendor/showup/showup');
+/* eslint-env browser, jquery */
+/* global CodeMirror, Cookies, moment, editor, ui, Spinner,
+ modeType, Idle, serverurl, key, gapi, Dropbox, FilePicker
+ ot, MediaUploader, hex2rgb, num_loaded, Visibility */
-require('../css/index.css');
-require('../css/extra.css');
-require('../css/slide-preview.css');
-require('../css/site.css');
+require('../vendor/showup/showup')
-require('highlight.js/styles/github-gist.css');
+require('../css/index.css')
+require('../css/extra.css')
+require('../css/slide-preview.css')
+require('../css/site.css')
-var toMarkdown = require('to-markdown');
+require('highlight.js/styles/github-gist.css')
-var saveAs = require('file-saver').saveAs;
-var randomColor = require('randomcolor');
+var toMarkdown = require('to-markdown')
-var _ = require("lodash");
+var saveAs = require('file-saver').saveAs
+var randomColor = require('randomcolor')
-var List = require('list.js');
+var _ = require('lodash')
+
+var List = require('list.js')
import {
checkLoginStateChanged,
setloginStateChangeEvent
-} from './lib/common/login';
+} from './lib/common/login'
import {
debug,
@@ -31,7 +35,7 @@ import {
noteurl,
urlpath,
version
-} from './lib/config';
+} from './lib/config'
import {
autoLinkify,
@@ -53,14 +57,14 @@ import {
updateLastChange,
updateLastChangeUser,
updateOwner
-} from './extra';
+} from './extra'
import {
clearMap,
setupSyncAreas,
syncScrollToEdit,
syncScrollToView
-} from './syncscroll';
+} from './syncscroll'
import {
writeHistory,
@@ -68,4007 +72,3856 @@ import {
getHistory,
saveHistory,
removeHistory
-} from './history';
+} from './history'
-var renderer = require('./render');
-var preventXSS = renderer.preventXSS;
+var renderer = require('./render')
+var preventXSS = renderer.preventXSS
-var defaultTextHeight = 20;
-var viewportMargin = 20;
-var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
-var defaultEditorMode = 'gfm';
+var defaultTextHeight = 20
+var viewportMargin = 20
+var mac = CodeMirror.keyMap['default'] === CodeMirror.keyMap.macDefault
+var defaultEditorMode = 'gfm'
var defaultExtraKeys = {
- "F10": function (cm) {
- cm.setOption("fullScreen", !cm.getOption("fullScreen"));
- },
- "Esc": function (cm) {
- if (cm.getOption('keyMap').substr(0, 3) === 'vim') return CodeMirror.Pass;
- else if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
- },
- "Cmd-S": function () {
- return false;
- },
- "Ctrl-S": function () {
- return false;
- },
- "Enter": "newlineAndIndentContinueMarkdownList",
- "Tab": function (cm) {
- var tab = '\t';
- var spaces = Array(parseInt(cm.getOption("indentUnit")) + 1).join(" ");
- //auto indent whole line when in list or blockquote
- var cursor = cm.getCursor();
- var line = cm.getLine(cursor.line);
- var regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/;
- var match;
- var multiple = cm.getSelection().split('\n').length > 1 || cm.getSelections().length > 1;
- if (multiple) {
- cm.execCommand('defaultTab');
- } else if ((match = regex.exec(line)) !== null) {
- var ch = match[1].length;
- var pos = {
- line: cursor.line,
- ch: ch
- };
- if (cm.getOption('indentWithTabs'))
- cm.replaceRange(tab, pos, pos, '+input');
- else
- cm.replaceRange(spaces, pos, pos, '+input');
- } else {
- if (cm.getOption('indentWithTabs'))
- cm.execCommand('defaultTab');
- else {
- cm.replaceSelection(spaces);
- }
- }
- },
- "Cmd-Left": "goLineLeftSmart",
- "Cmd-Right": "goLineRight",
- "Ctrl-C": function (cm) {
- if (!mac && cm.getOption('keyMap').substr(0, 3) === 'vim') document.execCommand("copy");
- else return CodeMirror.Pass;
- },
- "Ctrl-*": function (cm) {
- wrapTextWith(cm, '*');
- },
- "Shift-Ctrl-8": function (cm) {
- wrapTextWith(cm, '*');
- },
- "Ctrl-_": function (cm) {
- wrapTextWith(cm, '_');
- },
- "Shift-Ctrl--": function (cm) {
- wrapTextWith(cm, '_');
- },
- "Ctrl-~": function (cm) {
- wrapTextWith(cm, '~');
- },
- "Shift-Ctrl-`": function (cm) {
- wrapTextWith(cm, '~');
- },
- "Ctrl-^": function (cm) {
- wrapTextWith(cm, '^');
- },
- "Shift-Ctrl-6": function (cm) {
- wrapTextWith(cm, '^');
- },
- "Ctrl-+": function (cm) {
- wrapTextWith(cm, '+');
- },
- "Shift-Ctrl-=": function (cm) {
- wrapTextWith(cm, '+');
- },
- "Ctrl-=": function (cm) {
- wrapTextWith(cm, '=');
- },
- "Shift-Ctrl-Backspace": function (cm) {
- wrapTextWith(cm, 'Backspace');
- }
-};
-
-var wrapSymbols = ['*', '_', '~', '^', '+', '='];
-
-function wrapTextWith(cm, symbol) {
- if (!cm.getSelection()) {
- return CodeMirror.Pass;
+ 'F10': function (cm) {
+ cm.setOption('fullScreen', !cm.getOption('fullScreen'))
+ },
+ 'Esc': function (cm) {
+ if (cm.getOption('keyMap').substr(0, 3) === 'vim') return CodeMirror.Pass
+ else if (cm.getOption('fullScreen')) cm.setOption('fullScreen', false)
+ },
+ 'Cmd-S': function () {
+ return false
+ },
+ 'Ctrl-S': function () {
+ return false
+ },
+ 'Enter': 'newlineAndIndentContinueMarkdownList',
+ 'Tab': function (cm) {
+ var tab = '\t'
+ var spaces = Array(parseInt(cm.getOption('indentUnit')) + 1).join(' ')
+ // auto indent whole line when in list or blockquote
+ var cursor = cm.getCursor()
+ var line = cm.getLine(cursor.line)
+ var regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/
+ var match
+ var multiple = cm.getSelection().split('\n').length > 1 || cm.getSelections().length > 1
+ if (multiple) {
+ cm.execCommand('defaultTab')
+ } else if ((match = regex.exec(line)) !== null) {
+ var ch = match[1].length
+ var pos = {
+ line: cursor.line,
+ ch: ch
+ }
+ if (cm.getOption('indentWithTabs')) { cm.replaceRange(tab, pos, pos, '+input') } else { cm.replaceRange(spaces, pos, pos, '+input') }
} else {
- var ranges = cm.listSelections();
- for (var i = 0; i < ranges.length; i++) {
- var range = ranges[i];
- if (!range.empty()) {
- var from = range.from(), to = range.to();
- if (symbol !== 'Backspace') {
- cm.replaceRange(symbol, to, to, '+input');
- cm.replaceRange(symbol, from, from, '+input');
+ if (cm.getOption('indentWithTabs')) { cm.execCommand('defaultTab') } else {
+ cm.replaceSelection(spaces)
+ }
+ }
+ },
+ 'Cmd-Left': 'goLineLeftSmart',
+ 'Cmd-Right': 'goLineRight',
+ 'Ctrl-C': function (cm) {
+ if (!mac && cm.getOption('keyMap').substr(0, 3) === 'vim') document.execCommand('copy')
+ else return CodeMirror.Pass
+ },
+ 'Ctrl-*': function (cm) {
+ wrapTextWith(cm, '*')
+ },
+ 'Shift-Ctrl-8': function (cm) {
+ wrapTextWith(cm, '*')
+ },
+ 'Ctrl-_': function (cm) {
+ wrapTextWith(cm, '_')
+ },
+ 'Shift-Ctrl--': function (cm) {
+ wrapTextWith(cm, '_')
+ },
+ 'Ctrl-~': function (cm) {
+ wrapTextWith(cm, '~')
+ },
+ 'Shift-Ctrl-`': function (cm) {
+ wrapTextWith(cm, '~')
+ },
+ 'Ctrl-^': function (cm) {
+ wrapTextWith(cm, '^')
+ },
+ 'Shift-Ctrl-6': function (cm) {
+ wrapTextWith(cm, '^')
+ },
+ 'Ctrl-+': function (cm) {
+ wrapTextWith(cm, '+')
+ },
+ 'Shift-Ctrl-=': function (cm) {
+ wrapTextWith(cm, '+')
+ },
+ 'Ctrl-=': function (cm) {
+ wrapTextWith(cm, '=')
+ },
+ 'Shift-Ctrl-Backspace': function (cm) {
+ wrapTextWith(cm, 'Backspace')
+ }
+}
+
+var wrapSymbols = ['*', '_', '~', '^', '+', '=']
+
+function wrapTextWith (cm, symbol) {
+ if (!cm.getSelection()) {
+ return CodeMirror.Pass
+ } else {
+ var ranges = cm.listSelections()
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i]
+ if (!range.empty()) {
+ var from = range.from()
+ var to = range.to()
+ if (symbol !== 'Backspace') {
+ cm.replaceRange(symbol, to, to, '+input')
+ cm.replaceRange(symbol, from, from, '+input')
// workaround selection range not correct after add symbol
- var _ranges = cm.listSelections();
- var anchorIndex = editor.indexFromPos(_ranges[i].anchor);
- var headIndex = editor.indexFromPos(_ranges[i].head);
- if (anchorIndex > headIndex) {
- _ranges[i].anchor.ch--;
- } else {
- _ranges[i].head.ch--;
- }
- cm.setSelections(_ranges);
- } else {
- var preEndPos = {
- line: to.line,
- ch: to.ch + 1
- };
- var preText = cm.getRange(to, preEndPos);
- var preIndex = wrapSymbols.indexOf(preText);
- var postEndPos = {
- line: from.line,
- ch: from.ch - 1
- };
- var postText = cm.getRange(postEndPos, from);
- var postIndex = wrapSymbols.indexOf(postText);
+ var _ranges = cm.listSelections()
+ var anchorIndex = window.editor.indexFromPos(_ranges[i].anchor)
+ var headIndex = window.editor.indexFromPos(_ranges[i].head)
+ if (anchorIndex > headIndex) {
+ _ranges[i].anchor.ch--
+ } else {
+ _ranges[i].head.ch--
+ }
+ cm.setSelections(_ranges)
+ } else {
+ var preEndPos = {
+ line: to.line,
+ ch: to.ch + 1
+ }
+ var preText = cm.getRange(to, preEndPos)
+ var preIndex = wrapSymbols.indexOf(preText)
+ var postEndPos = {
+ line: from.line,
+ ch: from.ch - 1
+ }
+ var postText = cm.getRange(postEndPos, from)
+ var postIndex = wrapSymbols.indexOf(postText)
// check if surround symbol are list in array and matched
- if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) {
- cm.replaceRange("", to, preEndPos, '+delete');
- cm.replaceRange("", postEndPos, from, '+delete');
- }
- }
- }
+ if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) {
+ cm.replaceRange('', to, preEndPos, '+delete')
+ cm.replaceRange('', postEndPos, from, '+delete')
+ }
}
+ }
}
+ }
}
-var idleTime = 300000; //5 mins
-var updateViewDebounce = 100;
-var cursorMenuThrottle = 50;
-var cursorActivityDebounce = 50;
-var cursorAnimatePeriod = 100;
-var supportContainers = ['success', 'info', 'warning', 'danger'];
-var supportCodeModes = ['javascript', 'typescript', 'jsx', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'haskell', 'coffeescript', 'yaml', 'pug', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile', 'tiddlywiki', 'mediawiki', 'go'];
-var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid'];
+var idleTime = 300000 // 5 mins
+var updateViewDebounce = 100
+var cursorMenuThrottle = 50
+var cursorActivityDebounce = 50
+var cursorAnimatePeriod = 100
+var supportContainers = ['success', 'info', 'warning', 'danger']
+var supportCodeModes = ['javascript', 'typescript', 'jsx', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'haskell', 'coffeescript', 'yaml', 'pug', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile', 'tiddlywiki', 'mediawiki', 'go']
+var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid']
var supportHeaders = [
- {
- text: '# h1',
- search: '#'
- },
- {
- text: '## h2',
- search: '##'
- },
- {
- text: '### h3',
- search: '###'
- },
- {
- text: '#### h4',
- search: '####'
- },
- {
- text: '##### h5',
- search: '#####'
- },
- {
- text: '###### h6',
- search: '######'
- },
- {
- text: '###### tags: `example`',
- search: '###### tags:'
- }
-];
+ {
+ text: '# h1',
+ search: '#'
+ },
+ {
+ text: '## h2',
+ search: '##'
+ },
+ {
+ text: '### h3',
+ search: '###'
+ },
+ {
+ text: '#### h4',
+ search: '####'
+ },
+ {
+ text: '##### h5',
+ search: '#####'
+ },
+ {
+ text: '###### h6',
+ search: '######'
+ },
+ {
+ text: '###### tags: `example`',
+ search: '###### tags:'
+ }
+]
var supportReferrals = [
- {
- text: '[reference link]',
- search: '[]'
- },
- {
- text: '[reference]: https:// "title"',
- search: '[]:'
- },
- {
- text: '[^footnote link]',
- search: '[^]'
- },
- {
- text: '[^footnote reference]: https:// "title"',
- search: '[^]:'
- },
- {
- text: '^[inline footnote]',
- search: '^[]'
- },
- {
- text: '[link text][reference]',
- search: '[][]'
- },
- {
- text: '[link text](https:// "title")',
- search: '[]()'
- },
- {
- text: '![image alt][reference]',
- search: '![][]'
- },
- {
- text: '![image alt](https:// "title")',
- search: '![]()'
- },
- {
- text: '![image alt](https:// "title" =WidthxHeight)',
- search: '![]()'
- },
- {
- text: '[TOC]',
- search: '[]'
- }
-];
+ {
+ text: '[reference link]',
+ search: '[]'
+ },
+ {
+ text: '[reference]: https:// "title"',
+ search: '[]:'
+ },
+ {
+ text: '[^footnote link]',
+ search: '[^]'
+ },
+ {
+ text: '[^footnote reference]: https:// "title"',
+ search: '[^]:'
+ },
+ {
+ text: '^[inline footnote]',
+ search: '^[]'
+ },
+ {
+ text: '[link text][reference]',
+ search: '[][]'
+ },
+ {
+ text: '[link text](https:// "title")',
+ search: '[]()'
+ },
+ {
+ text: '![image alt][reference]',
+ search: '![][]'
+ },
+ {
+ text: '![image alt](https:// "title")',
+ search: '![]()'
+ },
+ {
+ text: '![image alt](https:// "title" =WidthxHeight)',
+ search: '![]()'
+ },
+ {
+ text: '[TOC]',
+ search: '[]'
+ }
+]
var supportExternals = [
- {
- text: '{%youtube youtubeid %}',
- search: 'youtube'
- },
- {
- text: '{%vimeo vimeoid %}',
- search: 'vimeo'
- },
- {
- text: '{%gist gistid %}',
- search: 'gist'
- },
- {
- text: '{%slideshare slideshareid %}',
- search: 'slideshare'
- },
- {
- text: '{%speakerdeck speakerdeckid %}',
- search: 'speakerdeck'
- },
- {
- text: '{%pdf pdfurl %}',
- search: 'pdf'
- }
-];
+ {
+ text: '{%youtube youtubeid %}',
+ search: 'youtube'
+ },
+ {
+ text: '{%vimeo vimeoid %}',
+ search: 'vimeo'
+ },
+ {
+ text: '{%gist gistid %}',
+ search: 'gist'
+ },
+ {
+ text: '{%slideshare slideshareid %}',
+ search: 'slideshare'
+ },
+ {
+ text: '{%speakerdeck speakerdeckid %}',
+ search: 'speakerdeck'
+ },
+ {
+ text: '{%pdf pdfurl %}',
+ search: 'pdf'
+ }
+]
var supportExtraTags = [
- {
- text: '[name tag]',
- search: '[]',
- command: function () {
- return '[name=' + personalInfo.name + ']';
- },
- },
- {
- text: '[time tag]',
- search: '[]',
- command: function () {
- return '[time=' + moment().format('llll') + ']';
- },
- },
- {
- text: '[my color tag]',
- search: '[]',
- command: function () {
- return '[color=' + personalInfo.color + ']';
- }
- },
- {
- text: '[random color tag]',
- search: '[]',
- command: function () {
- var color = randomColor();
- return '[color=' + color + ']';
- }
+ {
+ text: '[name tag]',
+ search: '[]',
+ command: function () {
+ return '[name=' + window.personalInfo.name + ']'
}
-];
+ },
+ {
+ text: '[time tag]',
+ search: '[]',
+ command: function () {
+ return '[time=' + moment().format('llll') + ']'
+ }
+ },
+ {
+ text: '[my color tag]',
+ search: '[]',
+ command: function () {
+ return '[color=' + window.personalInfo.color + ']'
+ }
+ },
+ {
+ text: '[random color tag]',
+ search: '[]',
+ command: function () {
+ var color = randomColor()
+ return '[color=' + color + ']'
+ }
+ }
+]
window.modeType = {
- edit: {
- name: "edit"
- },
- view: {
- name: "view"
- },
- both: {
- name: "both"
- }
-};
+ edit: {
+ name: 'edit'
+ },
+ view: {
+ name: 'view'
+ },
+ both: {
+ name: 'both'
+ }
+}
var statusType = {
- connected: {
- msg: "CONNECTED",
- label: "label-warning",
- fa: "fa-wifi"
- },
- online: {
- msg: "ONLINE",
- label: "label-primary",
- fa: "fa-users"
- },
- offline: {
- msg: "OFFLINE",
- label: "label-danger",
- fa: "fa-plug"
- }
-};
-var defaultMode = modeType.view;
+ connected: {
+ msg: 'CONNECTED',
+ label: 'label-warning',
+ fa: 'fa-wifi'
+ },
+ online: {
+ msg: 'ONLINE',
+ label: 'label-primary',
+ fa: 'fa-users'
+ },
+ offline: {
+ msg: 'OFFLINE',
+ label: 'label-danger',
+ fa: 'fa-plug'
+ }
+}
+var defaultMode = modeType.view
-//global vars
-window.loaded = false;
-window.needRefresh = false;
-window.isDirty = false;
-window.editShown = false;
-window.visibleXS = false;
-window.visibleSM = false;
-window.visibleMD = false;
-window.visibleLG = false;
-window.isTouchDevice = 'ontouchstart' in document.documentElement;
-window.currentMode = defaultMode;
-window.currentStatus = statusType.offline;
+// global vars
+window.loaded = false
+window.needRefresh = false
+window.isDirty = false
+window.editShown = false
+window.visibleXS = false
+window.visibleSM = false
+window.visibleMD = false
+window.visibleLG = false
+window.isTouchDevice = 'ontouchstart' in document.documentElement
+window.currentMode = defaultMode
+window.currentStatus = statusType.offline
window.lastInfo = {
- needRestore: false,
- cursor: null,
- scroll: null,
- edit: {
- scroll: {
- left: null,
- top: null
- },
- cursor: {
- line: null,
- ch: null
- },
- selections: null
+ needRestore: false,
+ cursor: null,
+ scroll: null,
+ edit: {
+ scroll: {
+ left: null,
+ top: null
},
- view: {
- scroll: {
- left: null,
- top: null
- }
+ cursor: {
+ line: null,
+ ch: null
},
- history: null
-};
-window.personalInfo = {};
-window.onlineUsers = [];
+ selections: null
+ },
+ view: {
+ scroll: {
+ left: null,
+ top: null
+ }
+ },
+ history: null
+}
+window.personalInfo = {}
+window.onlineUsers = []
window.fileTypes = {
- "pl": "perl",
- "cgi": "perl",
- "js": "javascript",
- "php": "php",
- "sh": "bash",
- "rb": "ruby",
- "html": "html",
- "py": "python"
-};
+ 'pl': 'perl',
+ 'cgi': 'perl',
+ 'js': 'javascript',
+ 'php': 'php',
+ 'sh': 'bash',
+ 'rb': 'ruby',
+ 'html': 'html',
+ 'py': 'python'
+}
-//editor settings
-var textit = document.getElementById("textit");
-if (!textit) throw new Error("There was no textit area!");
+// editor settings
+var textit = document.getElementById('textit')
+if (!textit) throw new Error('There was no textit area!')
window.editor = CodeMirror.fromTextArea(textit, {
- mode: defaultEditorMode,
- backdrop: defaultEditorMode,
- keyMap: "sublime",
- viewportMargin: viewportMargin,
- styleActiveLine: true,
- lineNumbers: true,
- lineWrapping: true,
- showCursorWhenSelecting: true,
- highlightSelectionMatches: true,
- indentUnit: 4,
- continueComments: "Enter",
- theme: "one-dark",
- inputStyle: "textarea",
- matchBrackets: true,
- autoCloseBrackets: true,
- matchTags: {
- bothTags: true
- },
- autoCloseTags: true,
- foldGutter: true,
- gutters: ["CodeMirror-linenumbers", "authorship-gutters", "CodeMirror-foldgutter"],
- extraKeys: defaultExtraKeys,
- flattenSpans: true,
- addModeClass: true,
- readOnly: true,
- autoRefresh: true,
- otherCursors: true,
- placeholder: "← Start by entering a title here\n===\nVisit /features if you don't know what to do.\nHappy hacking :)"
-});
-var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor);
-defaultTextHeight = parseInt($(".CodeMirror").css('line-height'));
+ mode: defaultEditorMode,
+ backdrop: defaultEditorMode,
+ keyMap: 'sublime',
+ viewportMargin: viewportMargin,
+ styleActiveLine: true,
+ lineNumbers: true,
+ lineWrapping: true,
+ showCursorWhenSelecting: true,
+ highlightSelectionMatches: true,
+ indentUnit: 4,
+ continueComments: 'Enter',
+ theme: 'one-dark',
+ inputStyle: 'textarea',
+ matchBrackets: true,
+ autoCloseBrackets: true,
+ matchTags: {
+ bothTags: true
+ },
+ autoCloseTags: true,
+ foldGutter: true,
+ gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'],
+ extraKeys: defaultExtraKeys,
+ flattenSpans: true,
+ addModeClass: true,
+ readOnly: true,
+ autoRefresh: true,
+ otherCursors: true,
+ placeholder: "← Start by entering a title here\n===\nVisit /features if you don't know what to do.\nHappy hacking :)"
+})
+var inlineAttach = window.inlineAttachment.editors.codemirror4.attach(editor)
+defaultTextHeight = parseInt($('.CodeMirror').css('line-height'))
-var statusBarTemplate = null;
-var statusBar = null;
-var statusPanel = null;
-var statusCursor = null;
-var statusFile = null;
-var statusIndicators = null;
-var statusLength = null;
-var statusKeymap = null;
-var statusIndent = null;
-var statusTheme = null;
-var statusSpellcheck = null;
-var statusPreferences = null;
+var statusBarTemplate = null
+var statusBar = null
+var statusCursor = null
+var statusFile = null
+var statusIndicators = null
+var statusLength = null
+var statusTheme = null
+var statusSpellcheck = null
-function getStatusBarTemplate(callback) {
- $.get(serverurl + '/views/statusbar.html', function (template) {
- statusBarTemplate = template;
- if (callback) callback();
- });
+function getStatusBarTemplate (callback) {
+ $.get(serverurl + '/views/statusbar.html', function (template) {
+ statusBarTemplate = template
+ if (callback) callback()
+ })
}
-getStatusBarTemplate();
+getStatusBarTemplate()
-function addStatusBar() {
- if (!statusBarTemplate) {
- getStatusBarTemplate(addStatusBar);
- return;
- }
- statusBar = $(statusBarTemplate);
- statusCursor = statusBar.find('.status-cursor');
- statusFile = statusBar.find('.status-file');
- statusIndicators = statusBar.find('.status-indicators');
- statusIndent = statusBar.find('.status-indent');
- statusKeymap = statusBar.find('.status-keymap');
- statusLength = statusBar.find('.status-length');
- statusTheme = statusBar.find('.status-theme');
- statusSpellcheck = statusBar.find('.status-spellcheck');
- statusPreferences = statusBar.find('.status-preferences');
- statusPanel = editor.addPanel(statusBar[0], {
- position: "bottom"
- });
+function addStatusBar () {
+ if (!statusBarTemplate) {
+ getStatusBarTemplate(addStatusBar)
+ return
+ }
+ statusBar = $(statusBarTemplate)
+ statusCursor = statusBar.find('.status-cursor')
+ statusFile = statusBar.find('.status-file')
+ statusIndicators = statusBar.find('.status-indicators')
+ statusBar.find('.status-indent')
+ statusBar.find('.status-keymap')
+ statusLength = statusBar.find('.status-length')
+ statusTheme = statusBar.find('.status-theme')
+ statusSpellcheck = statusBar.find('.status-spellcheck')
+ statusBar.find('.status-preferences')
+ editor.addPanel(statusBar[0], {
+ position: 'bottom'
+ })
- setIndent();
- setKeymap();
- setTheme();
- setSpellcheck();
- setPreferences();
+ setIndent()
+ setKeymap()
+ setTheme()
+ setSpellcheck()
+ setPreferences()
}
-function setIndent() {
- var cookieIndentType = Cookies.get('indent_type');
- var cookieTabSize = parseInt(Cookies.get('tab_size'));
- var cookieSpaceUnits = parseInt(Cookies.get('space_units'));
- if (cookieIndentType) {
- if (cookieIndentType == 'tab') {
- editor.setOption('indentWithTabs', true);
- if (cookieTabSize)
- editor.setOption('indentUnit', cookieTabSize);
- } else if (cookieIndentType == 'space') {
- editor.setOption('indentWithTabs', false);
- if (cookieSpaceUnits)
- editor.setOption('indentUnit', cookieSpaceUnits);
- }
+function setIndent () {
+ var cookieIndentType = Cookies.get('indent_type')
+ var cookieTabSize = parseInt(Cookies.get('tab_size'))
+ var cookieSpaceUnits = parseInt(Cookies.get('space_units'))
+ if (cookieIndentType) {
+ if (cookieIndentType === 'tab') {
+ editor.setOption('indentWithTabs', true)
+ if (cookieTabSize) { editor.setOption('indentUnit', cookieTabSize) }
+ } else if (cookieIndentType === 'space') {
+ editor.setOption('indentWithTabs', false)
+ if (cookieSpaceUnits) { editor.setOption('indentUnit', cookieSpaceUnits) }
}
- if (cookieTabSize)
- editor.setOption('tabSize', cookieTabSize);
+ }
+ if (cookieTabSize) { editor.setOption('tabSize', cookieTabSize) }
- var type = statusIndicators.find('.indent-type');
- var widthLabel = statusIndicators.find('.indent-width-label');
- var widthInput = statusIndicators.find('.indent-width-input');
+ var type = statusIndicators.find('.indent-type')
+ var widthLabel = statusIndicators.find('.indent-width-label')
+ var widthInput = statusIndicators.find('.indent-width-input')
- function setType() {
- if (editor.getOption('indentWithTabs')) {
- Cookies.set('indent_type', 'tab', {
- expires: 365
- });
- type.text('Tab Size:');
- } else {
- Cookies.set('indent_type', 'space', {
- expires: 365
- });
- type.text('Spaces:');
- }
- }
- setType();
-
- function setUnit() {
- var unit = editor.getOption('indentUnit');
- if (editor.getOption('indentWithTabs')) {
- Cookies.set('tab_size', unit, {
- expires: 365
- });
- } else {
- Cookies.set('space_units', unit, {
- expires: 365
- });
- }
- widthLabel.text(unit);
- }
- setUnit();
-
- type.click(function () {
- if (editor.getOption('indentWithTabs')) {
- editor.setOption('indentWithTabs', false);
- cookieSpaceUnits = parseInt(Cookies.get('space_units'));
- if (cookieSpaceUnits)
- editor.setOption('indentUnit', cookieSpaceUnits)
- } else {
- editor.setOption('indentWithTabs', true);
- cookieTabSize = parseInt(Cookies.get('tab_size'));
- if (cookieTabSize) {
- editor.setOption('indentUnit', cookieTabSize);
- editor.setOption('tabSize', cookieTabSize);
- }
- }
- setType();
- setUnit();
- });
- widthLabel.click(function () {
- if (widthLabel.is(':visible')) {
- widthLabel.addClass('hidden');
- widthInput.removeClass('hidden');
- widthInput.val(editor.getOption('indentUnit'));
- widthInput.select();
- } else {
- widthLabel.removeClass('hidden');
- widthInput.addClass('hidden');
- }
- });
- widthInput.on('change', function () {
- var val = parseInt(widthInput.val());
- if (!val) val = editor.getOption('indentUnit');
- if (val < 1) val = 1;
- else if (val > 10) val = 10;
-
- if (editor.getOption('indentWithTabs')) {
- editor.setOption('tabSize', val);
- }
- editor.setOption('indentUnit', val);
- setUnit();
- });
- widthInput.on('blur', function () {
- widthLabel.removeClass('hidden');
- widthInput.addClass('hidden');
- });
-}
-
-function setKeymap() {
- var cookieKeymap = Cookies.get('keymap');
- if (cookieKeymap)
- editor.setOption('keyMap', cookieKeymap);
-
- var label = statusIndicators.find('.ui-keymap-label');
- var sublime = statusIndicators.find('.ui-keymap-sublime');
- var emacs = statusIndicators.find('.ui-keymap-emacs');
- var vim = statusIndicators.find('.ui-keymap-vim');
-
- function setKeymapLabel() {
- var keymap = editor.getOption('keyMap');
- Cookies.set('keymap', keymap, {
- expires: 365
- });
- label.text(keymap);
- restoreOverrideEditorKeymap();
- setOverrideBrowserKeymap();
- }
- setKeymapLabel();
-
- sublime.click(function () {
- editor.setOption('keyMap', 'sublime');
- setKeymapLabel();
- });
- emacs.click(function () {
- editor.setOption('keyMap', 'emacs');
- setKeymapLabel();
- });
- vim.click(function () {
- editor.setOption('keyMap', 'vim');
- setKeymapLabel();
- });
-}
-
-function setTheme() {
- var cookieTheme = Cookies.get('theme');
- if (cookieTheme) {
- editor.setOption('theme', cookieTheme);
- }
-
- var themeToggle = statusTheme.find('.ui-theme-toggle');
- themeToggle.click(function () {
- var theme = editor.getOption('theme');
- if (theme == "one-dark") {
- theme = "default";
- } else {
- theme = "one-dark";
- }
- editor.setOption('theme', theme);
- Cookies.set('theme', theme, {
- expires: 365
- });
- checkTheme();
- });
- function checkTheme() {
- var theme = editor.getOption('theme');
- if (theme == "one-dark") {
- themeToggle.removeClass('active');
- } else {
- themeToggle.addClass('active');
- }
- }
- checkTheme();
-}
-
-function setSpellcheck() {
- var cookieSpellcheck = Cookies.get('spellcheck');
- if (cookieSpellcheck) {
- var mode = null;
- if (cookieSpellcheck === 'true' || cookieSpellcheck === true) {
- mode = 'spell-checker';
- } else {
- mode = defaultEditorMode;
- }
- if (mode && mode !== editor.getOption('mode')) {
- editor.setOption('mode', mode);
- }
- }
-
- var spellcheckToggle = statusSpellcheck.find('.ui-spellcheck-toggle');
- spellcheckToggle.click(function () {
- var mode = editor.getOption('mode');
- if (mode == defaultEditorMode) {
- mode = "spell-checker";
- } else {
- mode = defaultEditorMode;
- }
- if (mode && mode !== editor.getOption('mode')) {
- editor.setOption('mode', mode);
- }
- Cookies.set('spellcheck', (mode == "spell-checker"), {
- expires: 365
- });
- checkSpellcheck();
- });
- function checkSpellcheck() {
- var mode = editor.getOption('mode');
- if (mode == defaultEditorMode) {
- spellcheckToggle.removeClass('active');
- } else {
- spellcheckToggle.addClass('active');
- }
- }
- checkSpellcheck();
-
- //workaround spellcheck might not activate beacuse the ajax loading
- if (num_loaded < 2) {
- var spellcheckTimer = setInterval(function () {
- if (num_loaded >= 2) {
- if (editor.getOption('mode') == "spell-checker")
- editor.setOption('mode', "spell-checker");
- clearInterval(spellcheckTimer);
- }
- }, 100);
- }
-}
-
-var jumpToAddressBarKeymapName = mac ? "Cmd-L" : "Ctrl-L";
-var jumpToAddressBarKeymapValue = null;
-function resetEditorKeymapToBrowserKeymap() {
- var keymap = editor.getOption('keyMap');
- if (!jumpToAddressBarKeymapValue) {
- jumpToAddressBarKeymapValue = CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName];
- delete CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName];
- }
-}
-function restoreOverrideEditorKeymap() {
- var keymap = editor.getOption('keyMap');
- if (jumpToAddressBarKeymapValue) {
- CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] = jumpToAddressBarKeymapValue;
- jumpToAddressBarKeymapValue = null;
- }
-}
-function setOverrideBrowserKeymap() {
- var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]');
- if(overrideBrowserKeymap.is(":checked")) {
- Cookies.set('preferences-override-browser-keymap', true, {
- expires: 365
- });
- restoreOverrideEditorKeymap();
+ function setType () {
+ if (editor.getOption('indentWithTabs')) {
+ Cookies.set('indent_type', 'tab', {
+ expires: 365
+ })
+ type.text('Tab Size:')
} else {
- Cookies.remove('preferences-override-browser-keymap');
- resetEditorKeymapToBrowserKeymap();
+ Cookies.set('indent_type', 'space', {
+ expires: 365
+ })
+ type.text('Spaces:')
}
-}
+ }
+ setType()
-function setPreferences() {
- var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]');
- var cookieOverrideBrowserKeymap = Cookies.get('preferences-override-browser-keymap');
- if (cookieOverrideBrowserKeymap && cookieOverrideBrowserKeymap === "true") {
- overrideBrowserKeymap.prop('checked', true);
+ function setUnit () {
+ var unit = editor.getOption('indentUnit')
+ if (editor.getOption('indentWithTabs')) {
+ Cookies.set('tab_size', unit, {
+ expires: 365
+ })
} else {
- overrideBrowserKeymap.prop('checked', false);
+ Cookies.set('space_units', unit, {
+ expires: 365
+ })
}
- setOverrideBrowserKeymap();
+ widthLabel.text(unit)
+ }
+ setUnit()
- overrideBrowserKeymap.change(function() {
- setOverrideBrowserKeymap();
- });
+ type.click(function () {
+ if (editor.getOption('indentWithTabs')) {
+ editor.setOption('indentWithTabs', false)
+ cookieSpaceUnits = parseInt(Cookies.get('space_units'))
+ if (cookieSpaceUnits) { editor.setOption('indentUnit', cookieSpaceUnits) }
+ } else {
+ editor.setOption('indentWithTabs', true)
+ cookieTabSize = parseInt(Cookies.get('tab_size'))
+ if (cookieTabSize) {
+ editor.setOption('indentUnit', cookieTabSize)
+ editor.setOption('tabSize', cookieTabSize)
+ }
+ }
+ setType()
+ setUnit()
+ })
+ widthLabel.click(function () {
+ if (widthLabel.is(':visible')) {
+ widthLabel.addClass('hidden')
+ widthInput.removeClass('hidden')
+ widthInput.val(editor.getOption('indentUnit'))
+ widthInput.select()
+ } else {
+ widthLabel.removeClass('hidden')
+ widthInput.addClass('hidden')
+ }
+ })
+ widthInput.on('change', function () {
+ var val = parseInt(widthInput.val())
+ if (!val) val = editor.getOption('indentUnit')
+ if (val < 1) val = 1
+ else if (val > 10) val = 10
+
+ if (editor.getOption('indentWithTabs')) {
+ editor.setOption('tabSize', val)
+ }
+ editor.setOption('indentUnit', val)
+ setUnit()
+ })
+ widthInput.on('blur', function () {
+ widthLabel.removeClass('hidden')
+ widthInput.addClass('hidden')
+ })
}
-var selection = null;
+function setKeymap () {
+ var cookieKeymap = Cookies.get('keymap')
+ if (cookieKeymap) { editor.setOption('keyMap', cookieKeymap) }
-function updateStatusBar() {
- if (!statusBar) return;
- var cursor = editor.getCursor();
- var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1);
- if (selection) {
- var anchor = selection.anchor;
- var head = selection.head;
- var start = head.line <= anchor.line ? head : anchor;
- var end = head.line >= anchor.line ? head : anchor;
- var selectionText = ' — Selected ';
- var selectionCharCount = Math.abs(head.ch - anchor.ch);
+ var label = statusIndicators.find('.ui-keymap-label')
+ var sublime = statusIndicators.find('.ui-keymap-sublime')
+ var emacs = statusIndicators.find('.ui-keymap-emacs')
+ var vim = statusIndicators.find('.ui-keymap-vim')
+
+ function setKeymapLabel () {
+ var keymap = editor.getOption('keyMap')
+ Cookies.set('keymap', keymap, {
+ expires: 365
+ })
+ label.text(keymap)
+ restoreOverrideEditorKeymap()
+ setOverrideBrowserKeymap()
+ }
+ setKeymapLabel()
+
+ sublime.click(function () {
+ editor.setOption('keyMap', 'sublime')
+ setKeymapLabel()
+ })
+ emacs.click(function () {
+ editor.setOption('keyMap', 'emacs')
+ setKeymapLabel()
+ })
+ vim.click(function () {
+ editor.setOption('keyMap', 'vim')
+ setKeymapLabel()
+ })
+}
+
+function setTheme () {
+ var cookieTheme = Cookies.get('theme')
+ if (cookieTheme) {
+ editor.setOption('theme', cookieTheme)
+ }
+
+ var themeToggle = statusTheme.find('.ui-theme-toggle')
+ themeToggle.click(function () {
+ var theme = editor.getOption('theme')
+ if (theme === 'one-dark') {
+ theme = 'default'
+ } else {
+ theme = 'one-dark'
+ }
+ editor.setOption('theme', theme)
+ Cookies.set('theme', theme, {
+ expires: 365
+ })
+ checkTheme()
+ })
+ function checkTheme () {
+ var theme = editor.getOption('theme')
+ if (theme === 'one-dark') {
+ themeToggle.removeClass('active')
+ } else {
+ themeToggle.addClass('active')
+ }
+ }
+ checkTheme()
+}
+
+function setSpellcheck () {
+ var cookieSpellcheck = Cookies.get('spellcheck')
+ if (cookieSpellcheck) {
+ var mode = null
+ if (cookieSpellcheck === 'true' || cookieSpellcheck === true) {
+ mode = 'spell-checker'
+ } else {
+ mode = defaultEditorMode
+ }
+ if (mode && mode !== editor.getOption('mode')) {
+ editor.setOption('mode', mode)
+ }
+ }
+
+ var spellcheckToggle = statusSpellcheck.find('.ui-spellcheck-toggle')
+ spellcheckToggle.click(function () {
+ var mode = editor.getOption('mode')
+ if (mode === defaultEditorMode) {
+ mode = 'spell-checker'
+ } else {
+ mode = defaultEditorMode
+ }
+ if (mode && mode !== editor.getOption('mode')) {
+ editor.setOption('mode', mode)
+ }
+ Cookies.set('spellcheck', (mode === 'spell-checker'), {
+ expires: 365
+ })
+ checkSpellcheck()
+ })
+ function checkSpellcheck () {
+ var mode = editor.getOption('mode')
+ if (mode === defaultEditorMode) {
+ spellcheckToggle.removeClass('active')
+ } else {
+ spellcheckToggle.addClass('active')
+ }
+ }
+ checkSpellcheck()
+
+ // workaround spellcheck might not activate beacuse the ajax loading
+ /* eslint-disable camelcase */
+ if (num_loaded < 2) {
+ var spellcheckTimer = setInterval(function () {
+ if (num_loaded >= 2) {
+ if (editor.getOption('mode') === 'spell-checker') { editor.setOption('mode', 'spell-checker') }
+ clearInterval(spellcheckTimer)
+ }
+ }, 100)
+ }
+ /* eslint-endable camelcase */
+}
+
+var jumpToAddressBarKeymapName = mac ? 'Cmd-L' : 'Ctrl-L'
+var jumpToAddressBarKeymapValue = null
+function resetEditorKeymapToBrowserKeymap () {
+ var keymap = editor.getOption('keyMap')
+ if (!jumpToAddressBarKeymapValue) {
+ jumpToAddressBarKeymapValue = CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]
+ delete CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]
+ }
+}
+function restoreOverrideEditorKeymap () {
+ var keymap = editor.getOption('keyMap')
+ if (jumpToAddressBarKeymapValue) {
+ CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] = jumpToAddressBarKeymapValue
+ jumpToAddressBarKeymapValue = null
+ }
+}
+function setOverrideBrowserKeymap () {
+ var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]')
+ if (overrideBrowserKeymap.is(':checked')) {
+ Cookies.set('preferences-override-browser-keymap', true, {
+ expires: 365
+ })
+ restoreOverrideEditorKeymap()
+ } else {
+ Cookies.remove('preferences-override-browser-keymap')
+ resetEditorKeymapToBrowserKeymap()
+ }
+}
+
+function setPreferences () {
+ var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]')
+ var cookieOverrideBrowserKeymap = Cookies.get('preferences-override-browser-keymap')
+ if (cookieOverrideBrowserKeymap && cookieOverrideBrowserKeymap === 'true') {
+ overrideBrowserKeymap.prop('checked', true)
+ } else {
+ overrideBrowserKeymap.prop('checked', false)
+ }
+ setOverrideBrowserKeymap()
+
+ overrideBrowserKeymap.change(function () {
+ setOverrideBrowserKeymap()
+ })
+}
+
+var selection = null
+
+function updateStatusBar () {
+ if (!statusBar) return
+ var cursor = editor.getCursor()
+ var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1)
+ if (selection) {
+ var anchor = selection.anchor
+ var head = selection.head
+ var start = head.line <= anchor.line ? head : anchor
+ var end = head.line >= anchor.line ? head : anchor
+ var selectionText = ' — Selected '
+ var selectionCharCount = Math.abs(head.ch - anchor.ch)
// borrow from brackets EditorStatusBar.js
- if (start.line !== end.line) {
- var lines = end.line - start.line + 1;
- if (end.ch === 0) {
- lines--;
- }
- selectionText += lines + ' lines';
- } else if (selectionCharCount > 0)
- selectionText += selectionCharCount + ' columns';
- if (start.line !== end.line || selectionCharCount > 0)
- cursorText += selectionText;
- }
- statusCursor.text(cursorText);
- var fileText = ' — ' + editor.lineCount() + ' Lines';
- statusFile.text(fileText);
- var docLength = editor.getValue().length;
- statusLength.text('Length ' + docLength);
- if (docLength > (docmaxlength * 0.95)) {
- statusLength.css('color', 'red');
- statusLength.attr('title', 'Your almost reach note max length limit.');
- } else if (docLength > (docmaxlength * 0.8)) {
- statusLength.css('color', 'orange');
- statusLength.attr('title', 'You nearly fill the note, consider to make more pieces.');
- } else {
- statusLength.css('color', 'white');
- statusLength.attr('title', 'You could write up to ' + docmaxlength + ' characters in this note.');
- }
+ if (start.line !== end.line) {
+ var lines = end.line - start.line + 1
+ if (end.ch === 0) {
+ lines--
+ }
+ selectionText += lines + ' lines'
+ } else if (selectionCharCount > 0) { selectionText += selectionCharCount + ' columns' }
+ if (start.line !== end.line || selectionCharCount > 0) { cursorText += selectionText }
+ }
+ statusCursor.text(cursorText)
+ var fileText = ' — ' + editor.lineCount() + ' Lines'
+ statusFile.text(fileText)
+ var docLength = editor.getValue().length
+ statusLength.text('Length ' + docLength)
+ if (docLength > (docmaxlength * 0.95)) {
+ statusLength.css('color', 'red')
+ statusLength.attr('title', 'Your almost reach note max length limit.')
+ } else if (docLength > (docmaxlength * 0.8)) {
+ statusLength.css('color', 'orange')
+ statusLength.attr('title', 'You nearly fill the note, consider to make more pieces.')
+ } else {
+ statusLength.css('color', 'white')
+ statusLength.attr('title', 'You could write up to ' + docmaxlength + ' characters in this note.')
+ }
}
-//ui vars
+// ui vars
window.ui = {
- spinner: $(".ui-spinner"),
- content: $(".ui-content"),
- toolbar: {
- shortStatus: $(".ui-short-status"),
- status: $(".ui-status"),
- new: $(".ui-new"),
- publish: $(".ui-publish"),
- extra: {
- revision: $(".ui-extra-revision"),
- slide: $(".ui-extra-slide")
- },
- download: {
- markdown: $(".ui-download-markdown"),
- html: $(".ui-download-html"),
- rawhtml: $(".ui-download-raw-html"),
- pdf: $(".ui-download-pdf-beta"),
- },
- export: {
- dropbox: $(".ui-save-dropbox"),
- googleDrive: $(".ui-save-google-drive"),
- gist: $(".ui-save-gist"),
- snippet: $(".ui-save-snippet")
- },
- import: {
- dropbox: $(".ui-import-dropbox"),
- googleDrive: $(".ui-import-google-drive"),
- gist: $(".ui-import-gist"),
- snippet: $(".ui-import-snippet"),
- clipboard: $(".ui-import-clipboard")
- },
- mode: $(".ui-mode"),
- edit: $(".ui-edit"),
- view: $(".ui-view"),
- both: $(".ui-both"),
- uploadImage: $(".ui-upload-image")
+ spinner: $('.ui-spinner'),
+ content: $('.ui-content'),
+ toolbar: {
+ shortStatus: $('.ui-short-status'),
+ status: $('.ui-status'),
+ new: $('.ui-new'),
+ publish: $('.ui-publish'),
+ extra: {
+ revision: $('.ui-extra-revision'),
+ slide: $('.ui-extra-slide')
},
- infobar: {
- lastchange: $(".ui-lastchange"),
- lastchangeuser: $(".ui-lastchangeuser"),
- nolastchangeuser: $(".ui-no-lastchangeuser"),
- permission: {
- permission: $(".ui-permission"),
- label: $(".ui-permission-label"),
- freely: $(".ui-permission-freely"),
- editable: $(".ui-permission-editable"),
- locked: $(".ui-permission-locked"),
- private: $(".ui-permission-private"),
- limited: $(".ui-permission-limited"),
- protected: $(".ui-permission-protected")
- },
- delete: $(".ui-delete-note")
+ download: {
+ markdown: $('.ui-download-markdown'),
+ html: $('.ui-download-html'),
+ rawhtml: $('.ui-download-raw-html'),
+ pdf: $('.ui-download-pdf-beta')
},
- toc: {
- toc: $('.ui-toc'),
- affix: $('.ui-affix-toc'),
- label: $('.ui-toc-label'),
- dropdown: $('.ui-toc-dropdown')
+ export: {
+ dropbox: $('.ui-save-dropbox'),
+ googleDrive: $('.ui-save-google-drive'),
+ gist: $('.ui-save-gist'),
+ snippet: $('.ui-save-snippet')
},
- area: {
- edit: $(".ui-edit-area"),
- view: $(".ui-view-area"),
- codemirror: $(".ui-edit-area .CodeMirror"),
- codemirrorScroll: $(".ui-edit-area .CodeMirror .CodeMirror-scroll"),
- codemirrorSizer: $(".ui-edit-area .CodeMirror .CodeMirror-sizer"),
- codemirrorSizerInner: $(".ui-edit-area .CodeMirror .CodeMirror-sizer > div"),
- markdown: $(".ui-view-area .markdown-body"),
- resize: {
- handle: $('.ui-resizable-handle'),
- syncToggle: $('.ui-sync-toggle')
- }
+ import: {
+ dropbox: $('.ui-import-dropbox'),
+ googleDrive: $('.ui-import-google-drive'),
+ gist: $('.ui-import-gist'),
+ snippet: $('.ui-import-snippet'),
+ clipboard: $('.ui-import-clipboard')
},
- modal: {
- snippetImportProjects: $("#snippetImportModalProjects"),
- snippetImportSnippets: $("#snippetImportModalSnippets"),
- revision: $("#revisionModal")
+ mode: $('.ui-mode'),
+ edit: $('.ui-edit'),
+ view: $('.ui-view'),
+ both: $('.ui-both'),
+ uploadImage: $('.ui-upload-image')
+ },
+ infobar: {
+ lastchange: $('.ui-lastchange'),
+ lastchangeuser: $('.ui-lastchangeuser'),
+ nolastchangeuser: $('.ui-no-lastchangeuser'),
+ permission: {
+ permission: $('.ui-permission'),
+ label: $('.ui-permission-label'),
+ freely: $('.ui-permission-freely'),
+ editable: $('.ui-permission-editable'),
+ locked: $('.ui-permission-locked'),
+ private: $('.ui-permission-private'),
+ limited: $('.ui-permission-limited'),
+ protected: $('.ui-permission-protected')
+ },
+ delete: $('.ui-delete-note')
+ },
+ toc: {
+ toc: $('.ui-toc'),
+ affix: $('.ui-affix-toc'),
+ label: $('.ui-toc-label'),
+ dropdown: $('.ui-toc-dropdown')
+ },
+ area: {
+ edit: $('.ui-edit-area'),
+ view: $('.ui-view-area'),
+ codemirror: $('.ui-edit-area .CodeMirror'),
+ codemirrorScroll: $('.ui-edit-area .CodeMirror .CodeMirror-scroll'),
+ codemirrorSizer: $('.ui-edit-area .CodeMirror .CodeMirror-sizer'),
+ codemirrorSizerInner: $('.ui-edit-area .CodeMirror .CodeMirror-sizer > div'),
+ markdown: $('.ui-view-area .markdown-body'),
+ resize: {
+ handle: $('.ui-resizable-handle'),
+ syncToggle: $('.ui-sync-toggle')
}
-};
+ },
+ modal: {
+ snippetImportProjects: $('#snippetImportModalProjects'),
+ snippetImportSnippets: $('#snippetImportModalSnippets'),
+ revision: $('#revisionModal')
+ }
+}
-//page actions
+// page actions
var opts = {
- lines: 11, // The number of lines to draw
- length: 20, // The length of each line
- width: 2, // The line thickness
- radius: 30, // The radius of the inner circle
- corners: 0, // Corner roundness (0..1)
- rotate: 0, // The rotation offset
- direction: 1, // 1: clockwise, -1: counterclockwise
- color: '#000', // #rgb or #rrggbb or array of colors
- speed: 1.1, // Rounds per second
- trail: 60, // Afterglow percentage
- shadow: false, // Whether to render a shadow
- hwaccel: true, // Whether to use hardware acceleration
- className: 'spinner', // The CSS class to assign to the spinner
- zIndex: 2e9, // The z-index (defaults to 2000000000)
- top: '50%', // Top position relative to parent
- left: '50%' // Left position relative to parent
-};
-var spinner = new Spinner(opts).spin(ui.spinner[0]);
+ lines: 11, // The number of lines to draw
+ length: 20, // The length of each line
+ width: 2, // The line thickness
+ radius: 30, // The radius of the inner circle
+ corners: 0, // Corner roundness (0..1)
+ rotate: 0, // The rotation offset
+ direction: 1, // 1: clockwise, -1: counterclockwise
+ color: '#000', // #rgb or #rrggbb or array of colors
+ speed: 1.1, // Rounds per second
+ trail: 60, // Afterglow percentage
+ shadow: false, // Whether to render a shadow
+ hwaccel: true, // Whether to use hardware acceleration
+ className: 'spinner', // The CSS class to assign to the spinner
+ zIndex: 2e9, // The z-index (defaults to 2000000000)
+ top: '50%', // Top position relative to parent
+ left: '50%' // Left position relative to parent
+}
-//idle
+/* eslint-disable no-unused-vars */
+var spinner = new Spinner(opts).spin(ui.spinner[0])
+/* eslint-enable no-unused-vars */
+
+// idle
var idle = new Idle({
- onAway: function () {
- idle.isAway = true;
- emitUserStatus();
- updateOnlineStatus();
- },
- onAwayBack: function () {
- idle.isAway = false;
- emitUserStatus();
- updateOnlineStatus();
- setHaveUnreadChanges(false);
- updateTitleReminder();
- },
- awayTimeout: idleTime
-});
+ onAway: function () {
+ idle.isAway = true
+ emitUserStatus()
+ updateOnlineStatus()
+ },
+ onAwayBack: function () {
+ idle.isAway = false
+ emitUserStatus()
+ updateOnlineStatus()
+ setHaveUnreadChanges(false)
+ updateTitleReminder()
+ },
+ awayTimeout: idleTime
+})
ui.area.codemirror.on('touchstart', function () {
- idle.onActive();
-});
+ idle.onActive()
+})
-var haveUnreadChanges = false;
+var haveUnreadChanges = false
-function setHaveUnreadChanges(bool) {
- if (!loaded) return;
- if (bool && (idle.isAway || Visibility.hidden())) {
- haveUnreadChanges = true;
- } else if (!bool && !idle.isAway && !Visibility.hidden()) {
- haveUnreadChanges = false;
- }
+function setHaveUnreadChanges (bool) {
+ if (!window.loaded) return
+ if (bool && (idle.isAway || Visibility.hidden())) {
+ haveUnreadChanges = true
+ } else if (!bool && !idle.isAway && !Visibility.hidden()) {
+ haveUnreadChanges = false
+ }
}
-function updateTitleReminder() {
- if (!loaded) return;
- if (haveUnreadChanges) {
- document.title = '• ' + renderTitle(ui.area.markdown);
- } else {
- document.title = renderTitle(ui.area.markdown);
- }
+function updateTitleReminder () {
+ if (!window.loaded) return
+ if (haveUnreadChanges) {
+ document.title = '• ' + renderTitle(ui.area.markdown)
+ } else {
+ document.title = renderTitle(ui.area.markdown)
+ }
}
-function setRefreshModal(status) {
- $('#refreshModal').modal('show');
- $('#refreshModal').find('.modal-body > div').hide();
- $('#refreshModal').find('.' + status).show();
+function setRefreshModal (status) {
+ $('#refreshModal').modal('show')
+ $('#refreshModal').find('.modal-body > div').hide()
+ $('#refreshModal').find('.' + status).show()
}
-function setNeedRefresh() {
- needRefresh = true;
- editor.setOption('readOnly', true);
- socket.disconnect();
- showStatus(statusType.offline);
+function setNeedRefresh () {
+ window.needRefresh = true
+ editor.setOption('readOnly', true)
+ socket.disconnect()
+ showStatus(statusType.offline)
}
setloginStateChangeEvent(function () {
- setRefreshModal('user-state-changed');
- setNeedRefresh();
-});
+ setRefreshModal('user-state-changed')
+ setNeedRefresh()
+})
-//visibility
-var wasFocus = false;
+// visibility
+var wasFocus = false
Visibility.change(function (e, state) {
- var hidden = Visibility.hidden();
- if (hidden) {
- if (editorHasFocus()) {
- wasFocus = true;
- editor.getInputField().blur();
- }
- } else {
- if (wasFocus) {
- if (!visibleXS) {
- editor.focus();
- editor.refresh();
- }
- wasFocus = false;
- }
- setHaveUnreadChanges(false);
+ var hidden = Visibility.hidden()
+ if (hidden) {
+ if (editorHasFocus()) {
+ wasFocus = true
+ editor.getInputField().blur()
}
- updateTitleReminder();
-});
+ } else {
+ if (wasFocus) {
+ if (!window.visibleXS) {
+ editor.focus()
+ editor.refresh()
+ }
+ wasFocus = false
+ }
+ setHaveUnreadChanges(false)
+ }
+ updateTitleReminder()
+})
-//when page ready
+// when page ready
$(document).ready(function () {
- idle.checkAway();
- checkResponsive();
- //if in smaller screen, we don't need advanced scrollbar
- var scrollbarStyle;
- if (visibleXS) {
- scrollbarStyle = 'native';
- } else {
- scrollbarStyle = 'overlay';
- }
- if (scrollbarStyle != editor.getOption('scrollbarStyle')) {
- editor.setOption('scrollbarStyle', scrollbarStyle);
- clearMap();
- }
- checkEditorStyle();
+ idle.checkAway()
+ checkResponsive()
+ // if in smaller screen, we don't need advanced scrollbar
+ var scrollbarStyle
+ if (window.visibleXS) {
+ scrollbarStyle = 'native'
+ } else {
+ scrollbarStyle = 'overlay'
+ }
+ if (scrollbarStyle !== editor.getOption('scrollbarStyle')) {
+ editor.setOption('scrollbarStyle', scrollbarStyle)
+ clearMap()
+ }
+ checkEditorStyle()
/* we need this only on touch devices */
- if (isTouchDevice) {
+ if (window.isTouchDevice) {
/* cache dom references */
- var $body = jQuery('body');
+ var $body = jQuery('body')
/* bind events */
- $(document)
+ $(document)
.on('focus', 'textarea, input', function () {
- $body.addClass('fixfixed');
+ $body.addClass('fixfixed')
})
.on('blur', 'textarea, input', function () {
- $body.removeClass('fixfixed');
- });
- }
- //showup
- $().showUp('.navbar', {
- upClass: 'navbar-hide',
- downClass: 'navbar-show'
- });
- //tooltip
- $('[data-toggle="tooltip"]').tooltip();
+ $body.removeClass('fixfixed')
+ })
+ }
+ // showup
+ $().showUp('.navbar', {
+ upClass: 'navbar-hide',
+ downClass: 'navbar-show'
+ })
+ // tooltip
+ $('[data-toggle="tooltip"]').tooltip()
// shortcuts
// allow on all tags
- key.filter = function (e) { return true; };
- key('ctrl+alt+e', function (e) {
- changeMode(modeType.edit);
- });
- key('ctrl+alt+v', function (e) {
- changeMode(modeType.view);
- });
- key('ctrl+alt+b', function (e) {
- changeMode(modeType.both);
- });
+ key.filter = function (e) { return true }
+ key('ctrl+alt+e', function (e) {
+ changeMode(modeType.edit)
+ })
+ key('ctrl+alt+v', function (e) {
+ changeMode(modeType.view)
+ })
+ key('ctrl+alt+b', function (e) {
+ changeMode(modeType.both)
+ })
// toggle-dropdown
- $(document).on('click', '.toggle-dropdown .dropdown-menu', function (e) {
- e.stopPropagation();
- });
-});
-//when page resize
+ $(document).on('click', '.toggle-dropdown .dropdown-menu', function (e) {
+ e.stopPropagation()
+ })
+})
+// when page resize
$(window).resize(function () {
- checkLayout();
- checkEditorStyle();
- checkTocStyle();
- checkCursorMenu();
- windowResize();
-});
-//when page unload
+ checkLayout()
+ checkEditorStyle()
+ checkTocStyle()
+ checkCursorMenu()
+ windowResize()
+})
+// when page unload
$(window).on('unload', function () {
- //updateHistoryInner();
-});
+ // updateHistoryInner();
+})
$(window).on('error', function () {
- //setNeedRefresh();
-});
+ // setNeedRefresh();
+})
-setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown);
+setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown)
-function autoSyncscroll() {
- if (editorHasFocus()) {
- syncScrollToView();
- } else {
- syncScrollToEdit();
- }
+function autoSyncscroll () {
+ if (editorHasFocus()) {
+ syncScrollToView()
+ } else {
+ syncScrollToEdit()
+ }
}
-var windowResizeDebounce = 200;
-var windowResize = _.debounce(windowResizeInner, windowResizeDebounce);
+var windowResizeDebounce = 200
+var windowResize = _.debounce(windowResizeInner, windowResizeDebounce)
-function windowResizeInner(callback) {
- checkLayout();
- checkResponsive();
- checkEditorStyle();
- checkTocStyle();
- checkCursorMenu();
- //refresh editor
- if (loaded) {
- if (editor.getOption('scrollbarStyle') === 'native') {
- setTimeout(function () {
- clearMap();
- autoSyncscroll();
- updateScrollspy();
- if (callback && typeof callback === 'function')
- callback();
- }, 1);
- } else {
+function windowResizeInner (callback) {
+ checkLayout()
+ checkResponsive()
+ checkEditorStyle()
+ checkTocStyle()
+ checkCursorMenu()
+ // refresh editor
+ if (window.loaded) {
+ if (editor.getOption('scrollbarStyle') === 'native') {
+ setTimeout(function () {
+ clearMap()
+ autoSyncscroll()
+ updateScrollspy()
+ if (callback && typeof callback === 'function') { callback() }
+ }, 1)
+ } else {
// force it load all docs at once to prevent scroll knob blink
- editor.setOption('viewportMargin', Infinity);
- setTimeout(function () {
- clearMap();
- autoSyncscroll();
- editor.setOption('viewportMargin', viewportMargin);
- //add or update user cursors
- for (var i = 0; i < onlineUsers.length; i++) {
- if (onlineUsers[i].id != personalInfo.id)
- buildCursor(onlineUsers[i]);
- }
- updateScrollspy();
- if (callback && typeof callback === 'function')
- callback();
- }, 1);
+ editor.setOption('viewportMargin', Infinity)
+ setTimeout(function () {
+ clearMap()
+ autoSyncscroll()
+ editor.setOption('viewportMargin', viewportMargin)
+ // add or update user cursors
+ for (var i = 0; i < window.onlineUsers.length; i++) {
+ if (window.onlineUsers[i].id !== window.personalInfo.id) { buildCursor(window.onlineUsers[i]) }
}
+ updateScrollspy()
+ if (callback && typeof callback === 'function') { callback() }
+ }, 1)
}
+ }
}
-function checkLayout() {
- var navbarHieght = $('.navbar').outerHeight();
- $('body').css('padding-top', navbarHieght + 'px');
+function checkLayout () {
+ var navbarHieght = $('.navbar').outerHeight()
+ $('body').css('padding-top', navbarHieght + 'px')
}
-function editorHasFocus() {
- return $(editor.getInputField()).is(":focus");
+function editorHasFocus () {
+ return $(editor.getInputField()).is(':focus')
}
-//768-792px have a gap
-function checkResponsive() {
- visibleXS = $(".visible-xs").is(":visible");
- visibleSM = $(".visible-sm").is(":visible");
- visibleMD = $(".visible-md").is(":visible");
- visibleLG = $(".visible-lg").is(":visible");
+// 768-792px have a gap
+function checkResponsive () {
+ window.visibleXS = $('.visible-xs').is(':visible')
+ window.visibleSM = $('.visible-sm').is(':visible')
+ window.visibleMD = $('.visible-md').is(':visible')
+ window.visibleLG = $('.visible-lg').is(':visible')
- if (visibleXS && currentMode == modeType.both)
- if (editorHasFocus())
- changeMode(modeType.edit);
- else
- changeMode(modeType.view);
+ if (window.visibleXS && window.currentMode === modeType.both) {
+ if (editorHasFocus()) { changeMode(modeType.edit) } else { changeMode(modeType.view) }
+ }
- emitUserStatus();
+ emitUserStatus()
}
-var lastEditorWidth = 0;
-var previousFocusOnEditor = null;
+var lastEditorWidth = 0
+var previousFocusOnEditor = null
-function checkEditorStyle() {
- var desireHeight = statusBar ? (ui.area.edit.height() - statusBar.outerHeight()) : ui.area.edit.height();
+function checkEditorStyle () {
+ var desireHeight = statusBar ? (ui.area.edit.height() - statusBar.outerHeight()) : ui.area.edit.height()
// set editor height and min height based on scrollbar style and mode
- var scrollbarStyle = editor.getOption('scrollbarStyle');
- if (scrollbarStyle == 'overlay' || currentMode == modeType.both) {
- ui.area.codemirrorScroll.css('height', desireHeight + 'px');
- ui.area.codemirrorScroll.css('min-height', '');
- checkEditorScrollbar();
- } else if (scrollbarStyle == 'native') {
- ui.area.codemirrorScroll.css('height', '');
- ui.area.codemirrorScroll.css('min-height', desireHeight + 'px');
- }
+ var scrollbarStyle = editor.getOption('scrollbarStyle')
+ if (scrollbarStyle === 'overlay' || window.currentMode === modeType.both) {
+ ui.area.codemirrorScroll.css('height', desireHeight + 'px')
+ ui.area.codemirrorScroll.css('min-height', '')
+ checkEditorScrollbar()
+ } else if (scrollbarStyle === 'native') {
+ ui.area.codemirrorScroll.css('height', '')
+ ui.area.codemirrorScroll.css('min-height', desireHeight + 'px')
+ }
// workaround editor will have wrong doc height when editor height changed
- editor.setSize(null, ui.area.edit.height());
- //make editor resizable
- if (!ui.area.resize.handle.length) {
- ui.area.edit.resizable({
- handles: 'e',
- maxWidth: $(window).width() * 0.7,
- minWidth: $(window).width() * 0.2,
- create: function (e, ui) {
- $(this).parent().on('resize', function (e) {
- e.stopPropagation();
- });
- },
- start: function (e) {
- editor.setOption('viewportMargin', Infinity);
- },
- resize: function (e) {
- ui.area.resize.syncToggle.stop(true, true).show();
- checkTocStyle();
- },
- stop: function (e) {
- lastEditorWidth = ui.area.edit.width();
+ editor.setSize(null, ui.area.edit.height())
+ // make editor resizable
+ if (!ui.area.resize.handle.length) {
+ ui.area.edit.resizable({
+ handles: 'e',
+ maxWidth: $(window).width() * 0.7,
+ minWidth: $(window).width() * 0.2,
+ create: function (e, ui) {
+ $(this).parent().on('resize', function (e) {
+ e.stopPropagation()
+ })
+ },
+ start: function (e) {
+ editor.setOption('viewportMargin', Infinity)
+ },
+ resize: function (e) {
+ ui.area.resize.syncToggle.stop(true, true).show()
+ checkTocStyle()
+ },
+ stop: function (e) {
+ lastEditorWidth = ui.area.edit.width()
// workaround that scroll event bindings
- preventSyncScrollToView = 2;
- preventSyncScrollToEdit = true;
- editor.setOption('viewportMargin', viewportMargin);
- if (editorHasFocus()) {
- windowResizeInner(function () {
- ui.area.codemirrorScroll.scroll();
- });
- } else {
- windowResizeInner(function () {
- ui.area.view.scroll();
- });
- }
- checkEditorScrollbar();
- }
- });
- ui.area.resize.handle = $('.ui-resizable-handle');
- }
- if (!ui.area.resize.syncToggle.length) {
- ui.area.resize.syncToggle = $('');
- ui.area.resize.syncToggle.hover(function () {
- previousFocusOnEditor = editorHasFocus();
- }, function () {
- previousFocusOnEditor = null;
- });
- ui.area.resize.syncToggle.click(function () {
- syncscroll = !syncscroll;
- checkSyncToggle();
- });
- ui.area.resize.handle.append(ui.area.resize.syncToggle);
- ui.area.resize.syncToggle.hide();
- ui.area.resize.handle.hover(function () {
- ui.area.resize.syncToggle.stop(true, true).delay(200).fadeIn(100);
- }, function () {
- ui.area.resize.syncToggle.stop(true, true).delay(300).fadeOut(300);
- });
- }
+ window.preventSyncScrollToView = 2
+ window.preventSyncScrollToEdit = true
+ editor.setOption('viewportMargin', viewportMargin)
+ if (editorHasFocus()) {
+ windowResizeInner(function () {
+ ui.area.codemirrorScroll.scroll()
+ })
+ } else {
+ windowResizeInner(function () {
+ ui.area.view.scroll()
+ })
+ }
+ checkEditorScrollbar()
+ }
+ })
+ ui.area.resize.handle = $('.ui-resizable-handle')
+ }
+ if (!ui.area.resize.syncToggle.length) {
+ ui.area.resize.syncToggle = $('')
+ ui.area.resize.syncToggle.hover(function () {
+ previousFocusOnEditor = editorHasFocus()
+ }, function () {
+ previousFocusOnEditor = null
+ })
+ ui.area.resize.syncToggle.click(function () {
+ window.syncscroll = !window.syncscroll
+ checkSyncToggle()
+ })
+ ui.area.resize.handle.append(ui.area.resize.syncToggle)
+ ui.area.resize.syncToggle.hide()
+ ui.area.resize.handle.hover(function () {
+ ui.area.resize.syncToggle.stop(true, true).delay(200).fadeIn(100)
+ }, function () {
+ ui.area.resize.syncToggle.stop(true, true).delay(300).fadeOut(300)
+ })
+ }
}
-function checkSyncToggle() {
- if (syncscroll) {
- if (previousFocusOnEditor) {
- preventSyncScrollToView = false;
- syncScrollToView();
- } else {
- preventSyncScrollToEdit = false;
- syncScrollToEdit();
- }
- ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link');
+function checkSyncToggle () {
+ if (window.syncscroll) {
+ if (previousFocusOnEditor) {
+ window.preventSyncScrollToView = false
+ syncScrollToView()
} else {
- ui.area.resize.syncToggle.find('i').removeClass('fa-link').addClass('fa-unlink');
+ window.preventSyncScrollToEdit = false
+ syncScrollToEdit()
}
+ ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link')
+ } else {
+ ui.area.resize.syncToggle.find('i').removeClass('fa-link').addClass('fa-unlink')
+ }
}
var checkEditorScrollbar = _.debounce(function () {
- editor.operation(checkEditorScrollbarInner);
-}, 50);
+ editor.operation(checkEditorScrollbarInner)
+}, 50)
-function checkEditorScrollbarInner() {
+function checkEditorScrollbarInner () {
// workaround simple scroll bar knob
// will get wrong position when editor height changed
- var scrollInfo = editor.getScrollInfo();
- editor.scrollTo(null, scrollInfo.top - 1);
- editor.scrollTo(null, scrollInfo.top);
+ var scrollInfo = editor.getScrollInfo()
+ editor.scrollTo(null, scrollInfo.top - 1)
+ editor.scrollTo(null, scrollInfo.top)
}
-function checkTocStyle() {
- //toc right
- var paddingRight = parseFloat(ui.area.markdown.css('padding-right'));
- var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight));
- ui.toc.toc.css('right', right + 'px');
- //affix toc left
- var newbool;
- var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2;
- //for ipad or wider device
- if (rightMargin >= 133) {
- newbool = true;
- var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2;
- var left = ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - affixLeftMargin;
- ui.toc.affix.css('left', left + 'px');
- ui.toc.affix.css('width', rightMargin + 'px');
- } else {
- newbool = false;
- }
- //toc scrollspy
- ui.toc.toc.removeClass('scrollspy-body, scrollspy-view');
- ui.toc.affix.removeClass('scrollspy-body, scrollspy-view');
- if (currentMode == modeType.both) {
- ui.toc.toc.addClass('scrollspy-view');
- ui.toc.affix.addClass('scrollspy-view');
- } else if (currentMode != modeType.both && !newbool) {
- ui.toc.toc.addClass('scrollspy-body');
- ui.toc.affix.addClass('scrollspy-body');
- } else {
- ui.toc.toc.addClass('scrollspy-view');
- ui.toc.affix.addClass('scrollspy-body');
- }
- if (newbool != enoughForAffixToc) {
- enoughForAffixToc = newbool;
- generateScrollspy();
- }
+function checkTocStyle () {
+ // toc right
+ var paddingRight = parseFloat(ui.area.markdown.css('padding-right'))
+ var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight))
+ ui.toc.toc.css('right', right + 'px')
+ // affix toc left
+ var newbool
+ var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2
+ // for ipad or wider device
+ if (rightMargin >= 133) {
+ newbool = true
+ var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2
+ var left = ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - affixLeftMargin
+ ui.toc.affix.css('left', left + 'px')
+ ui.toc.affix.css('width', rightMargin + 'px')
+ } else {
+ newbool = false
+ }
+ // toc scrollspy
+ ui.toc.toc.removeClass('scrollspy-body, scrollspy-view')
+ ui.toc.affix.removeClass('scrollspy-body, scrollspy-view')
+ if (window.currentMode === modeType.both) {
+ ui.toc.toc.addClass('scrollspy-view')
+ ui.toc.affix.addClass('scrollspy-view')
+ } else if (window.currentMode !== modeType.both && !newbool) {
+ ui.toc.toc.addClass('scrollspy-body')
+ ui.toc.affix.addClass('scrollspy-body')
+ } else {
+ ui.toc.toc.addClass('scrollspy-view')
+ ui.toc.affix.addClass('scrollspy-body')
+ }
+ if (newbool !== enoughForAffixToc) {
+ enoughForAffixToc = newbool
+ generateScrollspy()
+ }
}
-function showStatus(type, num) {
- currentStatus = type;
- var shortStatus = ui.toolbar.shortStatus;
- var status = ui.toolbar.status;
- var label = $('');
- var fa = $('');
- var msg = "";
- var shortMsg = "";
+function showStatus (type, num) {
+ window.currentStatus = type
+ var shortStatus = ui.toolbar.shortStatus
+ var status = ui.toolbar.status
+ var label = $('')
+ var fa = $('')
+ var msg = ''
+ var shortMsg = ''
- shortStatus.html("");
- status.html("");
+ shortStatus.html('')
+ status.html('')
- switch (currentStatus) {
- case statusType.connected:
- label.addClass(statusType.connected.label);
- fa.addClass(statusType.connected.fa);
- msg = statusType.connected.msg;
- break;
- case statusType.online:
- label.addClass(statusType.online.label);
- fa.addClass(statusType.online.fa);
- shortMsg = num;
- msg = num + " " + statusType.online.msg;
- break;
- case statusType.offline:
- label.addClass(statusType.offline.label);
- fa.addClass(statusType.offline.fa);
- msg = statusType.offline.msg;
- break;
- }
+ switch (window.currentStatus) {
+ case statusType.connected:
+ label.addClass(statusType.connected.label)
+ fa.addClass(statusType.connected.fa)
+ msg = statusType.connected.msg
+ break
+ case statusType.online:
+ label.addClass(statusType.online.label)
+ fa.addClass(statusType.online.fa)
+ shortMsg = num
+ msg = num + ' ' + statusType.online.msg
+ break
+ case statusType.offline:
+ label.addClass(statusType.offline.label)
+ fa.addClass(statusType.offline.fa)
+ msg = statusType.offline.msg
+ break
+ }
- label.append(fa);
- var shortLabel = label.clone();
+ label.append(fa)
+ var shortLabel = label.clone()
- shortLabel.append(" " + shortMsg);
- shortStatus.append(shortLabel);
+ shortLabel.append(' ' + shortMsg)
+ shortStatus.append(shortLabel)
- label.append(" " + msg);
- status.append(label);
+ label.append(' ' + msg)
+ status.append(label)
}
-function toggleMode() {
- switch (currentMode) {
- case modeType.edit:
- changeMode(modeType.view);
- break;
- case modeType.view:
- changeMode(modeType.edit);
- break;
- case modeType.both:
- changeMode(modeType.view);
- break;
- }
+function toggleMode () {
+ switch (window.currentMode) {
+ case modeType.edit:
+ changeMode(modeType.view)
+ break
+ case modeType.view:
+ changeMode(modeType.edit)
+ break
+ case modeType.both:
+ changeMode(modeType.view)
+ break
+ }
}
-var lastMode = null;
+var lastMode = null
-function changeMode(type) {
+function changeMode (type) {
// lock navbar to prevent it hide after changeMode
- lockNavbar();
- saveInfo();
- if (type) {
- lastMode = currentMode;
- currentMode = type;
- }
- var responsiveClass = "col-lg-6 col-md-6 col-sm-6";
- var scrollClass = "ui-scrollable";
- ui.area.codemirror.removeClass(scrollClass);
- ui.area.edit.removeClass(responsiveClass);
- ui.area.view.removeClass(scrollClass);
- ui.area.view.removeClass(responsiveClass);
- switch (currentMode) {
- case modeType.edit:
- ui.area.edit.show();
- ui.area.view.hide();
- if (!editShown) {
- editor.refresh();
- editShown = true;
- }
- break;
- case modeType.view:
- ui.area.edit.hide();
- ui.area.view.show();
- break;
- case modeType.both:
- ui.area.codemirror.addClass(scrollClass);
- ui.area.edit.addClass(responsiveClass).show();
- ui.area.view.addClass(scrollClass);
- ui.area.view.show();
- break;
- }
+ lockNavbar()
+ saveInfo()
+ if (type) {
+ lastMode = window.currentMode
+ window.currentMode = type
+ }
+ var responsiveClass = 'col-lg-6 col-md-6 col-sm-6'
+ var scrollClass = 'ui-scrollable'
+ ui.area.codemirror.removeClass(scrollClass)
+ ui.area.edit.removeClass(responsiveClass)
+ ui.area.view.removeClass(scrollClass)
+ ui.area.view.removeClass(responsiveClass)
+ switch (window.currentMode) {
+ case modeType.edit:
+ ui.area.edit.show()
+ ui.area.view.hide()
+ if (!window.editShown) {
+ editor.refresh()
+ window.editShown = true
+ }
+ break
+ case modeType.view:
+ ui.area.edit.hide()
+ ui.area.view.show()
+ break
+ case modeType.both:
+ ui.area.codemirror.addClass(scrollClass)
+ ui.area.edit.addClass(responsiveClass).show()
+ ui.area.view.addClass(scrollClass)
+ ui.area.view.show()
+ break
+ }
// save mode to url
- if (history.replaceState && loaded) history.replaceState(null, "", serverurl + '/' + noteid + '?' + currentMode.name);
- if (currentMode == modeType.view) {
- editor.getInputField().blur();
- }
- if (currentMode == modeType.edit || currentMode == modeType.both) {
- ui.toolbar.uploadImage.fadeIn();
- //add and update status bar
- if (!statusBar) {
- addStatusBar();
- updateStatusBar();
- }
- //work around foldGutter might not init properly
- editor.setOption('foldGutter', false);
- editor.setOption('foldGutter', true);
- } else {
- ui.toolbar.uploadImage.fadeOut();
- }
- if (currentMode != modeType.edit) {
- $(document.body).css('background-color', 'white');
- updateView();
- } else {
- $(document.body).css('background-color', ui.area.codemirror.css('background-color'));
- }
- //check resizable editor style
- if (currentMode == modeType.both) {
- if (lastEditorWidth > 0)
- ui.area.edit.css('width', lastEditorWidth + 'px');
- else
- ui.area.edit.css('width', '');
- ui.area.resize.handle.show();
- } else {
- ui.area.edit.css('width', '');
- ui.area.resize.handle.hide();
+ if (history.replaceState && window.loaded) history.replaceState(null, '', serverurl + '/' + noteid + '?' + window.currentMode.name)
+ if (window.currentMode === modeType.view) {
+ editor.getInputField().blur()
+ }
+ if (window.currentMode === modeType.edit || window.currentMode === modeType.both) {
+ ui.toolbar.uploadImage.fadeIn()
+ // add and update status bar
+ if (!statusBar) {
+ addStatusBar()
+ updateStatusBar()
}
+ // work around foldGutter might not init properly
+ editor.setOption('foldGutter', false)
+ editor.setOption('foldGutter', true)
+ } else {
+ ui.toolbar.uploadImage.fadeOut()
+ }
+ if (window.currentMode !== modeType.edit) {
+ $(document.body).css('background-color', 'white')
+ updateView()
+ } else {
+ $(document.body).css('background-color', ui.area.codemirror.css('background-color'))
+ }
+ // check resizable editor style
+ if (window.currentMode === modeType.both) {
+ if (lastEditorWidth > 0) { ui.area.edit.css('width', lastEditorWidth + 'px') } else { ui.area.edit.css('width', '') }
+ ui.area.resize.handle.show()
+ } else {
+ ui.area.edit.css('width', '')
+ ui.area.resize.handle.hide()
+ }
- windowResizeInner();
+ windowResizeInner()
- restoreInfo();
+ restoreInfo()
- if (lastMode == modeType.view && currentMode == modeType.both) {
- preventSyncScrollToView = 2;
- syncScrollToEdit(null, true);
- }
+ if (lastMode === modeType.view && window.currentMode === modeType.both) {
+ window.preventSyncScrollToView = 2
+ syncScrollToEdit(null, true)
+ }
- if (lastMode == modeType.edit && currentMode == modeType.both) {
- preventSyncScrollToEdit = 2;
- syncScrollToView(null, true);
- }
+ if (lastMode === modeType.edit && window.currentMode === modeType.both) {
+ window.preventSyncScrollToEdit = 2
+ syncScrollToView(null, true)
+ }
- if (lastMode == modeType.both && currentMode != modeType.both) {
- preventSyncScrollToView = false;
- preventSyncScrollToEdit = false;
- }
+ if (lastMode === modeType.both && window.currentMode !== modeType.both) {
+ window.preventSyncScrollToView = false
+ window.preventSyncScrollToEdit = false
+ }
- if (lastMode != modeType.edit && currentMode == modeType.edit) {
- editor.refresh();
- }
+ if (lastMode !== modeType.edit && window.currentMode === modeType.edit) {
+ editor.refresh()
+ }
- $(document.body).scrollspy('refresh');
- ui.area.view.scrollspy('refresh');
+ $(document.body).scrollspy('refresh')
+ ui.area.view.scrollspy('refresh')
- ui.toolbar.both.removeClass("active");
- ui.toolbar.edit.removeClass("active");
- ui.toolbar.view.removeClass("active");
- var modeIcon = ui.toolbar.mode.find('i');
- modeIcon.removeClass('fa-pencil').removeClass('fa-eye');
- if (ui.area.edit.is(":visible") && ui.area.view.is(":visible")) { //both
- ui.toolbar.both.addClass("active");
- modeIcon.addClass('fa-eye');
- } else if (ui.area.edit.is(":visible")) { //edit
- ui.toolbar.edit.addClass("active");
- modeIcon.addClass('fa-eye');
- } else if (ui.area.view.is(":visible")) { //view
- ui.toolbar.view.addClass("active");
- modeIcon.addClass('fa-pencil');
- }
- unlockNavbar();
+ ui.toolbar.both.removeClass('active')
+ ui.toolbar.edit.removeClass('active')
+ ui.toolbar.view.removeClass('active')
+ var modeIcon = ui.toolbar.mode.find('i')
+ modeIcon.removeClass('fa-pencil').removeClass('fa-eye')
+ if (ui.area.edit.is(':visible') && ui.area.view.is(':visible')) { // both
+ ui.toolbar.both.addClass('active')
+ modeIcon.addClass('fa-eye')
+ } else if (ui.area.edit.is(':visible')) { // edit
+ ui.toolbar.edit.addClass('active')
+ modeIcon.addClass('fa-eye')
+ } else if (ui.area.view.is(':visible')) { // view
+ ui.toolbar.view.addClass('active')
+ modeIcon.addClass('fa-pencil')
+ }
+ unlockNavbar()
}
-function lockNavbar() {
- $('.navbar').addClass('locked');
+function lockNavbar () {
+ $('.navbar').addClass('locked')
}
var unlockNavbar = _.debounce(function () {
- $('.navbar').removeClass('locked');
-}, 200);
+ $('.navbar').removeClass('locked')
+}, 200)
-function closestIndex(arr, closestTo) {
- var closest = Math.max.apply(null, arr); //Get the highest number in arr in case it match nothing.
- var index = 0;
- for (var i = 0; i < arr.length; i++) { //Loop the array
- if (arr[i] >= closestTo && arr[i] < closest) {
- closest = arr[i]; //Check if it's higher than your number, but lower than your closest value
- index = i;
- }
- }
- return index; // return the value
-}
-
-function showMessageModal(title, header, href, text, success) {
- var modal = $('.message-modal');
- modal.find('.modal-title').html(title);
- modal.find('.modal-body h5').html(header);
- if (href)
- modal.find('.modal-body a').attr('href', href).text(text);
- else
- modal.find('.modal-body a').removeAttr('href').text(text);
- modal.find('.modal-footer button').removeClass('btn-default btn-success btn-danger')
- if (success)
- modal.find('.modal-footer button').addClass('btn-success');
- else
- modal.find('.modal-footer button').addClass('btn-danger');
- modal.modal('show');
+function showMessageModal (title, header, href, text, success) {
+ var modal = $('.message-modal')
+ modal.find('.modal-title').html(title)
+ modal.find('.modal-body h5').html(header)
+ if (href) { modal.find('.modal-body a').attr('href', href).text(text) } else { modal.find('.modal-body a').removeAttr('href').text(text) }
+ modal.find('.modal-footer button').removeClass('btn-default btn-success btn-danger')
+ if (success) { modal.find('.modal-footer button').addClass('btn-success') } else { modal.find('.modal-footer button').addClass('btn-danger') }
+ modal.modal('show')
}
// check if dropbox app key is set and load scripts
if (DROPBOX_APP_KEY) {
- $('' );
-
- var leadingWs = text.match( /^\n?(\s*)/ )[1].length,
- leadingTabs = text.match( /^\n?(\t*)/ )[1].length;
-
- if( leadingTabs > 0 ) {
- text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}','g'), '\n' );
- }
- else if( leadingWs > 1 ) {
- text = text.replace( new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n' );
- }
-
- return text;
-
- }
-
- /**
- * Given a markdown slide section element, this will
- * return all arguments that aren't related to markdown
- * parsing. Used to forward any other user-defined arguments
- * to the output markdown slide.
- */
- function getForwardedAttributes( section ) {
-
- var attributes = section.attributes;
- var result = [];
-
- for( var i = 0, len = attributes.length; i < len; i++ ) {
- var name = attributes[i].name,
- value = attributes[i].value;
-
- // disregard attributes that are used for markdown loading/parsing
- if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue;
-
- if( value ) {
- result.push( name + '="' + value + '"' );
- }
- else {
- result.push( name );
- }
- }
-
- return result.join( ' ' );
-
- }
-
- /**
- * Inspects the given options and fills out default
- * values for what's not defined.
- */
- function getSlidifyOptions( options ) {
-
- options = options || {};
- options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR;
- options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR;
- options.attributes = options.attributes || '';
-
- return options;
-
- }
-
- /**
- * Helper function for constructing a markdown slide.
- */
- function createMarkdownSlide( content, options ) {
-
- options = getSlidifyOptions( options );
-
- var notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) );
-
- if( notesMatch.length === 2 ) {
- content = notesMatch[0] + '';
- }
-
- // prevent script end tags in the content from interfering
- // with parsing
- content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER );
-
- return '';
-
- }
-
- /**
- * Parses a data string into multiple slides based
- * on the passed in separator arguments.
- */
- function slidify( markdown, options ) {
-
- options = getSlidifyOptions( options );
-
- var separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ),
- horizontalSeparatorRegex = new RegExp( options.separator );
-
- var matches,
- lastIndex = 0,
- isHorizontal,
- wasHorizontal = true,
- content,
- sectionStack = [];
-
- // iterate until all blocks between separators are stacked up
- while( matches = separatorRegex.exec( markdown ) ) {
- notes = null;
-
- // determine direction (horizontal by default)
- isHorizontal = horizontalSeparatorRegex.test( matches[0] );
-
- if( !isHorizontal && wasHorizontal ) {
- // create vertical stack
- sectionStack.push( [] );
- }
-
- // pluck slide content from markdown input
- content = markdown.substring( lastIndex, matches.index );
-
- if( isHorizontal && wasHorizontal ) {
- // add to horizontal stack
- sectionStack.push( content );
- }
- else {
- // add to vertical stack
- sectionStack[sectionStack.length-1].push( content );
- }
-
- lastIndex = separatorRegex.lastIndex;
- wasHorizontal = isHorizontal;
- }
-
- // add the remaining slide
- ( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) );
-
- var markdownSections = '';
-
- // flatten the hierarchical stack, and insert Remember that you need to serve the presentation HTML from a HTTP server.
' + - 'Remember that you need to serve the presentation HTML from a HTTP server.
' + + '${highlighted}
\n`;
- }
+ if (tokens[idx].map && tokens[idx].level === 0) {
+ const startline = tokens[idx].map[0] + 1
+ const endline = tokens[idx].map[1]
+ return `${highlighted}
\n`
+ }
- return `${highlighted}
\n`;
-};
+ return `${highlighted}
\n`
+}
md.renderer.rules.code_block = (tokens, idx, options, env, self) => {
- if (tokens[idx].map && tokens[idx].level === 0) {
- const startline = tokens[idx].map[0] + 1;
- const endline = tokens[idx].map[1];
- return `${md.utils.escapeHtml(tokens[idx].content)}
\n`;
- }
- return `${md.utils.escapeHtml(tokens[idx].content)}
\n`;
-};
-function renderContainer(tokens, idx, options, env, self) {
- tokens[idx].attrJoin('role', 'alert');
- tokens[idx].attrJoin('class', 'alert');
- tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`);
- addPart(tokens, idx);
- return self.renderToken(...arguments);
+ if (tokens[idx].map && tokens[idx].level === 0) {
+ const startline = tokens[idx].map[0] + 1
+ const endline = tokens[idx].map[1]
+ return `${md.utils.escapeHtml(tokens[idx].content)}
\n`
+ }
+ return `${md.utils.escapeHtml(tokens[idx].content)}
\n`
+}
+function renderContainer (tokens, idx, options, env, self) {
+ tokens[idx].attrJoin('role', 'alert')
+ tokens[idx].attrJoin('class', 'alert')
+ tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`)
+ addPart(tokens, idx)
+ return self.renderToken(...arguments)
}
-md.use(markdownitContainer, 'success', { render: renderContainer });
-md.use(markdownitContainer, 'info', { render: renderContainer });
-md.use(markdownitContainer, 'warning', { render: renderContainer });
-md.use(markdownitContainer, 'danger', { render: renderContainer });
+md.use(markdownitContainer, 'success', { render: renderContainer })
+md.use(markdownitContainer, 'info', { render: renderContainer })
+md.use(markdownitContainer, 'warning', { render: renderContainer })
+md.use(markdownitContainer, 'danger', { render: renderContainer })
// FIXME: expose syncscroll to window
-window.syncscroll = true;
+window.syncscroll = true
-window.preventSyncScrollToEdit = false;
-window.preventSyncScrollToView = false;
+window.preventSyncScrollToEdit = false
+window.preventSyncScrollToView = false
-const editScrollThrottle = 5;
-const viewScrollThrottle = 5;
-const buildMapThrottle = 100;
+const editScrollThrottle = 5
+const viewScrollThrottle = 5
+const buildMapThrottle = 100
-let viewScrolling = false;
-let editScrolling = false;
+let viewScrolling = false
+let editScrolling = false
-let editArea = null;
-let viewArea = null;
-let markdownArea = null;
+let editArea = null
+let viewArea = null
+let markdownArea = null
-export function setupSyncAreas(edit, view, markdown) {
- editArea = edit;
- viewArea = view;
- markdownArea = markdown;
- editArea.on('scroll', _.throttle(syncScrollToView, editScrollThrottle));
- viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle));
+export function setupSyncAreas (edit, view, markdown) {
+ editArea = edit
+ viewArea = view
+ markdownArea = markdown
+ editArea.on('scroll', _.throttle(syncScrollToView, editScrollThrottle))
+ viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle))
}
-let scrollMap, lineHeightMap, viewTop, viewBottom;
+let scrollMap, lineHeightMap, viewTop, viewBottom
-export function clearMap() {
- scrollMap = null;
- lineHeightMap = null;
- viewTop = null;
- viewBottom = null;
+export function clearMap () {
+ scrollMap = null
+ lineHeightMap = null
+ viewTop = null
+ viewBottom = null
}
-window.viewAjaxCallback = clearMap;
+window.viewAjaxCallback = clearMap
-const buildMap = _.throttle(buildMapInner, buildMapThrottle);
+const buildMap = _.throttle(buildMapInner, buildMapThrottle)
// Build offsets for each line (lines can be wrapped)
// That's a bit dirty to process each line everytime, but ok for demo.
// Optimizations are required only for big texts.
-function buildMapInner(callback) {
- if (!viewArea || !markdownArea) return;
- let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap;
+function buildMapInner (callback) {
+ if (!viewArea || !markdownArea) return
+ let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap
- offset = viewArea.scrollTop() - viewArea.offset().top;
- _scrollMap = [];
- nonEmptyList = [];
- _lineHeightMap = [];
- viewTop = 0;
- viewBottom = viewArea[0].scrollHeight - viewArea.height();
+ offset = viewArea.scrollTop() - viewArea.offset().top
+ _scrollMap = []
+ nonEmptyList = []
+ _lineHeightMap = []
+ viewTop = 0
+ viewBottom = viewArea[0].scrollHeight - viewArea.height()
- acc = 0;
- const lines = editor.getValue().split('\n');
- const lineHeight = editor.defaultTextHeight();
- for (i = 0; i < lines.length; i++) {
- const str = lines[i];
+ acc = 0
+ const lines = window.editor.getValue().split('\n')
+ const lineHeight = window.editor.defaultTextHeight()
+ for (i = 0; i < lines.length; i++) {
+ const str = lines[i]
- _lineHeightMap.push(acc);
+ _lineHeightMap.push(acc)
- if (str.length === 0) {
- acc++;
- continue;
- }
-
- const h = editor.heightAtLine(i + 1) - editor.heightAtLine(i);
- acc += Math.round(h / lineHeight);
- }
- _lineHeightMap.push(acc);
- linesCount = acc;
-
- for (i = 0; i < linesCount; i++) {
- _scrollMap.push(-1);
+ if (str.length === 0) {
+ acc++
+ continue
}
- nonEmptyList.push(0);
+ const h = window.editor.heightAtLine(i + 1) - window.editor.heightAtLine(i)
+ acc += Math.round(h / lineHeight)
+ }
+ _lineHeightMap.push(acc)
+ linesCount = acc
+
+ for (i = 0; i < linesCount; i++) {
+ _scrollMap.push(-1)
+ }
+
+ nonEmptyList.push(0)
// make the first line go top
- _scrollMap[0] = viewTop;
+ _scrollMap[0] = viewTop
- const parts = markdownArea.find('.part').toArray();
- for (i = 0; i < parts.length; i++) {
- const $el = $(parts[i]);
- let t = $el.attr('data-startline') - 1;
- if (t === '') {
- return;
- }
- t = _lineHeightMap[t];
- if (t !== 0 && t !== nonEmptyList[nonEmptyList.length - 1]) {
- nonEmptyList.push(t);
- }
- _scrollMap[t] = Math.round($el.offset().top + offset - 10);
+ const parts = markdownArea.find('.part').toArray()
+ for (i = 0; i < parts.length; i++) {
+ const $el = $(parts[i])
+ let t = $el.attr('data-startline') - 1
+ if (t === '') {
+ return
+ }
+ t = _lineHeightMap[t]
+ if (t !== 0 && t !== nonEmptyList[nonEmptyList.length - 1]) {
+ nonEmptyList.push(t)
+ }
+ _scrollMap[t] = Math.round($el.offset().top + offset - 10)
+ }
+
+ nonEmptyList.push(linesCount)
+ _scrollMap[linesCount] = viewArea[0].scrollHeight
+
+ pos = 0
+ for (i = 1; i < linesCount; i++) {
+ if (_scrollMap[i] !== -1) {
+ pos++
+ continue
}
- nonEmptyList.push(linesCount);
- _scrollMap[linesCount] = viewArea[0].scrollHeight;
+ a = nonEmptyList[pos]
+ b = nonEmptyList[pos + 1]
+ _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a))
+ }
- pos = 0;
- for (i = 1; i < linesCount; i++) {
- if (_scrollMap[i] !== -1) {
- pos++;
- continue;
- }
+ _scrollMap[0] = 0
- a = nonEmptyList[pos];
- b = nonEmptyList[pos + 1];
- _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a));
- }
+ scrollMap = _scrollMap
+ lineHeightMap = _lineHeightMap
- _scrollMap[0] = 0;
-
- scrollMap = _scrollMap;
- lineHeightMap = _lineHeightMap;
-
- if (loaded && callback) callback();
+ if (window.loaded && callback) callback()
}
// sync view scroll progress to edit
-let viewScrollingTimer = null;
+let viewScrollingTimer = null
-export function syncScrollToEdit(event, preventAnimate) {
- if (currentMode != modeType.both || !syncscroll || !editArea) return;
- if (preventSyncScrollToEdit) {
- if (typeof preventSyncScrollToEdit === 'number') {
- preventSyncScrollToEdit--;
- } else {
- preventSyncScrollToEdit = false;
- }
- return;
- }
- if (!scrollMap || !lineHeightMap) {
- buildMap(() => {
- syncScrollToEdit(event, preventAnimate);
- });
- return;
- }
- if (editScrolling) return;
-
- const scrollTop = viewArea[0].scrollTop;
- let lineIndex = 0;
- for (var i = 0, l = scrollMap.length; i < l; i++) {
- if (scrollMap[i] > scrollTop) {
- break;
- } else {
- lineIndex = i;
- }
- }
- let lineNo = 0;
- let lineDiff = 0;
- for (var i = 0, l = lineHeightMap.length; i < l; i++) {
- if (lineHeightMap[i] > lineIndex) {
- break;
- } else {
- lineNo = lineHeightMap[i];
- lineDiff = lineHeightMap[i + 1] - lineNo;
- }
- }
-
- let posTo = 0;
- let topDiffPercent = 0;
- let posToNextDiff = 0;
- const scrollInfo = editor.getScrollInfo();
- const textHeight = editor.defaultTextHeight();
- const preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight;
- const preLastLineNo = Math.round(preLastLineHeight / textHeight);
- const preLastLinePos = scrollMap[preLastLineNo];
-
- if (scrollInfo.height > scrollInfo.clientHeight && scrollTop >= preLastLinePos) {
- posTo = preLastLineHeight;
- topDiffPercent = (scrollTop - preLastLinePos) / (viewBottom - preLastLinePos);
- posToNextDiff = textHeight * topDiffPercent;
- posTo += Math.ceil(posToNextDiff);
+export function syncScrollToEdit (event, preventAnimate) {
+ if (window.currentMode !== window.modeType.both || !window.syncscroll || !editArea) return
+ if (window.preventSyncScrollToEdit) {
+ if (typeof window.preventSyncScrollToEdit === 'number') {
+ window.preventSyncScrollToEdit--
} else {
- posTo = lineNo * textHeight;
- topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo]);
- posToNextDiff = textHeight * lineDiff * topDiffPercent;
- posTo += Math.ceil(posToNextDiff);
+ window.preventSyncScrollToEdit = false
}
+ return
+ }
+ if (!scrollMap || !lineHeightMap) {
+ buildMap(() => {
+ syncScrollToEdit(event, preventAnimate)
+ })
+ return
+ }
+ if (editScrolling) return
- if (preventAnimate) {
- editArea.scrollTop(posTo);
+ const scrollTop = viewArea[0].scrollTop
+ let lineIndex = 0
+ for (let i = 0, l = scrollMap.length; i < l; i++) {
+ if (scrollMap[i] > scrollTop) {
+ break
} else {
- const posDiff = Math.abs(scrollInfo.top - posTo);
- var duration = posDiff / 50;
- duration = duration >= 100 ? duration : 100;
- editArea.stop(true, true).animate({
- scrollTop: posTo
- }, duration, "linear");
+ lineIndex = i
}
+ }
+ let lineNo = 0
+ let lineDiff = 0
+ for (let i = 0, l = lineHeightMap.length; i < l; i++) {
+ if (lineHeightMap[i] > lineIndex) {
+ break
+ } else {
+ lineNo = lineHeightMap[i]
+ lineDiff = lineHeightMap[i + 1] - lineNo
+ }
+ }
- viewScrolling = true;
- clearTimeout(viewScrollingTimer);
- viewScrollingTimer = setTimeout(viewScrollingTimeoutInner, duration * 1.5);
+ let posTo = 0
+ let topDiffPercent = 0
+ let posToNextDiff = 0
+ const scrollInfo = window.editor.getScrollInfo()
+ const textHeight = window.editor.defaultTextHeight()
+ const preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight
+ const preLastLineNo = Math.round(preLastLineHeight / textHeight)
+ const preLastLinePos = scrollMap[preLastLineNo]
+
+ if (scrollInfo.height > scrollInfo.clientHeight && scrollTop >= preLastLinePos) {
+ posTo = preLastLineHeight
+ topDiffPercent = (scrollTop - preLastLinePos) / (viewBottom - preLastLinePos)
+ posToNextDiff = textHeight * topDiffPercent
+ posTo += Math.ceil(posToNextDiff)
+ } else {
+ posTo = lineNo * textHeight
+ topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo])
+ posToNextDiff = textHeight * lineDiff * topDiffPercent
+ posTo += Math.ceil(posToNextDiff)
+ }
+
+ if (preventAnimate) {
+ editArea.scrollTop(posTo)
+ } else {
+ const posDiff = Math.abs(scrollInfo.top - posTo)
+ var duration = posDiff / 50
+ duration = duration >= 100 ? duration : 100
+ editArea.stop(true, true).animate({
+ scrollTop: posTo
+ }, duration, 'linear')
+ }
+
+ viewScrolling = true
+ clearTimeout(viewScrollingTimer)
+ viewScrollingTimer = setTimeout(viewScrollingTimeoutInner, duration * 1.5)
}
-function viewScrollingTimeoutInner() {
- viewScrolling = false;
+function viewScrollingTimeoutInner () {
+ viewScrolling = false
}
// sync edit scroll progress to view
-let editScrollingTimer = null;
+let editScrollingTimer = null
-export function syncScrollToView(event, preventAnimate) {
- if (currentMode != modeType.both || !syncscroll || !viewArea) return;
- if (preventSyncScrollToView) {
- if (typeof preventSyncScrollToView === 'number') {
- preventSyncScrollToView--;
- } else {
- preventSyncScrollToView = false;
- }
- return;
+export function syncScrollToView (event, preventAnimate) {
+ if (window.currentMode !== window.modeType.both || !window.syncscroll || !viewArea) return
+ if (window.preventSyncScrollToView) {
+ if (typeof preventSyncScrollToView === 'number') {
+ window.preventSyncScrollToView--
+ } else {
+ window.preventSyncScrollToView = false
}
- if (!scrollMap || !lineHeightMap) {
- buildMap(() => {
- syncScrollToView(event, preventAnimate);
- });
- return;
- }
- if (viewScrolling) return;
+ return
+ }
+ if (!scrollMap || !lineHeightMap) {
+ buildMap(() => {
+ syncScrollToView(event, preventAnimate)
+ })
+ return
+ }
+ if (viewScrolling) return
- let lineNo, posTo;
- let topDiffPercent, posToNextDiff;
- const scrollInfo = editor.getScrollInfo();
- const textHeight = editor.defaultTextHeight();
- lineNo = Math.floor(scrollInfo.top / textHeight);
+ let lineNo, posTo
+ let topDiffPercent, posToNextDiff
+ const scrollInfo = window.editor.getScrollInfo()
+ const textHeight = window.editor.defaultTextHeight()
+ lineNo = Math.floor(scrollInfo.top / textHeight)
// if reach the last line, will start lerp to the bottom
- const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight);
- if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) {
- topDiffPercent = diffToBottom / textHeight;
- posTo = scrollMap[lineNo + 1];
- posToNextDiff = (viewBottom - posTo) * topDiffPercent;
- posTo += Math.floor(posToNextDiff);
- } else {
- topDiffPercent = (scrollInfo.top % textHeight) / textHeight;
- posTo = scrollMap[lineNo];
- posToNextDiff = (scrollMap[lineNo + 1] - posTo) * topDiffPercent;
- posTo += Math.floor(posToNextDiff);
- }
+ const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight)
+ if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) {
+ topDiffPercent = diffToBottom / textHeight
+ posTo = scrollMap[lineNo + 1]
+ posToNextDiff = (viewBottom - posTo) * topDiffPercent
+ posTo += Math.floor(posToNextDiff)
+ } else {
+ topDiffPercent = (scrollInfo.top % textHeight) / textHeight
+ posTo = scrollMap[lineNo]
+ posToNextDiff = (scrollMap[lineNo + 1] - posTo) * topDiffPercent
+ posTo += Math.floor(posToNextDiff)
+ }
- if (preventAnimate) {
- viewArea.scrollTop(posTo);
- } else {
- const posDiff = Math.abs(viewArea.scrollTop() - posTo);
- var duration = posDiff / 50;
- duration = duration >= 100 ? duration : 100;
- viewArea.stop(true, true).animate({
- scrollTop: posTo
- }, duration, "linear");
- }
+ if (preventAnimate) {
+ viewArea.scrollTop(posTo)
+ } else {
+ const posDiff = Math.abs(viewArea.scrollTop() - posTo)
+ var duration = posDiff / 50
+ duration = duration >= 100 ? duration : 100
+ viewArea.stop(true, true).animate({
+ scrollTop: posTo
+ }, duration, 'linear')
+ }
- editScrolling = true;
- clearTimeout(editScrollingTimer);
- editScrollingTimer = setTimeout(editScrollingTimeoutInner, duration * 1.5);
+ editScrolling = true
+ clearTimeout(editScrollingTimer)
+ editScrollingTimer = setTimeout(editScrollingTimeoutInner, duration * 1.5)
}
-function editScrollingTimeoutInner() {
- editScrolling = false;
+function editScrollingTimeoutInner () {
+ editScrolling = false
}
diff --git a/public/vendor/md-toc.js b/public/vendor/md-toc.js
index 200275a5..f93f7921 100755
--- a/public/vendor/md-toc.js
+++ b/public/vendor/md-toc.js
@@ -1,129 +1,123 @@
+/* eslint-env browser, jquery */
/**
* md-toc.js v1.0.2
* https://github.com/yijian166/md-toc.js
*/
(function (window) {
- function Toc(id, options) {
- this.el = document.getElementById(id);
- if (!this.el) return;
- this.options = options || {};
- this.tocLevel = parseInt(options.level) || 0;
- this.tocClass = options['class'] || 'toc';
- this.ulClass = options['ulClass'];
- this.tocTop = parseInt(options.top) || 0;
- this.elChilds = this.el.children;
- this.process = options['process'];
- if (!this.elChilds.length) return;
- this._init();
+ function Toc (id, options) {
+ this.el = document.getElementById(id)
+ if (!this.el) return
+ this.options = options || {}
+ this.tocLevel = parseInt(options.level) || 0
+ this.tocClass = options['class'] || 'toc'
+ this.ulClass = options['ulClass']
+ this.tocTop = parseInt(options.top) || 0
+ this.elChilds = this.el.children
+ this.process = options['process']
+ if (!this.elChilds.length) return
+ this._init()
+ }
+
+ Toc.prototype._init = function () {
+ this._collectTitleElements()
+ this._createTocContent()
+ this._showToc()
+ }
+
+ Toc.prototype._collectTitleElements = function () {
+ this._elTitlesNames = []
+ this.elTitleElements = []
+ for (var i = 1; i < 7; i++) {
+ if (this.el.getElementsByTagName('h' + i).length) {
+ this._elTitlesNames.push('h' + i)
+ }
}
- Toc.prototype._init = function () {
- this._collectTitleElements();
- this._createTocContent();
- this._showToc();
- };
+ this._elTitlesNames.length = this._elTitlesNames.length > this.tocLevel ? this.tocLevel : this._elTitlesNames.length
- Toc.prototype._collectTitleElements = function () {
- this._elTitlesNames = [],
- this.elTitleElements = [];
- for (var i = 1; i < 7; i++) {
- if (this.el.getElementsByTagName('h' + i).length) {
- this._elTitlesNames.push('h' + i);
+ for (var j = 0; j < this.elChilds.length; j++) {
+ this._elChildName = this.elChilds[j].tagName.toLowerCase()
+ if (this._elTitlesNames.toString().match(this._elChildName)) {
+ this.elTitleElements.push(this.elChilds[j])
+ }
+ }
+ }
+
+ Toc.prototype._createTocContent = function () {
+ this._elTitleElementsLen = this.elTitleElements.length
+ if (!this._elTitleElementsLen) return
+ this.tocContent = ''
+ this._tempLists = []
+
+ for (var i = 0; i < this._elTitleElementsLen; i++) {
+ var j = i + 1
+ this._elTitleElement = this.elTitleElements[i]
+ this._elTitleElementName = this._elTitleElement.tagName
+ this._elTitleElementText = (typeof this.process === 'function' ? this.process(this._elTitleElement) : this._elTitleElement.innerHTML).replace(/<(?:.|\n)*?>/gm, '')
+ var id = this._elTitleElement.getAttribute('id')
+ if (!id) {
+ this._elTitleElement.setAttribute('id', 'tip' + i)
+ id = '#tip' + i
+ } else {
+ id = '#' + id
+ }
+
+ this.tocContent += '