From 49c7dded4539044d8053dba8d3fe24b97056c0d2 Mon Sep 17 00:00:00 2001 From: Wu Cheng-Han Date: Sun, 17 Jan 2016 09:51:27 -0600 Subject: [PATCH] Added private permission and clean up codes, solved potential race condition in realtime.js --- lib/note.js | 8 +- lib/ot/editor-socketio-server.js | 3 + lib/realtime.js | 204 ++++++++++++++---------- lib/response.js | 258 +++++++++++++++++-------------- public/js/index.js | 22 ++- public/views/body.ejs | 1 + 6 files changed, 297 insertions(+), 199 deletions(-) diff --git a/lib/note.js b/lib/note.js index 671e5383b..cd3816f97 100644 --- a/lib/note.js +++ b/lib/note.js @@ -12,7 +12,7 @@ var db = require("./db.js"); var logger = require("./logger.js"); //permission types -permissionTypes = ["freely", "editable", "locked"]; +permissionTypes = ["freely", "editable", "locked", "private"]; // create a note model var model = mongoose.model('note', { @@ -126,7 +126,11 @@ function findNote(id, callback) { }); } -function newNote(id, permission, callback) { +function newNote(id, owner, callback) { + var permission = "freely"; + if (owner && owner != "null") { + permission = "editable"; + } var note = new model({ id: id, permission: permission, diff --git a/lib/ot/editor-socketio-server.js b/lib/ot/editor-socketio-server.js index 1b2529be5..9e4ddf965 100755 --- a/lib/ot/editor-socketio-server.js +++ b/lib/ot/editor-socketio-server.js @@ -43,6 +43,7 @@ EditorSocketIOServer.prototype.addClient = function (socket) { socket.on('operation', function (revision, operation, selection) { operation = LZString.decompressFromUTF16(operation); operation = JSON.parse(operation); + socket.origin = 'operation'; self.mayWrite(socket, function (mayWrite) { if (!mayWrite) { console.log("User doesn't have the right to edit."); @@ -59,6 +60,7 @@ EditorSocketIOServer.prototype.addClient = function (socket) { self.onGetOperations(socket, base, head); }); socket.on('selection', function (obj) { + socket.origin = 'selection'; self.mayWrite(socket, function (mayWrite) { if (!mayWrite) { console.log("User doesn't have the right to edit."); @@ -104,6 +106,7 @@ EditorSocketIOServer.prototype.onOperation = function (socket, revision, operati 'operation', clientId, revision, wrappedPrime.wrapped.toJSON(), wrappedPrime.meta ); + //set document is dirty this.isDirty = true; } catch (exc) { logger.error(exc); diff --git a/lib/realtime.js b/lib/realtime.js index 484ef12e2..d0db60729 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -90,46 +90,10 @@ var updater = setInterval(function () { if (note.server.isDirty) { if (config.debug) logger.info("updater found dirty note: " + key); - Note.findNote(note.id, function (err, _note) { - if (err || !_note) return callback(err, null); - //mongo update - if (note.lastchangeuser && _note.lastchangeuser != note.lastchangeuser) { - var lastchangeuser = note.lastchangeuser; - var lastchangeuserprofile = null; - User.findUser(lastchangeuser, function (err, user) { - if (err) return callback(err, null); - if (user && user.profile) { - var profile = JSON.parse(user.profile); - if (profile) { - lastchangeuserprofile = { - name: profile.displayName || profile.username, - photo: User.parsePhotoByProfile(profile) - } - note.lastchangeuser = lastchangeuser; - note.lastchangeuserprofile = lastchangeuserprofile; - Note.updateLastChangeUser(_note, lastchangeuser, function (err, result) { - if (err) return callback(err, null); - }); - } - } - }); - } else { - note.lastchangeuser = null; - note.lastchangeuserprofile = null; - Note.updateLastChangeUser(_note, null, function (err, result) { - if (err) return callback(err, null); - }); - } - //postgres update - var body = note.server.document; - var title = Note.getNoteTitle(body); - title = LZString.compressToBase64(title); - body = LZString.compressToBase64(body); - db.saveToDB(key, title, body, function (err, result) { + updaterUpdateMongo(note, function(err, result) { + if (err) return callback(err, null); + updaterUpdatePostgres(note, function(err, result) { if (err) return callback(err, null); - note.server.isDirty = false; - note.updatetime = Date.now(); - emitCheck(note); callback(null, null); }); }); @@ -140,6 +104,56 @@ var updater = setInterval(function () { if (err) return logger.error('updater error', err); }); }, 1000); +function updaterUpdateMongo(note, callback) { + Note.findNote(note.id, function (err, _note) { + if (err || !_note) return callback(err, null); + if (note.lastchangeuser) { + if (_note.lastchangeuser != note.lastchangeuser) { + var lastchangeuser = note.lastchangeuser; + var lastchangeuserprofile = null; + User.findUser(lastchangeuser, function (err, user) { + if (err) return callback(err, null); + if (user && user.profile) { + var profile = JSON.parse(user.profile); + if (profile) { + lastchangeuserprofile = { + name: profile.displayName || profile.username, + photo: User.parsePhotoByProfile(profile) + } + _note.lastchangeuser = lastchangeuser; + note.lastchangeuserprofile = lastchangeuserprofile; + Note.updateLastChangeUser(_note, lastchangeuser, function (err, result) { + if (err) return callback(err, null); + callback(null, null); + }); + } + } + }); + } + } else { + _note.lastchangeuser = null; + note.lastchangeuserprofile = null; + Note.updateLastChangeUser(_note, null, function (err, result) { + if (err) return callback(err, null); + callback(null, null); + }); + } + }); +} +function updaterUpdatePostgres(note, callback) { + //postgres update + var body = note.server.document; + var title = Note.getNoteTitle(body); + title = LZString.compressToBase64(title); + body = LZString.compressToBase64(body); + db.saveToDB(note.id, title, body, function (err, result) { + if (err) return callback(err, null); + note.server.isDirty = false; + note.updatetime = Date.now(); + emitCheck(note); + callback(null, 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) { @@ -310,6 +324,19 @@ var disconnectSocketQueue = []; function finishConnection(socket, note, user) { if (!socket || !note || !user) return; + //check view permission + if (note.permission == 'private') { + if (socket.request.user && socket.request.user.logged_in && socket.request.user._id == note.owner) { + //na + } else { + socket.emit('info', { + code: 403 + }); + clearSocketQueue(connectionSocketQueue, socket); + isConnectionBusy = false; + return socket.disconnect(true); + } + } note.users[socket.id] = user; note.socks.push(socket); note.server.addClient(socket); @@ -363,13 +390,9 @@ function startConnection(socket) { var owner = data.rows[0].owner; var ownerprofile = null; - var permission = "freely"; - if (owner && owner != "null") { - permission = "editable"; - } //find or new note - Note.findOrNewNote(notename, permission, function (err, note) { + Note.findOrNewNote(notename, owner, function (err, note) { if (err) { responseError(res, "404", "Not Found", "oops."); clearSocketQueue(connectionSocketQueue, socket); @@ -399,41 +422,52 @@ function startConnection(socket) { updatetime: moment(updatetime).valueOf(), server: server }; - - if (lastchangeuser) { - //find last change user profile if lastchangeuser exists - User.findUser(lastchangeuser, function (err, user) { - if (!err && user && user.profile) { - var profile = JSON.parse(user.profile); - if (profile) { - lastchangeuserprofile = { - name: profile.displayName || profile.username, - photo: User.parsePhotoByProfile(profile) + + async.parallel([ + function getlastchangeuser(callback) { + if (lastchangeuser) { + //find last change user profile if lastchangeuser exists + User.findUser(lastchangeuser, function (err, user) { + if (!err && user && user.profile) { + var profile = JSON.parse(user.profile); + if (profile) { + lastchangeuserprofile = { + name: profile.displayName || profile.username, + photo: User.parsePhotoByProfile(profile) + } + notes[notename].lastchangeuserprofile = lastchangeuserprofile; + } } - notes[notename].lastchangeuserprofile = lastchangeuserprofile; - } + callback(null, null); + }); + } else { + callback(null, null); } - }); - } - - if (owner && owner != "null") { - //find owner profile if owner exists - User.findUser(owner, function (err, user) { - if (!err && user && user.profile) { - var profile = JSON.parse(user.profile); - if (profile) { - ownerprofile = { - name: profile.displayName || profile.username, - photo: User.parsePhotoByProfile(profile) + }, + function getowner(callback) { + if (owner && owner != "null") { + //find owner profile if owner exists + User.findUser(owner, function (err, user) { + if (!err && user && user.profile) { + var profile = JSON.parse(user.profile); + if (profile) { + ownerprofile = { + name: profile.displayName || profile.username, + photo: User.parsePhotoByProfile(profile) + } + notes[notename].ownerprofile = ownerprofile; + } } - notes[notename].ownerprofile = ownerprofile; - } + callback(null, null); + }); + } else { + callback(null, null); } - finishConnection(socket, notes[notename], users[socket.id]); - }); - } else { + } + ], function(err, results){ + if (err) return; finishConnection(socket, notes[notename], users[socket.id]); - } + }); }); }); } else { @@ -545,14 +579,14 @@ function ifMayEdit(socket, callback) { if (!socket.request.user || !socket.request.user.logged_in) mayEdit = false; break; - case "locked": + case "locked": case "private": //only owner can change if (note.owner != socket.request.user._id) mayEdit = false; break; } //if user may edit and this note have owner (not anonymous usage) - if (mayEdit && note.owner && note.owner != "null") { + if (socket.origin == 'operation' && mayEdit && note.owner && note.owner != "null") { //save for the last change user id if (socket.request.user && socket.request.user.logged_in) { note.lastchangeuser = socket.request.user._id; @@ -652,12 +686,22 @@ function connection(socket) { 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]; - sock.emit('permission', out); - }; - */ + if (typeof sock !== 'undefined' && sock) { + //check view permission + if (permission == 'private') { + if (sock.request.user && sock.request.user.logged_in && sock.request.user._id == note.owner) { + //na + } else { + sock.emit('info', { + code: 403 + }); + return sock.disconnect(true); + } + } + } + } }); }); } diff --git a/lib/response.js b/lib/response.js index a30df4700..e27411c85 100644 --- a/lib/response.js +++ b/lib/response.js @@ -90,21 +90,9 @@ function showIndex(req, res, next) { } function responseHackMD(res, noteId) { - if (noteId != config.featuresnotename) { - if (!Note.checkNoteIdValid(noteId)) { - responseError(res, "404", "Not Found", "oops."); - return; - } - noteId = LZString.decompressFromBase64(noteId); - if (!noteId) { - responseError(res, "404", "Not Found", "oops."); - return; - } - } db.readFromDB(noteId, function (err, data) { if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } var body = LZString.decompressFromBase64(data.rows[0].content); var meta = null; @@ -144,14 +132,18 @@ function newNote(req, res, next) { body = LZString.compressToBase64(body); var owner = null; if (req.isAuthenticated()) { - owner = req.session.passport.user; + owner = req.user._id; } db.newToDB(newId, owner, body, function (err, result) { if (err) { - responseError(res, "500", "Internal Error", "wtf."); - return; + return response.errorInternalError(res); } - res.redirect("/" + LZString.compressToBase64(newId)); + Note.newNote(newId, owner, function(err, result) { + if (err) { + return response.errorInternalError(res); + } + res.redirect("/" + LZString.compressToBase64(newId)); + }); }); } @@ -162,8 +154,7 @@ function showFeatures(req, res, next) { body = LZString.compressToBase64(body); db.newToDB(config.featuresnotename, null, body, function (err, result) { if (err) { - responseError(res, "500", "Internal Error", "wtf."); - return; + return response.errorInternalError(res); } responseHackMD(res, config.featuresnotename); }); @@ -175,11 +166,32 @@ function showFeatures(req, res, next) { function showNote(req, res, next) { var noteId = req.params.noteId; - if (!Note.checkNoteIdValid(noteId)) { - responseError(res, "404", "Not Found", "oops."); - return; + if (noteId != config.featuresnotename) { + if (!Note.checkNoteIdValid(noteId)) { + return response.errorNotFound(res); + } + noteId = LZString.decompressFromBase64(noteId); + if (!noteId) { + return response.errorNotFound(res); + } } - responseHackMD(res, noteId); + Note.findNote(noteId, function (err, note) { + if (err || !note) { + return response.errorNotFound(res); + } + db.readFromDB(note.id, function (err, data) { + if (err) { + return response.errorNotFound(res); + } + var notedata = data.rows[0]; + //check view permission + if (note.permission == 'private') { + if (!req.isAuthenticated() || notedata.owner != req.user._id) + return response.errorForbidden(res); + } + responseHackMD(res, noteId); + }); + }); } function showPublishNote(req, res, next) { @@ -187,30 +199,33 @@ function showPublishNote(req, res, next) { if (shortId.isValid(shortid)) { Note.findNote(shortid, function (err, note) { if (err || !note) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } - //increase note viewcount - Note.increaseViewCount(note, function (err, note) { - if (err || !note) { - responseError(res, "404", "Not Found", "oops."); - return; + db.readFromDB(note.id, function (err, data) { + if (err) { + return response.errorNotFound(res); } - db.readFromDB(note.id, function (err, data) { - if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + var notedata = data.rows[0]; + //check view permission + if (note.permission == 'private') { + if (!req.isAuthenticated() || notedata.owner != req.user._id) + return response.errorForbidden(res); + } + //increase note viewcount + Note.increaseViewCount(note, function (err, note) { + if (err || !note) { + return response.errorNotFound(res); } - var body = LZString.decompressFromBase64(data.rows[0].content); + var body = LZString.decompressFromBase64(notedata.content); var meta = null; try { meta = metaMarked(body).meta; } catch(err) { //na } - var updatetime = data.rows[0].update_time; + var updatetime = notedata.update_time; var text = S(body).escapeHTML().s; - var title = data.rows[0].title; + var title = notedata.title; var decodedTitle = LZString.decompressFromBase64(title); if (decodedTitle) title = decodedTitle; title = Note.generateWebTitle(title); @@ -247,7 +262,7 @@ function showPublishNote(req, res, next) { }); }); } else { - responseError(res, "404", "Not Found", "oops."); + return response.errorNotFound(res); } } @@ -271,18 +286,12 @@ function renderPublish(data, res) { function actionPublish(req, res, noteId) { db.readFromDB(noteId, function (err, data) { if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } var owner = data.rows[0].owner; - var permission = "freely"; - if (owner && owner != "null") { - permission = "editable"; - } - Note.findOrNewNote(noteId, permission, function (err, note) { + Note.findOrNewNote(noteId, owner, function (err, note) { if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } res.redirect("/s/" + note.shortid); }); @@ -292,18 +301,12 @@ function actionPublish(req, res, noteId) { function actionSlide(req, res, noteId) { db.readFromDB(noteId, function (err, data) { if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } var owner = data.rows[0].owner; - var permission = "freely"; - if (owner && owner != "null") { - permission = "editable"; - } - Note.findOrNewNote(noteId, permission, function (err, note) { + Note.findOrNewNote(noteId, owner, function (err, note) { if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } res.redirect("/p/" + note.shortid); }); @@ -313,8 +316,7 @@ function actionSlide(req, res, noteId) { function actionDownload(req, res, noteId) { db.readFromDB(noteId, function (err, data) { if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } var body = LZString.decompressFromBase64(data.rows[0].content); var title = Note.getNoteTitle(body); @@ -331,8 +333,7 @@ function actionDownload(req, res, noteId) { function actionPDF(req, res, noteId) { db.readFromDB(noteId, function (err, data) { if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } var body = LZString.decompressFromBase64(data.rows[0].content); try { @@ -365,57 +366,81 @@ function noteActions(req, res, next) { var noteId = req.params.noteId; if (noteId != config.featuresnotename) { if (!Note.checkNoteIdValid(noteId)) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } noteId = LZString.decompressFromBase64(noteId); if (!noteId) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } } - var action = req.params.action; - switch (action) { - case "publish": - case "pretty": //pretty deprecated - actionPublish(req, res, noteId); - break; - case "slide": - actionSlide(req, res, noteId); - break; - case "download": - actionDownload(req, res, noteId); - break; - case "pdf": - actionPDF(req, res, noteId); - break; - default: - if (noteId != config.featuresnotename) - res.redirect('/' + LZString.compressToBase64(noteId)); - else - res.redirect('/' + noteId); - break; - } + Note.findNote(noteId, function (err, note) { + if (err || !note) { + return response.errorNotFound(res); + } + db.readFromDB(note.id, function (err, data) { + if (err) { + return response.errorNotFound(res); + } + var notedata = data.rows[0]; + //check view permission + if (note.permission == 'private') { + if (!req.isAuthenticated() || notedata.owner != req.user._id) + return response.errorForbidden(res); + } + var action = req.params.action; + switch (action) { + case "publish": + case "pretty": //pretty deprecated + actionPublish(req, res, noteId); + break; + case "slide": + actionSlide(req, res, noteId); + break; + case "download": + actionDownload(req, res, noteId); + break; + case "pdf": + actionPDF(req, res, noteId); + break; + default: + if (noteId != config.featuresnotename) + res.redirect('/' + LZString.compressToBase64(noteId)); + else + res.redirect('/' + noteId); + break; + } + }); + }); } function publishNoteActions(req, res, next) { - var action = req.params.action; - switch (action) { - case "edit": - var shortid = req.params.shortid; - if (shortId.isValid(shortid)) { - Note.findNote(shortid, function (err, note) { - if (err || !note) { - responseError(res, "404", "Not Found", "oops."); - return; + var shortid = req.params.shortid; + if (shortId.isValid(shortid)) { + Note.findNote(shortid, function (err, note) { + if (err || !note) { + return response.errorNotFound(res); + } + db.readFromDB(note.id, function (err, data) { + if (err) { + return response.errorNotFound(res); + } + var notedata = data.rows[0]; + //check view permission + if (note.permission == 'private') { + if (!req.isAuthenticated() || notedata.owner != req.user._id) + return response.errorForbidden(res); + } + var action = req.params.action; + switch (action) { + case "edit": + if (note.id != config.featuresnotename) + res.redirect('/' + LZString.compressToBase64(note.id)); + else + res.redirect('/' + note.id); + break; } - if (note.id != config.featuresnotename) - res.redirect('/' + LZString.compressToBase64(note.id)); - else - res.redirect('/' + note.id); }); - } - break; + }); } } @@ -424,27 +449,30 @@ function showPublishSlide(req, res, next) { if (shortId.isValid(shortid)) { Note.findNote(shortid, function (err, note) { if (err || !note) { - responseError(res, "404", "Not Found", "oops."); - return; + return response.errorNotFound(res); } - //increase note viewcount - Note.increaseViewCount(note, function (err, note) { - if (err || !note) { - responseError(res, "404", "Not Found", "oops."); - return; + db.readFromDB(note.id, function (err, data) { + if (err) { + return response.errorNotFound(res); } - db.readFromDB(note.id, function (err, data) { - if (err) { - responseError(res, "404", "Not Found", "oops."); - return; + var notedata = data.rows[0]; + //check view permission + if (note.permission == 'private') { + if (!req.isAuthenticated() || notedata.owner != req.user._id) + return response.errorForbidden(res); + } + //increase note viewcount + Note.increaseViewCount(note, function (err, note) { + if (err || !note) { + return response.errorNotFound(res); } - var body = LZString.decompressFromBase64(data.rows[0].content); + var body = LZString.decompressFromBase64(notedata.content); try { body = metaMarked(body).markdown; } catch(err) { //na } - var title = data.rows[0].title; + var title = notedata.title; var decodedTitle = LZString.decompressFromBase64(title); if (decodedTitle) title = decodedTitle; title = Note.generateWebTitle(title); @@ -454,7 +482,7 @@ function showPublishSlide(req, res, next) { }); }); } else { - responseError(res, "404", "Not Found", "oops."); + return response.errorNotFound(res); } } diff --git a/public/js/index.js b/public/js/index.js index 1dfadc9af..86771fde0 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -327,7 +327,8 @@ var ui = { label: $(".ui-permission-label"), freely: $(".ui-permission-freely"), editable: $(".ui-permission-editable"), - locked: $(".ui-permission-locked") + locked: $(".ui-permission-locked"), + private: $(".ui-permission-private") } }, toc: { @@ -1067,6 +1068,10 @@ ui.infobar.permission.editable.click(function () { ui.infobar.permission.locked.click(function () { emitPermission("locked"); }); +//private +ui.infobar.permission.private.click(function () { + emitPermission("private"); +}); function emitPermission(_permission) { if (_permission != permission) { @@ -1094,6 +1099,10 @@ function updatePermission(newPermission) { label = ' Locked'; title = "Only owner can edit"; break; + case "private": + label = ' Private'; + title = "Only owner can view & edit"; + break; } if (personalInfo.userid == owner) { label += ' '; @@ -1118,6 +1127,7 @@ function havePermission() { } break; case "locked": + case "private": if (personalInfo.userid != owner) { bool = false; } else { @@ -1145,7 +1155,14 @@ socket.emit = function () { }; socket.on('info', function (data) { console.error(data); - location.href = "./404"; + switch (data.code) { + case 404: + location.href = "./404"; + break; + case 403: + location.href = "./403"; + break; + } }); socket.on('error', function (data) { console.error(data); @@ -1755,6 +1772,7 @@ editor.on('beforeChange', function (cm, change) { $('.signin-modal').modal('show'); break; case "locked": + case "private": $('.locked-modal').modal('show'); break; } diff --git a/public/views/body.ejs b/public/views/body.ejs index b3a49db8a..fa7436e7f 100644 --- a/public/views/body.ejs +++ b/public/views/body.ejs @@ -18,6 +18,7 @@
  • Freely - Anyone can edit
  • Editable - Signed people can edit
  • Locked - Only owner can edit
  • +
  • Private - Only owner can view & edit