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/package.json b/package.json
index a179d93e..2012dbdf 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "main": "app.js",
   "license": "MIT",
   "scripts": {
-    "test": "npm run-script lint",
+    "test": "node ./node_modules/standard/bin/cmd.js && npm run-script lint",
     "lint": "eslint .",
     "dev": "webpack --config webpack.config.js --progress --colors --watch",
     "build": "webpack --config webpack.production.js --progress --colors",
@@ -165,8 +165,15 @@
     "optimize-css-assets-webpack-plugin": "^1.3.0",
     "script-loader": "^0.7.0",
     "style-loader": "^0.13.1",
+    "standard": "^9.0.1",
     "url-loader": "^0.5.7",
     "webpack": "^1.14.0",
     "webpack-parallel-uglify-plugin": "^0.2.0"
+  },
+  "standard": {
+    "ignore": [
+      "lib/ot",
+      "public/vendor"
+    ]
   }
 }