ethercalc
Version:
Multi-User Spreadsheet Server
713 lines (712 loc) • 23.9 kB
JavaScript
// Generated by LiveScript 1.4.0
(function(){
var join$ = [].join;
this.include = function(){
var J, csvParse, DB, SC, KEY, BASEPATH, EXPIRE, HMAC_CACHE, hmac, ref$, Text, Html, Csv, Json, fs, RealBin, DevMode, sendFile, newRoom, IO, api, ExportCSVJSON, ExportCSV, ExportHTML, JTypeMap, ExportJ, ExportExcelXML, requestToCommand, requestToSave, i$, len$, route, ref1$;
this.use('json', this.app.router, this.express['static'](__dirname));
this.app.use('/edit', this.express['static'](__dirname));
this.app.use('/view', this.express['static'](__dirname));
this.include('dotcloud');
this.include('player-broadcast');
this.include('player-graph');
this.include('player');
J = require('j');
csvParse = require('csv-parse');
DB = this.include('db');
SC = this.include('sc');
KEY = this.KEY;
BASEPATH = this.BASEPATH;
EXPIRE = this.EXPIRE;
HMAC_CACHE = {};
hmac = !KEY
? function(it){
return it;
}
: function(it){
var encoder;
return HMAC_CACHE[it] || (HMAC_CACHE[it] = (encoder = require('crypto').createHmac('sha256', KEY), encoder.update(it.toString()), encoder.digest('hex')));
};
ref$ = ['text/plain', 'text/html', 'text/csv', 'application/json'].map((function(it){
return it + "; charset=utf-8";
})), Text = ref$[0], Html = ref$[1], Csv = ref$[2], Json = ref$[3];
fs = require('fs');
RealBin = require('path').dirname(fs.realpathSync(__filename));
DevMode = fs.existsSync(RealBin + "/.git");
sendFile = function(file){
return function(){
this.response.type(Html);
return this.response.sendfile(RealBin + "/" + file);
};
};
if (this.CORS) {
console.log("Cross-Origin Resource Sharing (CORS) enabled.");
this.all('*', function(req, res, next){
this.response.header('Access-Control-Allow-Origin', '*');
this.response.header('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type,If-Modified-Since');
this.response.header('Access-Control-Allow-Methods', 'GET,POST,PUT');
if ((req != null ? req.method : void 8) === 'OPTIONS') {
return res.send(204);
}
return next();
});
}
newRoom = function(){
return require('uuid-pure').newId(10, 36).toLowerCase();
};
this.get({
'/': sendFile('index.html')
});
this.get({
'/favicon.ico': function(){
return this.response.send(404, '');
}
});
this.get({
'/manifest.appcache': function(){
this.response.type('text/cache-manifest');
if (DevMode) {
return this.response.send(200, "CACHE MANIFEST\n\n#" + Date() + "\n\nNETWORK:\n*\n");
} else {
return this.response.sendfile(RealBin + "/manifest.appcache");
}
}
});
this.get({
'/static/socialcalc:part.js': function(){
var part;
part = this.params.part;
this.response.type('application/javascript');
return this.response.sendfile(RealBin + "/socialcalc" + part + ".js");
}
});
this.get({
'/static/form:part.js': function(){
var part;
part = this.params.part;
this.response.type('application/javascript');
return this.response.sendfile(RealBin + "/form" + part + ".js");
}
});
this.get({
'/=_new': function(){
var room;
room = newRoom();
return this.response.redirect(KEY
? BASEPATH + "/=" + room + "/edit"
: BASEPATH + "/=" + room);
}
});
this.get({
'/_new': function(){
var room;
room = newRoom();
return this.response.redirect(KEY
? BASEPATH + "/" + room + "/edit"
: BASEPATH + "/" + room);
}
});
this.get({
'/_start': sendFile('start.html')
});
IO = this.io;
api = function(cb, cbMultiple){
return function(){
var room, this$ = this;
room = encodeURIComponent(this.params.room).replace(/%3A/g, ':');
if (/^%3D/.exec(room) && cbMultiple) {
room = room.slice(3);
return SC._get(room, IO, function(arg$){
var snapshot;
snapshot = arg$.snapshot;
if (!snapshot) {
this$.response.type(Text);
this$.response.send(404, '');
return;
}
return SC[room].exportCSV(function(csv){
return csvParse(csv, {
delimiter: ','
}, function(_, body){
var todo, names, i$, len$, idx, ref$, link, title;
body.shift();
todo = DB.multi();
names = [];
for (i$ = 0, len$ = body.length; i$ < len$; ++i$) {
idx = i$;
ref$ = body[i$], link = ref$[0], title = ref$[1];
if (link && title && /^\//.exec(link)) {
names = names.concat(title);
todo = todo.get("snapshot-" + link.slice(1));
}
}
return todo.exec(function(_, saves){
var ref$, type, content;
ref$ = cbMultiple.call(this$.params, names, saves), type = ref$[0], content = ref$[1];
this$.response.type(type);
this$.response.set('Content-Disposition', "attachment; filename=\"" + room + ".xlsx\"");
return this$.response.send(200, content);
});
});
});
});
} else {
return SC._get(room, IO, function(arg$){
var snapshot, ref$, type, content;
snapshot = arg$.snapshot;
if (snapshot) {
ref$ = cb.call(this$.params, snapshot), type = ref$[0], content = ref$[1];
if (type === Csv) {
this$.response.set('Content-Disposition', "attachment; filename=\"" + this$.params.room + ".csv\"");
}
if (content instanceof Function) {
return content(SC[room], function(rv){
this$.response.type(type);
return this$.response.send(200, rv);
});
} else {
this$.response.type(type);
return this$.response.send(200, content);
}
} else {
this$.response.type(Text);
return this$.response.send(404, '');
}
});
}
};
};
ExportCSVJSON = api(function(){
return [
Json, function(sc, cb){
return sc.exportCSV(function(csv){
return csvParse(csv, {
delimiter: ','
}, function(_, body){
return cb(body);
});
});
}
];
});
ExportCSV = api(function(){
return [
Csv, function(sc, cb){
return sc.exportCSV(cb);
}
];
});
ExportHTML = api(function(){
return [
Html, function(sc, cb){
return sc.exportHTML(cb);
}
];
});
JTypeMap = {
md: 'text/x-markdown',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ods: 'application/vnd.oasis.opendocument.spreadsheet'
};
ExportJ = function(type){
return api(function(it){
var rv;
rv = J.utils["to_" + type](J.read(it));
if ((rv != null ? rv.Sheet1 : void 8) != null) {
rv = rv.Sheet1;
}
return [JTypeMap[type], rv];
}, function(names, saves){
var input, i$, len$, idx, save, ref$, harb, Sheet1, rv;
input = [
null, {
SheetNames: names,
Sheets: {}
}
];
for (i$ = 0, len$ = saves.length; i$ < len$; ++i$) {
idx = i$;
save = saves[i$];
ref$ = J.read(save), harb = ref$[0], Sheet1 = ref$[1].Sheets.Sheet1;
input[0] || (input[0] = harb);
input[1].Sheets[names[idx]] = Sheet1;
}
rv = J.utils["to_" + type](input);
return [JTypeMap[type], rv];
});
};
ExportExcelXML = api(function(){});
this.get({
'/:room.csv': ExportCSV
});
this.get({
'/:room.csv.json': ExportCSVJSON
});
this.get({
'/:room.html': ExportHTML
});
this.get({
'/:room.xlsx': ExportJ('xlsx')
});
this.get({
'/:room.md': ExportJ('md')
});
this.get({
'/_from/:template': function(){
var room, template, this$ = this;
room = newRoom();
template = this.params.template;
delete SC[room];
return SC._get(template, IO, function(arg$){
var snapshot;
snapshot = arg$.snapshot;
return SC._put(room, snapshot, function(){
return this$.response.redirect(KEY
? BASEPATH + "/" + room + "/edit"
: BASEPATH + "/" + room);
});
});
}
});
this.get({
'/:room': function(){
var uiFile, ref$;
uiFile = /^=/.exec(this.params.room) ? 'multi/index.html' : 'index.html';
if (KEY) {
if ((ref$ = this.query.auth) != null && ref$.length) {
return sendFile(uiFile).call(this);
} else {
return this.response.redirect(BASEPATH + "/" + this.params.room + "?auth=0");
}
} else {
return sendFile(uiFile).call(this);
}
}
});
this.get({
'/:room/edit': function(){
var room;
room = this.params.room;
return this.response.redirect(BASEPATH + "/" + room + "?auth=" + hmac(room));
}
});
this.get({
'/:room/view': function(){
var room;
room = this.params.room;
return this.response.redirect(BASEPATH + "/" + room + "?auth=0");
}
});
this.get({
'/_/:room/cells/:cell': api(function(){
var this$ = this;
return [
Json, function(sc, cb){
return sc.exportCell(this$.cell, cb);
}
];
})
});
this.get({
'/_/:room/cells': api(function(){
return [
Json, function(sc, cb){
return sc.exportCells(cb);
}
];
})
});
this.get({
'/_/:room/html': ExportHTML
});
this.get({
'/_/:room/csv': ExportCSV
});
this.get({
'/_/:room/csv.json': ExportCSVJSON
});
this.get({
'/_/:room/xlsx': ExportJ('xlsx')
});
this.get({
'/_/:room/md': ExportJ('md')
});
this.get({
'/_/:room': api(function(it){
return [Text, it];
})
});
requestToCommand = function(request, cb){
var command, ref$, cs, this$ = this;
if (request.is('application/json')) {
command = (ref$ = request.body) != null ? ref$.command : void 8;
if (command) {
return cb(command);
}
}
cs = [];
request.on('data', function(chunk){
return cs = cs.concat(chunk);
});
return request.on('end', function(){
var buf, k, ref$, save;
buf = Buffer.concat(cs);
if (request.is('text/x-socialcalc')) {
return cb(buf.toString('utf8'));
}
if (request.is('text/plain')) {
return cb(buf.toString('utf8'));
}
for (k in ref$ = J.utils.to_socialcalc(J.read(buf)) || {
'': ''
}) {
save = ref$[k];
save = save.replace(/[\d\D]*?\ncell:/, 'cell:');
save = save.replace(/\s--SocialCalcSpreadsheetControlSave--[\d\D]*/, '\n');
if (~save.indexOf("\\")) {
save = save.replace(/\\/g, "\\b");
}
if (~save.indexOf(":")) {
save = save.replace(/:/g, "\\c");
}
if (~save.indexOf("\n")) {
save = save.replace(/\n/g, "\\n");
}
return cb("loadclipboard " + save);
}
});
};
requestToSave = function(request, cb){
var snapshot, ref$, cs, this$ = this;
if (request.is('application/json')) {
snapshot = (ref$ = request.body) != null ? ref$.snapshot : void 8;
if (snapshot) {
return cb(snapshot);
}
}
cs = [];
request.on('data', function(chunk){
return cs = cs.concat(chunk);
});
return request.on('end', function(){
var buf, k, ref$, save;
buf = Buffer.concat(cs);
if (request.is('text/x-socialcalc')) {
return cb(buf.toString('utf8'));
}
for (k in ref$ = J.utils.to_socialcalc(J.read(buf)) || {
'': ''
}) {
save = ref$[k];
return cb(save);
}
});
};
for (i$ = 0, len$ = (ref$ = ['/=:room.xlsx', '/_/=:room/xlsx']).length; i$ < len$; ++i$) {
route = ref$[i$];
this.put((ref1$ = {}, ref1$[route + ""] = fn$, ref1$));
}
this.put({
'/_/:room': function(){
var room, this$ = this;
this.response.type(Text);
room = this.params.room;
return requestToSave(this.request, function(snapshot){
var ref$;
if ((ref$ = SC[room]) != null) {
ref$.terminate();
}
delete SC[room];
return SC._put(room, snapshot, function(){
return DB.del("log-" + room, function(){
IO.sockets['in']("log-" + room).emit('data', {
snapshot: snapshot,
type: 'snapshot'
});
return this$.response.send(201, 'OK');
});
});
});
}
});
this.post({
'/_/:room': function(){
var room, this$ = this;
room = this.params.room;
if (room === 'Kaohsiung-explode-20140801') {
return;
}
return requestToCommand(this.request, function(command){
if (!command) {
this$.response.type(Text);
return this$.response.send(400, 'Please send command');
}
return SC._get(room, IO, function(arg$){
var log, snapshot, row, cmdstr;
log = arg$.log, snapshot = arg$.snapshot;
if (/^loadclipboard\s*/.exec(command)) {
row = 1;
if (/\nsheet:c:\d+:r:(\d+):/.exec(snapshot)) {
row += Number(RegExp.$1);
}
if (parseInt(this$.query.row)) {
row = parseInt(this$.query.row);
command = [command, "insertrow A" + row, "paste A" + row + " all"];
} else {
command = [command, "paste A" + row + " all"];
}
}
if (/^set\s+(A\d+):B\d+\s+empty\s+multi-cascade/.exec(command)) {
DB.multi().get("snapshot-" + room).exec(function(_, arg$){
var snapshot, sheetId, matches, removeKey, backupKey;
snapshot = arg$[0];
if (snapshot) {
sheetId = RegExp.$1;
matches = snapshot.match(new RegExp("cell:" + sheetId + ":t:/(.+)\n", "i"));
if (matches) {
removeKey = matches[1];
backupKey = matches[1] + ".bak";
return DB.multi().del("snapshot-" + backupKey).rename("snapshot-" + removeKey, "snapshot-" + backupKey).del("log-" + backupKey).rename("log-" + removeKey, "log-" + backupKey).del("audit-" + backupKey).rename("audit-" + removeKey, "audit-" + backupKey).bgsave().exec(function(_){});
}
}
});
}
if (!Array.isArray(command)) {
command = [command];
}
cmdstr = join$.call(command, '\n');
return DB.multi().rpush("log-" + room, cmdstr).rpush("audit-" + room, cmdstr).bgsave().exec(function(){
var ref$;
if ((ref$ = SC[room]) != null) {
ref$.ExecuteCommand(cmdstr);
}
IO.sockets['in']("log-" + room).emit('data', {
cmdstr: cmdstr,
room: room,
type: 'execute'
});
return this$.response.json(202, {
command: command
});
});
});
});
}
});
this.post({
'/_': function(){
var this$ = this;
return requestToSave(this.request, function(snapshot){
var room, ref$;
room = ((ref$ = this$.body) != null ? ref$.room : void 8) || newRoom();
return SC._put(room, snapshot, function(){
this$.response.type(Text);
this$.response.location("/_/" + room);
return this$.response.send(201, "/" + room);
});
});
}
});
this.on({
disconnect: function(){
var id, ref$, key, i$, ref1$, len$, client, room, ref2$, val, isConnected, ref3$;
id = this.socket.id;
if (((ref$ = IO.sockets.manager) != null ? ref$.roomClients : void 8) != null) {
CleanRoomLegacy: for (key in IO.sockets.manager.roomClients[id]) {
if (/^\/log-/.exec(key)) {
for (i$ = 0, len$ = (ref1$ = IO.sockets.clients(key.substr(1))).length; i$ < len$; ++i$) {
client = ref1$[i$];
if (client.id !== id) {
continue CleanRoomLegacy;
}
}
room = key.substr(5);
if ((ref1$ = SC[room]) != null) {
ref1$.terminate();
}
delete SC[room];
}
}
return;
}
CleanRoom: for (key in ref2$ = IO.sockets.adapter.rooms) {
val = ref2$[key];
if (/^log-/.exec(key)) {
for (client in val) {
isConnected = val[client];
if (isConnected && client !== id) {
continue CleanRoom;
}
}
room = key.substr(4);
if ((ref3$ = SC[room]) != null) {
ref3$.terminate();
}
delete SC[room];
}
}
}
});
return this.on({
data: function(){
var ref$, room, msg, user, ecell, cmdstr, type, auth, reply, broadcast, this$ = this;
ref$ = this.data, room = ref$.room, msg = ref$.msg, user = ref$.user, ecell = ref$.ecell, cmdstr = ref$.cmdstr, type = ref$.type, auth = ref$.auth;
room = (room + "").replace(/^_+/, '');
if (EXPIRE) {
DB.expire("snapshot-" + room, EXPIRE);
}
reply = function(data){
return this$.emit({
data: data
});
};
broadcast = function(data){
return this$.socket.broadcast.to(this$.data.to
? "user-" + this$.data.to
: "log-" + room).emit('data', data);
};
switch (type) {
case 'chat':
DB.rpush("chat-" + room, msg, function(){
return broadcast(this$.data);
});
break;
case 'ask.ecells':
DB.hgetall("ecell-" + room, function(_, values){
return broadcast({
type: 'ecells',
ecells: values,
room: room
});
});
break;
case 'my.ecell':
DB.hset("ecell-" + room, user, ecell);
break;
case 'execute':
if (auth === '0') {
return;
}
if (/^set sheet defaulttextvalueformat text-wiki\s*$/.exec(cmdstr)) {
return;
}
if (KEY && hmac(room) !== auth) {
reply({
type: 'error',
message: "Invalid session key. Modifications will not be saved."
});
return;
}
DB.multi().rpush("log-" + room, cmdstr).rpush("audit-" + room, cmdstr).bgsave().exec(function(){
var ref$;
if (SC[room] == null) {
console.log("SC[" + room + "] went away. Reloading...");
DB.multi().get("snapshot-" + room).lrange("log-" + room, 0, -1).exec(function(_, arg$){
var snapshot, log;
snapshot = arg$[0], log = arg$[1];
return SC[room] = SC._init(snapshot, log, DB, room, this$.io);
});
}
if ((ref$ = SC[room]) != null) {
ref$.ExecuteCommand(cmdstr);
}
return broadcast(this$.data);
});
break;
case 'ask.log':
this.socket.join("log-" + room);
this.socket.join("user-" + user);
DB.multi().get("snapshot-" + room).lrange("log-" + room, 0, -1).lrange("chat-" + room, 0, -1).exec(function(_, arg$){
var snapshot, log, chat;
snapshot = arg$[0], log = arg$[1], chat = arg$[2];
SC[room] = SC._init(snapshot, log, DB, room, this$.io);
return reply({
type: 'log',
room: room,
log: log,
chat: chat,
snapshot: snapshot
});
});
break;
case 'ask.recalc':
this.socket.join("recalc." + room);
if ((ref$ = SC[room]) != null) {
ref$.terminate();
}
delete SC[room];
SC._get(room, this.io, function(arg$){
var log, snapshot;
log = arg$.log, snapshot = arg$.snapshot;
return reply({
type: 'recalc',
room: room,
log: log,
snapshot: snapshot
});
});
break;
case 'stopHuddle':
if (this.KEY && KEY !== this.KEY) {
return;
}
DB.del(['audit', 'log', 'chat', 'ecell', 'snapshot'].map(function(it){
return it + "-" + room;
}), function(){
var ref$;
if ((ref$ = SC[room]) != null) {
ref$.terminate();
}
delete SC[room];
return broadcast(this$.data);
});
break;
case 'ecell':
if (auth === '0' || KEY && auth !== hmac(room)) {
return;
}
broadcast(this.data);
break;
default:
broadcast(this.data);
}
}
});
function fn$(){
var room, cs, this$ = this;
room = encodeURIComponent(this.params.room).replace(/%3A/g, ':');
cs = [];
this.request.on('data', function(chunk){
return cs = cs.concat(chunk);
});
return this.request.on('end', function(){
var buf, idx, toc, parsed, sheetsToIdx, res, k, Sheet1, todo, save;
buf = Buffer.concat(cs);
idx = 0;
toc = '#url,#title\n';
parsed = J.utils.to_socialcalc(J.read(buf));
sheetsToIdx = {};
res = [];
for (k in parsed) {
idx++;
sheetsToIdx[k] = idx;
toc += "\"/" + this$.params.room.replace(/"/g, '""') + "." + idx + "\",";
toc += "\"" + k.replace(/"/g, '""') + "\"\n";
res.push(k.replace(/'/g, "''").replace(/(\W)/g, '\\$1'));
}
Sheet1 = J.utils.to_socialcalc(J.read(toc)).Sheet1;
todo = DB.multi().set("snapshot-" + room, Sheet1);
for (k in parsed) {
save = parsed[k];
idx = sheetsToIdx[k];
save = save.replace(RegExp('(\'?)\\b(' + res.join('|') + ')\\1!', 'g'), fn$);
todo = todo.set("snapshot-" + room + "." + idx, save);
}
todo.bgsave().exec();
return this$.response.send(201, 'OK');
function fn$(arg$, arg1$, ref){
return "'" + this$.params.room.replace(/'/g, "''") + "." + sheetsToIdx[ref.replace(/''/g, "'")] + "'!";
}
});
}
};
}).call(this);