ep_codepad
Version:
Turn etherpad into a realtime collaborative development environment
446 lines (376 loc) • 20.6 kB
JavaScript
var crypto = require('crypto');
var padManager = require("ep_etherpad-lite/node/db/PadManager");
var fs = require('fs');
var settings = require('ep_etherpad-lite/node/utils/Settings');
var authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
var sessionManager = require('ep_etherpad-lite/node/db/SessionManager');
// this should be overridden in codepad conf
var project_path = '/tmp/';
if (!settings.ep_codepad) {
console.log("CODEPAD needs ep_codepad parameters in settings.json.");
} else {
if (!settings.ep_codepad.project_path) {
console.log("CODEPAD No ep_codepad.project_path set in settings.json.");
} else project_path = settings.ep_codepad.project_path + "/";
}
// messages constants
var msg_push = 'PUSH_TO_FILESYSTEM';
var msg_write = 'WRITE_TO_FILESYSTEM';
var msg_read = 'READ_FROM_FILESYSTEM';
var msg_check = 'CHECK_ON_FILESYSTEM';
var err_msg = {};
// code beutifier
var beautify = require('js-beautify').js_beautify;
var beautify_css = require('js-beautify').css;
var beautify_html = require('js-beautify').html;
var exec = require("child_process").exec;
var getBrush = require('./extensions.js').getBrush;
// code syntax checker
var jshint = require('jshint').JSHINT;
var cb = function() {};
var padMessageHandler = require("ep_etherpad-lite/node/handler/PadMessageHandler");
var send_ok = function(padid) {
var ok_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "codepad",
errors: null
}
}
};
padMessageHandler.handleCustomObjectMessage(ok_msg, undefined, cb);
};
exports.handleMessage = function(hook_name, context, callback) {
if (context.message && context.message.data) {
var msg = context.message.data.type;
var uid = context.message.data.userId;
var padid = context.message.data.padId;
authorManager.getAuthorName(uid, function(iderr, name) {
if (iderr === null) {
console.log("CODEPAD MSG: " + msg + " " + uid + " " + name);
} else console.log("CODEPAD MSG: " + msg + " " + uid + " ERR getting name.");
});
// proxy for editing events - for highlighting lines of other users
if (msg === "EDIT") {
console.log("EDIT EVENT RECIEVED " + context.message.data.line);
var replymsg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
userId: uid,
line: context.message.data.line,
from: "ace"
}
}
};
padMessageHandler.handleCustomObjectMessage(replymsg, undefined, cb);
}
if (msg === msg_check) {
padManager.getPad(padid, null, function(gperr, pmpad) {
if (gperr === null) {
var path = project_path + padid;
// remove newline character from the end of the string.
//var text = pmpad.atext.text.slice(0, -1);
var text = pmpad.atext.text;
fs.readFile(path, function(frerr, data) {
if (frerr) {
// notify user about the read error
console.log("CODEPAD NTC - file doesent exist " + path);
//callback(null);
} else {
var adat = data.toString();
if (adat !== text.slice(0, -1)) {
console.log("READ from filesystem. " + path);
err_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "check",
errors: "File inconsistent with pad content " + adat.length + "/" + (text.length - 1)
}
}
};
padMessageHandler.handleCustomObjectMessage(err_msg, undefined, cb);
} else
send_ok(padid);
}
});
} else console.log("CODEPAD ERR - getPad failed!");
});
//callback(null);
}
if (msg === msg_read) {
padManager.getPad(padid, null, function(gperr, pmpad) {
if (gperr === null) {
var path = project_path + padid;
// remove newline character from the end of the string.
//var text = pmpad.atext.text.slice(0, -1);
var text = pmpad.atext.text;
fs.readFile(path, function(frerr, data) {
if (frerr) {
// notify user about the read error
console.log("CODEPAD ERR - file read error " + path);
err_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "fs",
errors: frerr
}
}
};
padMessageHandler.handleCustomObjectMessage(err_msg, undefined, cb);
//callback(null);
} else {
var adat = data.toString();
if (adat !== text) {
console.log("READ from filesystem. " + path);
pmpad.setText(adat);
padMessageHandler.updatePadClients(pmpad, cb);
}
send_ok(padid);
}
});
} else console.log("CODEPAD ERR - getPad failed!");
});
//callback(null);
}
if (msg === msg_write || msg === msg_push) {
padManager.getPad(padid, null, function(err, pmpad) {
var api = require('ep_etherpad-lite/node/db/API');
//var util = require('util');
// if all ok, send only one OK message.
var all_is_ok = true;
// check if anyone editing while a push was sent.
var now = new Date().getTime();
api.padUsers(padid, function(paduserr, padus) {
if (paduserr === null) {
var pushusr = '';
var pusherr = '';
if (msg == msg_push) {
// check each user
for (var i = 0; i < padus.padUsers.length; i++) {
var obi = padus.padUsers[i];
// exept the one who requested the push - timout limit is 5 sec
if (obi.id !== uid && now - obi.timestamp < 5000) {
all_is_ok = false;
if (obi.name) pusherr += obi.name.toString();
else pusherr += obi.id.toString();
pusherr += " ";
console.log("CODEPAD push denied " + obi.name + " " + obi.id + " is editing the pad. " + padid);
}
// to let other users know
if (obi.name) {
if (obi.id == uid) pushusr = obi.name.toString();
}
}
}
if (!all_is_ok) {
err_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "push",
user: uid,
name: pushusr,
errors: pusherr
}
}
};
padMessageHandler.handleCustomObjectMessage(err_msg, undefined, cb);
} else {
var padsi = padid.indexOf('/');
var folder = '';
// since EEXISTS is an error, we skip that error
// there will be a message if the write failes anyway
mkdir_err = function(err) {};
// create subfolders
while (padsi > 0) {
folder = padid.substring(0, padsi);
fs.mkdir(project_path + folder, mkdir_err);
padsi = padid.indexOf('/', 1 + folder.length);
}
// full path to write the file to
var path = project_path + padid;
// get the file extension/brush
var ext = getBrush(padid);
// the beutified or the raw text of the pad
// remove newline character from the end of the string.
//var text = pmpad.atext.text.slice(0, -1);
var text = pmpad.atext.text;
var beat = text;
// if .js file beautify
if (ext === 'js') {
//check for missing semiciolons forst
if (msg == msg_push && !jshint(text)) {
// determine if semicolons should be added at the end of line
var check = function(errors, line, length) {
var count = 0;
var chpos = 0;
errors.forEach(function(err) {
if (err) {
// err.line range is 1..n while line is 1..m
if (err.line == line + 1) {
count++;
if (err.code == 'W033') chpos = err.character;
// TODO ... maybe it should abort the whole process if there was another error
}
}
});
if (count === 1 && chpos !== 0 && length < chpos) return true;
else return false;
};
// Add the semicolons
var lines = text.split('\n');
beat = '';
for (var j = 0; j < lines.length; j++) {
//code here using lines[i] which will give you each line
if (check(jshint.errors, j, lines[j].length))
beat += lines[j] + ';' + '\n';
else beat += lines[j] + '\n';
}
}
// if push and jshint said ok, then beautify text for real
if (msg == msg_push && jshint(text)) beat = beautify(text, {
indent_size: 4
});
// re-check the new beautified code
if (!jshint(beat)) {
// still has errors, we put them to the console log on the server
jshint.errors.forEach(function(err) {
if (err) {
console.info(" ! " + padid + ":" + err.line + ":" + err.character + " " + err.reason + "|" + err.evidence);
// more detailed if you wish
//console.info(" ! " + padid + ":" + err.line + ":" + err.character + " " + err.reason + " !" + err.scope + "|" + err.evidence + "|" + err.id + "|" + err.code + "|" + err.raw);
}
});
all_is_ok = false;
err_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "jshint",
errors: jshint.errors
}
}
};
padMessageHandler.handleCustomObjectMessage(err_msg, undefined, cb);
}
}
// if .css file beautify
if (ext === 'css' && msg == msg_push) {
beat = beautify_css(text, {
indent_size: 4
});
}
// if .html file beautify
if (ext === 'xml' && msg == msg_push) {
beat = beautify_html(text, {
indent_size: 4
});
}
// TODO add bash beautifier and syntax checker here
// text and beat may be different, depending on jsHint and the beutifier
if (text !== beat) {
pmpad.setText(beat);
padMessageHandler.updatePadClients(pmpad, cb);
}
// If we write an empty file -- we actually will delete the file // but not delete pad
if (text.length <= 1 && text.charCodeAt(0) == 10) {
if (fs.existsSync(path)) {
fs.unlinkSync(path);
console.log("CODEPAD delete file " + path);
err_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "codepad",
errors: "File deleted."
}
}
};
padMessageHandler.handleCustomObjectMessage(err_msg, undefined, cb);
}
} else
// WRITE to the FILE
fs.writeFile(path, beat, function(err) {
if (err) {
all_is_ok = false;
console.log("CODEPAD Failed to write text to " + path);
err_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "fs",
errors: err
}
}
};
padMessageHandler.handleCustomObjectMessage(err_msg, undefined, cb);
} else {
console.log("CODEPAD Wrote pad contents to " + path);
// if push_action is defined in settings.json, it will run here, use it for git/svn/hg ... or whatever.
if (settings.ep_codepad && msg == msg_push) {
if (settings.ep_codepad.push_action) {
exec(settings.ep_codepad.push_action, function(exec_err, stdout, stderr) {
if (stdout) console.log("CODEPAD push_action stdout: " + stdout);
if (stderr) console.log("CODEPAD push_action stderr: " + stderr);
if (exec_err) {
console.log('CODEPAD push_action error: ', exec_err);
err_msg = {
type: 'COLLABROOM',
data: {
type: "CUSTOM",
payload: {
padId: padid,
from: "exec",
errors: exec_err
}
}
};
padMessageHandler.handleCustomObjectMessage(err_msg, undefined, cb);
} else if (all_is_ok) send_ok(padid);
});
} else if (all_is_ok) send_ok(padid);
} else if (all_is_ok) send_ok(padid);
}
}); // WRITE
} // else all_is_ok
} else console.log("CODEPAD error - Could not determine users."); // if paduserr
}); // api.padUsers
}); //padManager.getPad
//callback(null);
} //END if (msg == msg_write || msg == msg_push)
} //END if (context.message && context.message.data)
callback();
}; //END exports.handleMessage
/// jshint - quickhelp
/* An example from err.
"id": "(error)",
"raw": "Missing semicolon.",
"code": "W033",
"evidence": " };",
"line": 6,
"character": 5,
"scope": "(main)",
"reason": "Missing semicolon."
*/
//
//