The Big Renaming - etherpad is now an NPM module

This commit is contained in:
Egil Moeller 2012-02-26 13:07:51 +01:00
parent 1955bdec9a
commit 1239ce7f28
116 changed files with 9721 additions and 30 deletions

13
src/node/README.md Normal file
View file

@ -0,0 +1,13 @@
# About the folder structure
* **db** - all modules that are accesing the data structure and are communicating directly to the database
* **handler** - all modules that responds directly to requests/messages of the browser
* **utils** - helper modules
# Module name conventions
Module file names starts with a capital letter and uses camelCase
# Where does it start?
server.js is started directly

520
src/node/db/API.js Normal file
View file

@ -0,0 +1,520 @@
/**
* This module provides all API functions
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var padManager = require("./PadManager");
var padMessageHandler = require("../handler/PadMessageHandler");
var readOnlyManager = require("./ReadOnlyManager");
var groupManager = require("./GroupManager");
var authorManager = require("./AuthorManager");
var sessionManager = require("./SessionManager");
var async = require("async");
var exportHtml = require("../utils/ExportHtml");
var importHtml = require("../utils/ImportHtml");
var cleanText = require("./Pad").cleanText;
/**********************/
/**GROUP FUNCTIONS*****/
/**********************/
exports.createGroup = groupManager.createGroup;
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
exports.deleteGroup = groupManager.deleteGroup;
exports.listPads = groupManager.listPads;
exports.createGroupPad = groupManager.createGroupPad;
/**********************/
/**AUTHOR FUNCTIONS****/
/**********************/
exports.createAuthor = authorManager.createAuthor;
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
/**********************/
/**SESSION FUNCTIONS***/
/**********************/
exports.createSession = sessionManager.createSession;
exports.deleteSession = sessionManager.deleteSession;
exports.getSessionInfo = sessionManager.getSessionInfo;
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup;
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
/************************/
/**PAD CONTENT FUNCTIONS*/
/************************/
/**
getText(padID, [rev]) returns the text of a pad
Example returns:
{code: 0, message:"ok", data: {text:"Welcome Text"}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getText = function(padID, rev, callback)
{
//check if rev is set
if(typeof rev == "function")
{
callback = rev;
rev = undefined;
}
//check if rev is a number
if(rev !== undefined && typeof rev != "number")
{
//try to parse the number
if(!isNaN(parseInt(rev)))
{
rev = parseInt(rev);
}
else
{
callback(new customError("rev is not a number", "apierror"));
return;
}
}
//ensure this is not a negativ number
if(rev !== undefined && rev < 0)
{
callback(new customError("rev is a negativ number","apierror"));
return;
}
//ensure this is not a float value
if(rev !== undefined && !is_int(rev))
{
callback(new customError("rev is a float value","apierror"));
return;
}
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
//the client asked for a special revision
if(rev !== undefined)
{
//check if this is a valid revision
if(rev > pad.getHeadRevisionNumber())
{
callback(new customError("rev is higher than the head revision of the pad","apierror"));
return;
}
//get the text of this revision
pad.getInternalRevisionAText(rev, function(err, atext)
{
if(ERR(err, callback)) return;
data = {text: atext.text};
callback(null, data);
})
}
//the client wants the latest text, lets return it to him
else
{
callback(null, {"text": pad.text()});
}
});
}
/**
setText(padID, text) sets the text of a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.setText = function(padID, text, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
//set the text
pad.setText(text);
//update the clients on the pad
padMessageHandler.updatePadClients(pad, callback);
});
}
/**
getHTML(padID, [rev]) returns the html of a pad
Example returns:
{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getHTML = function(padID, rev, callback)
{
if(typeof rev == "function")
{
callback = rev;
rev = undefined;
}
if (rev !== undefined && typeof rev != "number")
{
if (!isNaN(parseInt(rev)))
{
rev = parseInt(rev);
}
else
{
callback(new customError("rev is not a number","apierror"));
return;
}
}
if(rev !== undefined && rev < 0)
{
callback(new customError("rev is a negative number","apierror"));
return;
}
if(rev !== undefined && !is_int(rev))
{
callback(new customError("rev is a float value","apierror"));
return;
}
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
//the client asked for a special revision
if(rev !== undefined)
{
//check if this is a valid revision
if(rev > pad.getHeadRevisionNumber())
{
callback(new customError("rev is higher than the head revision of the pad","apierror"));
return;
}
//get the html of this revision
exportHtml.getPadHTML(pad, rev, function(err, html)
{
if(ERR(err, callback)) return;
data = {html: html};
callback(null, data);
});
}
//the client wants the latest text, lets return it to him
else
{
exportHtml.getPadHTML(pad, undefined, function (err, html)
{
if(ERR(err, callback)) return;
data = {html: html};
callback(null, data);
});
}
});
}
exports.setHTML = function(padID, html, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
// add a new changeset with the new html to the pad
importHtml.setPadHTML(pad, cleanText(html));
//update the clients on the pad
padMessageHandler.updatePadClients(pad, callback);
});
}
/*****************/
/**PAD FUNCTIONS */
/*****************/
/**
getRevisionsCount(padID) returns the number of revisions of this pad
Example returns:
{code: 0, message:"ok", data: {revisions: 56}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getRevisionsCount = function(padID, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
callback(null, {revisions: pad.getHeadRevisionNumber()});
});
}
/**
createPad(padName [, text]) creates a new pad in this group
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"pad does already exist", data: null}
*/
exports.createPad = function(padID, text, callback)
{
//ensure there is no $ in the padID
if(padID && padID.indexOf("$") != -1)
{
callback(new customError("createPad can't create group pads","apierror"));
return;
}
//create pad
getPadSafe(padID, false, text, function(err)
{
if(ERR(err, callback)) return;
callback();
});
}
/**
deletePad(padID) deletes a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.deletePad = function(padID, callback)
{
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
pad.remove(callback);
});
}
/**
getReadOnlyLink(padID) returns the read only link of a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getReadOnlyID = function(padID, callback)
{
//we don't need the pad object, but this function does all the security stuff for us
getPadSafe(padID, true, function(err)
{
if(ERR(err, callback)) return;
//get the readonlyId
readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId)
{
if(ERR(err, callback)) return;
callback(null, {readOnlyID: readOnlyId});
});
});
}
/**
setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setPublicStatus = function(padID, publicStatus, callback)
{
//ensure this is a group pad
if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror"));
return;
}
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
//convert string to boolean
if(typeof publicStatus == "string")
publicStatus = publicStatus == "true" ? true : false;
//set the password
pad.setPublicStatus(publicStatus);
callback();
});
}
/**
getPublicStatus(padID) return true of false
Example returns:
{code: 0, message:"ok", data: {publicStatus: true}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getPublicStatus = function(padID, callback)
{
//ensure this is a group pad
if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror"));
return;
}
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
callback(null, {publicStatus: pad.getPublicStatus()});
});
}
/**
setPassword(padID, password) returns ok or a error message
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setPassword = function(padID, password, callback)
{
//ensure this is a group pad
if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the password of pads that belong to a group","apierror"));
return;
}
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
//set the password
pad.setPassword(password);
callback();
});
}
/**
isPasswordProtected(padID) returns true or false
Example returns:
{code: 0, message:"ok", data: {passwordProtection: true}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.isPasswordProtected = function(padID, callback)
{
//ensure this is a group pad
if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the password of pads that belong to a group","apierror"));
return;
}
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
callback(null, {isPasswordProtected: pad.isPasswordProtected()});
});
}
/******************************/
/** INTERNAL HELPER FUNCTIONS */
/******************************/
//checks if a number is an int
function is_int(value)
{
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
}
//gets a pad safe
function getPadSafe(padID, shouldExist, text, callback)
{
if(typeof text == "function")
{
callback = text;
text = null;
}
//check if padID is a string
if(typeof padID != "string")
{
callback(new customError("padID is not a string","apierror"));
return;
}
//check if the padID maches the requirements
if(!padManager.isValidPadId(padID))
{
callback(new customError("padID did not match requirements","apierror"));
return;
}
//check if the pad exists
padManager.doesPadExists(padID, function(err, exists)
{
if(ERR(err, callback)) return;
//does not exist, but should
if(exists == false && shouldExist == true)
{
callback(new customError("padID does not exist","apierror"));
}
//does exists, but shouldn't
else if(exists == true && shouldExist == false)
{
callback(new customError("padID does already exist","apierror"));
}
//pad exists, let's get it
else
{
padManager.getPad(padID, text, callback);
}
});
}

View file

@ -0,0 +1,181 @@
/**
* The AuthorManager controlls all information about the Pad authors
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
var randomString = CommonCode.require('/pad_utils').randomString;
/**
* Checks if the author exists
*/
exports.doesAuthorExists = function (authorID, callback)
{
//check if the database entry of this author exists
db.get("globalAuthor:" + authorID, function (err, author)
{
if(ERR(err, callback)) return;
callback(null, author != null);
});
}
/**
* Returns the AuthorID for a token.
* @param {String} token The token
* @param {Function} callback callback (err, author)
*/
exports.getAuthor4Token = function (token, callback)
{
mapAuthorWithDBKey("token2author", token, function(err, author)
{
if(ERR(err, callback)) return;
//return only the sub value authorID
callback(null, author ? author.authorID : author);
});
}
/**
* Returns the AuthorID for a mapper.
* @param {String} token The mapper
* @param {Function} callback callback (err, author)
*/
exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback)
{
mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author)
{
if(ERR(err, callback)) return;
//set the name of this author
if(name)
exports.setAuthorName(author.authorID, name);
//return the authorID
callback(null, author);
});
}
/**
* Returns the AuthorID for a mapper. We can map using a mapperkey,
* so far this is token2author and mapper2author
* @param {String} mapperkey The database key name for this mapper
* @param {String} mapper The mapper
* @param {Function} callback callback (err, author)
*/
function mapAuthorWithDBKey (mapperkey, mapper, callback)
{
//try to map to an author
db.get(mapperkey + ":" + mapper, function (err, author)
{
if(ERR(err, callback)) return;
//there is no author with this mapper, so create one
if(author == null)
{
exports.createAuthor(null, function(err, author)
{
if(ERR(err, callback)) return;
//create the token2author relation
db.set(mapperkey + ":" + mapper, author.authorID);
//return the author
callback(null, author);
});
}
//there is a author with this mapper
else
{
//update the timestamp of this author
db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime());
//return the author
callback(null, {authorID: author});
}
});
}
/**
* Internal function that creates the database entry for an author
* @param {String} name The name of the author
*/
exports.createAuthor = function(name, callback)
{
//create the new author name
var author = "a." + randomString(16);
//create the globalAuthors db entry
var authorObj = {"colorId" : Math.floor(Math.random()*32), "name": name, "timestamp": new Date().getTime()};
//set the global author db entry
db.set("globalAuthor:" + author, authorObj);
callback(null, {authorID: author});
}
/**
* Returns the Author Obj of the author
* @param {String} author The id of the author
* @param {Function} callback callback(err, authorObj)
*/
exports.getAuthor = function (author, callback)
{
db.get("globalAuthor:" + author, callback);
}
/**
* Returns the color Id of the author
* @param {String} author The id of the author
* @param {Function} callback callback(err, colorId)
*/
exports.getAuthorColorId = function (author, callback)
{
db.getSub("globalAuthor:" + author, ["colorId"], callback);
}
/**
* Sets the color Id of the author
* @param {String} author The id of the author
* @param {Function} callback (optional)
*/
exports.setAuthorColorId = function (author, colorId, callback)
{
db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback);
}
/**
* Returns the name of the author
* @param {String} author The id of the author
* @param {Function} callback callback(err, name)
*/
exports.getAuthorName = function (author, callback)
{
db.getSub("globalAuthor:" + author, ["name"], callback);
}
/**
* Sets the name of the author
* @param {String} author The id of the author
* @param {Function} callback (optional)
*/
exports.setAuthorName = function (author, name, callback)
{
db.setSub("globalAuthor:" + author, ["name"], name, callback);
}

57
src/node/db/DB.js Normal file
View file

@ -0,0 +1,57 @@
/**
* The DB Module provides a database initalized with the settings
* provided by the settings module
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ueberDB = require("ueberDB");
var settings = require("../utils/Settings");
var log4js = require('log4js');
//set database settings
var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB"));
/**
* The UeberDB Object that provides the database functions
*/
exports.db = null;
/**
* Initalizes the database with the settings provided by the settings module
* @param {Function} callback
*/
exports.init = function(callback)
{
//initalize the database async
db.init(function(err)
{
//there was an error while initializing the database, output it and stop
if(err)
{
console.error("ERROR: Problem while initalizing the database");
console.error(err.stack ? err.stack : err);
process.exit(1);
}
//everything ok
else
{
exports.db = db;
callback(null);
}
});
}

263
src/node/db/GroupManager.js Normal file
View file

@ -0,0 +1,263 @@
/**
* The Group Manager provides functions to manage groups in the database
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var randomString = CommonCode.require('/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
exports.deleteGroup = function(groupID, callback)
{
var group;
async.series([
//ensure group exists
function (callback)
{
//try to get the group entry
db.get("group:" + groupID, function (err, _group)
{
if(ERR(err, callback)) return;
//group does not exist
if(_group == null)
{
callback(new customError("groupID does not exist","apierror"));
}
//group exists, everything is fine
else
{
group = _group;
callback();
}
});
},
//iterate trough all pads of this groups and delete them
function(callback)
{
//collect all padIDs in an array, that allows us to use async.forEach
var padIDs = [];
for(var i in group.pads)
{
padIDs.push(i);
}
//loop trough all pads and delete them
async.forEach(padIDs, function(padID, callback)
{
padManager.getPad(padID, function(err, pad)
{
if(ERR(err, callback)) return;
pad.remove(callback);
});
}, callback);
},
//iterate trough group2sessions and delete all sessions
function(callback)
{
//try to get the group entry
db.get("group2sessions:" + groupID, function (err, group2sessions)
{
if(ERR(err, callback)) return;
//skip if there is no group2sessions entry
if(group2sessions == null) {callback(); return}
//collect all sessions in an array, that allows us to use async.forEach
var sessions = [];
for(var i in group2sessions.sessionsIDs)
{
sessions.push(i);
}
//loop trough all sessions and delete them
async.forEach(sessions, function(session, callback)
{
sessionManager.deleteSession(session, callback);
}, callback);
});
},
//remove group and group2sessions entry
function(callback)
{
db.remove("group2sessions:" + groupID);
db.remove("group:" + groupID);
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback();
});
}
exports.doesGroupExist = function(groupID, callback)
{
//try to get the group entry
db.get("group:" + groupID, function (err, group)
{
if(ERR(err, callback)) return;
callback(null, group != null);
});
}
exports.createGroup = function(callback)
{
//search for non existing groupID
var groupID = "g." + randomString(16);
//create the group
db.set("group:" + groupID, {pads: {}});
callback(null, {groupID: groupID});
}
exports.createGroupIfNotExistsFor = function(groupMapper, callback)
{
//ensure mapper is optional
if(typeof groupMapper != "string")
{
callback(new customError("groupMapper is no string","apierror"));
return;
}
//try to get a group for this mapper
db.get("mapper2group:"+groupMapper, function(err, groupID)
{
if(ERR(err, callback)) return;
//there is no group for this mapper, let's create a group
if(groupID == null)
{
exports.createGroup(function(err, responseObj)
{
if(ERR(err, callback)) return;
//create the mapper entry for this group
db.set("mapper2group:"+groupMapper, responseObj.groupID);
callback(null, responseObj);
});
}
//there is a group for this mapper, let's return it
else
{
if(ERR(err, callback)) return;
callback(null, {groupID: groupID});
}
});
}
exports.createGroupPad = function(groupID, padName, text, callback)
{
//create the padID
var padID = groupID + "$" + padName;
async.series([
//ensure group exists
function (callback)
{
exports.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
}
//group exists, everything is fine
else
{
callback();
}
});
},
//ensure pad does not exists
function (callback)
{
padManager.doesPadExists(padID, function(err, exists)
{
if(ERR(err, callback)) return;
//pad exists already
if(exists == true)
{
callback(new customError("padName does already exist","apierror"));
}
//pad does not exist, everything is fine
else
{
callback();
}
});
},
//create the pad
function (callback)
{
padManager.getPad(padID, text, function(err)
{
if(ERR(err, callback)) return;
callback();
});
},
//create an entry in the group for this pad
function (callback)
{
db.setSub("group:" + groupID, ["pads", padID], 1);
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, {padID: padID});
});
}
exports.listPads = function(groupID, callback)
{
exports.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
}
//group exists, let's get the pads
else
{
db.getSub("group:" + groupID, ["pads"], function(err, result)
{
if(ERR(err, callback)) return;
var pads = [];
for ( var padId in result ) {
pads.push(padId);
}
callback(null, {padIDs: pads});
});
}
});
}

488
src/node/db/Pad.js Normal file
View file

@ -0,0 +1,488 @@
/**
* The pad object, defined with joose
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var Changeset = CommonCode.require("/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
var randomString = CommonCode.require('/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var settings = require('../utils/Settings');
var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var padMessageHandler = require("../handler/PadMessageHandler");
var readOnlyManager = require("./ReadOnlyManager");
var crypto = require("crypto");
/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces
* @param txt
*/
exports.cleanText = function (txt) {
return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
};
var Pad = function Pad(id) {
this.atext = Changeset.makeAText("\n");
this.pool = AttributePoolFactory.createAttributePool();
this.head = -1;
this.chatHead = -1;
this.publicStatus = false;
this.passwordHash = null;
this.id = id;
};
exports.Pad = Pad;
Pad.prototype.apool = function apool() {
return this.pool;
};
Pad.prototype.getHeadRevisionNumber = function getHeadRevisionNumber() {
return this.head;
};
Pad.prototype.getPublicStatus = function getPublicStatus() {
return this.publicStatus;
};
Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
if(!author)
author = '';
var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
Changeset.copyAText(newAText, this.atext);
var newRev = ++this.head;
var newRevData = {};
newRevData.changeset = aChangeset;
newRevData.meta = {};
newRevData.meta.author = author;
newRevData.meta.timestamp = new Date().getTime();
//ex. getNumForAuthor
if(author != '')
this.pool.putAttrib(['author', author || '']);
if(newRev % 100 == 0)
{
newRevData.meta.atext = this.atext;
}
db.set("pad:"+this.id+":revs:"+newRev, newRevData);
db.set("pad:"+this.id, {atext: this.atext,
pool: this.pool.toJsonable(),
head: this.head,
chatHead: this.chatHead,
publicStatus: this.publicStatus,
passwordHash: this.passwordHash});
};
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
};
Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback);
};
Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
};
Pad.prototype.getAllAuthors = function getAllAuthors() {
var authors = [];
for(key in this.pool.numToAttrib)
{
if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "")
{
authors.push(this.pool.numToAttrib[key][1]);
}
}
return authors;
};
Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targetRev, callback) {
var _this = this;
var keyRev = this.getKeyRevisionNumber(targetRev);
var atext;
var changesets = [];
//find out which changesets are needed
var neededChangesets = [];
var curRev = keyRev;
while (curRev < targetRev)
{
curRev++;
neededChangesets.push(curRev);
}
async.series([
//get all needed data out of the database
function(callback)
{
async.parallel([
//get the atext of the key revision
function (callback)
{
db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext)
{
if(ERR(err, callback)) return;
atext = Changeset.cloneAText(_atext);
callback();
});
},
//get all needed changesets
function (callback)
{
async.forEach(neededChangesets, function(item, callback)
{
_this.getRevisionChangeset(item, function(err, changeset)
{
if(ERR(err, callback)) return;
changesets[item] = changeset;
callback();
});
}, callback);
}
], callback);
},
//apply all changesets to the key changeset
function(callback)
{
var apool = _this.apool();
var curRev = keyRev;
while (curRev < targetRev)
{
curRev++;
var cs = changesets[curRev];
atext = Changeset.applyToAText(cs, atext, apool);
}
callback(null);
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, atext);
});
};
Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) {
return Math.floor(revNum / 100) * 100;
};
Pad.prototype.text = function text() {
return this.atext.text;
};
Pad.prototype.setText = function setText(newText) {
//clean the new text
newText = exports.cleanText(newText);
var oldText = this.text();
//create the changeset
var changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText);
//append the changeset
this.appendRevision(changeset);
};
Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) {
this.chatHead++;
//save the chat entry in the database
db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
//save the new chat head
db.setSub("pad:"+this.id, ["chatHead"], this.chatHead);
};
Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) {
var _this = this;
var entry;
async.series([
//get the chat entry
function(callback)
{
db.get("pad:"+_this.id+":chat:"+entryNum, function(err, _entry)
{
if(ERR(err, callback)) return;
entry = _entry;
callback();
});
},
//add the authorName
function(callback)
{
//this chat message doesn't exist, return null
if(entry == null)
{
callback();
return;
}
//get the authorName
authorManager.getAuthorName(entry.userId, function(err, authorName)
{
if(ERR(err, callback)) return;
entry.userName = authorName;
callback();
});
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, entry);
});
};
Pad.prototype.getLastChatMessages = function getLastChatMessages(count, callback) {
//return an empty array if there are no chat messages
if(this.chatHead == -1)
{
callback(null, []);
return;
}
var _this = this;
//works only if we decrement the amount, for some reason
count--;
//set the startpoint
var start = this.chatHead-count;
if(start < 0)
start = 0;
//set the endpoint
var end = this.chatHead;
//collect the numbers of chat entries and in which order we need them
var neededEntries = [];
var order = 0;
for(var i=start;i<=end; i++)
{
neededEntries.push({entryNum:i, order: order});
order++;
}
//get all entries out of the database
var entries = [];
async.forEach(neededEntries, function(entryObject, callback)
{
_this.getChatMessage(entryObject.entryNum, function(err, entry)
{
if(ERR(err, callback)) return;
entries[entryObject.order] = entry;
callback();
});
}, function(err)
{
if(ERR(err, callback)) return;
//sort out broken chat entries
//it looks like in happend in the past that the chat head was
//incremented, but the chat message wasn't added
var cleanedEntries = [];
for(var i=0;i<entries.length;i++)
{
if(entries[i]!=null)
cleanedEntries.push(entries[i]);
else
console.warn("WARNING: Found broken chat entry in pad " + _this.id);
}
callback(null, cleanedEntries);
});
};
Pad.prototype.init = function init(text, callback) {
var _this = this;
//replace text with default text if text isn't set
if(text == null)
{
text = settings.defaultPadText;
}
//try to load the pad
db.get("pad:"+this.id, function(err, value)
{
if(ERR(err, callback)) return;
//if this pad exists, load it
if(value != null)
{
_this.head = value.head;
_this.atext = value.atext;
_this.pool = _this.pool.fromJsonable(value.pool);
//ensure we have a local chatHead variable
if(value.chatHead != null)
_this.chatHead = value.chatHead;
else
_this.chatHead = -1;
//ensure we have a local publicStatus variable
if(value.publicStatus != null)
_this.publicStatus = value.publicStatus;
else
_this.publicStatus = false;
//ensure we have a local passwordHash variable
if(value.passwordHash != null)
_this.passwordHash = value.passwordHash;
else
_this.passwordHash = null;
}
//this pad doesn't exist, so create it
else
{
var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
_this.appendRevision(firstChangeset, '');
}
callback(null);
});
};
Pad.prototype.remove = function remove(callback) {
var padID = this.id;
var _this = this;
//kick everyone from this pad
padMessageHandler.kickSessionsFromPad(padID);
async.series([
//delete all relations
function(callback)
{
async.parallel([
//is it a group pad? -> delete the entry of this pad in the group
function(callback)
{
//is it a group pad?
if(padID.indexOf("$")!=-1)
{
var groupID = padID.substring(0,padID.indexOf("$"));
db.get("group:" + groupID, function (err, group)
{
if(ERR(err, callback)) return;
//remove the pad entry
delete group.pads[padID];
//set the new value
db.set("group:" + groupID, group);
callback();
});
}
//its no group pad, nothing to do here
else
{
callback();
}
},
//remove the readonly entries
function(callback)
{
readOnlyManager.getReadOnlyId(padID, function(err, readonlyID)
{
if(ERR(err, callback)) return;
db.remove("pad2readonly:" + padID);
db.remove("readonly2pad:" + readonlyID);
callback();
});
},
//delete all chat messages
function(callback)
{
var chatHead = _this.chatHead;
for(var i=0;i<=chatHead;i++)
{
db.remove("pad:"+padID+":chat:"+i);
}
callback();
},
//delete all revisions
function(callback)
{
var revHead = _this.head;
for(var i=0;i<=revHead;i++)
{
db.remove("pad:"+padID+":revs:"+i);
}
callback();
}
], callback);
},
//delete the pad entry and delete pad from padManager
function(callback)
{
db.remove("pad:"+padID);
padManager.unloadPad(padID);
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback();
});
};
//set in db
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
this.publicStatus = publicStatus;
db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus);
};
Pad.prototype.setPassword = function setPassword(password) {
this.passwordHash = password == null ? null : hash(password, generateSalt());
db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash);
};
Pad.prototype.isCorrectPassword = function isCorrectPassword(password) {
return compare(this.passwordHash, password);
};
Pad.prototype.isPasswordProtected = function isPasswordProtected() {
return this.passwordHash != null;
};
/* Crypto helper methods */
function hash(password, salt)
{
var shasum = crypto.createHash('sha512');
shasum.update(password + salt);
return shasum.digest("hex") + "$" + salt;
}
function generateSalt()
{
return randomString(86);
}
function compare(hashStr, password)
{
return hash(password, hashStr.split("$")[1]) === hashStr;
}

164
src/node/db/PadManager.js Normal file
View file

@ -0,0 +1,164 @@
/**
* The Pad Manager is a Factory for pad Objects
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var Pad = require("../db/Pad").Pad;
var db = require("./DB").db;
/**
* An Object containing all known Pads. Provides "get" and "set" functions,
* which should be used instead of indexing with brackets. These prepend a
* colon to the key, to avoid conflicting with built-in Object methods or with
* these functions themselves.
*
* If this is needed in other places, it would be wise to make this a prototype
* that's defined somewhere more sensible.
*/
var globalPads = {
get: function (name) { return this[':'+name]; },
set: function (name, value) { this[':'+name] = value; },
remove: function (name) { delete this[':'+name]; }
};
/**
* An array of padId transformations. These represent changes in pad name policy over
* time, and allow us to "play back" these changes so legacy padIds can be found.
*/
var padIdTransforms = [
[/\s+/g, '_']
];
/**
* Returns a Pad Object with the callback
* @param id A String with the id of the pad
* @param {Function} callback
*/
exports.getPad = function(id, text, callback)
{
//check if this is a valid padId
if(!exports.isValidPadId(id))
{
callback(new customError(id + " is not a valid padId","apierror"));
return;
}
//make text an optional parameter
if(typeof text == "function")
{
callback = text;
text = null;
}
//check if this is a valid text
if(text != null)
{
//check if text is a string
if(typeof text != "string")
{
callback(new customError("text is not a string","apierror"));
return;
}
//check if text is less than 100k chars
if(text.length > 100000)
{
callback(new customError("text must be less than 100k chars","apierror"));
return;
}
}
var pad = globalPads.get(id);
//return pad if its already loaded
if(pad != null)
{
callback(null, pad);
}
//try to load pad
else
{
pad = new Pad(id);
//initalize the pad
pad.init(text, function(err)
{
if(ERR(err, callback)) return;
globalPads.set(id, pad);
callback(null, pad);
});
}
}
//checks if a pad exists
exports.doesPadExists = function(padId, callback)
{
db.get("pad:"+padId, function(err, value)
{
if(ERR(err, callback)) return;
callback(null, value != null);
});
}
//returns a sanitized padId, respecting legacy pad id formats
exports.sanitizePadId = function(padId, callback) {
var transform_index = arguments[2] || 0;
//we're out of possible transformations, so just return it
if(transform_index >= padIdTransforms.length)
{
callback(padId);
}
//check if padId exists
else
{
exports.doesPadExists(padId, function(junk, exists)
{
if(exists)
{
callback(padId);
}
else
{
//get the next transformation *that's different*
var transformedPadId = padId;
while(transformedPadId == padId && transform_index < padIdTransforms.length)
{
transformedPadId = padId.replace(padIdTransforms[transform_index][0], padIdTransforms[transform_index][1]);
transform_index += 1;
}
//check the next transform
exports.sanitizePadId(transformedPadId, callback, transform_index);
}
});
}
}
exports.isValidPadId = function(padId)
{
return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
}
//removes a pad from the array
exports.unloadPad = function(padId)
{
if(globalPads.get(padId))
globalPads.remove(padId);
}

View file

@ -0,0 +1,74 @@
/**
* The ReadOnlyManager manages the database and rendering releated to read only pads
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
var randomString = CommonCode.require('/pad_utils').randomString;
/**
* returns a read only id for a pad
* @param {String} padId the id of the pad
*/
exports.getReadOnlyId = function (padId, callback)
{
var readOnlyId;
async.waterfall([
//check if there is a pad2readonly entry
function(callback)
{
db.get("pad2readonly:" + padId, callback);
},
function(dbReadOnlyId, callback)
{
//there is no readOnly Entry in the database, let's create one
if(dbReadOnlyId == null)
{
readOnlyId = "r." + randomString(16);
db.set("pad2readonly:" + padId, readOnlyId);
db.set("readonly2pad:" + readOnlyId, padId);
}
//there is a readOnly Entry in the database, let's take this one
else
{
readOnlyId = dbReadOnlyId;
}
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
//return the results
callback(null, readOnlyId);
})
}
/**
* returns a the padId for a read only id
* @param {String} readOnlyId read only id
*/
exports.getPadId = function(readOnlyId, callback)
{
db.get("readonly2pad:" + readOnlyId, callback);
}

View file

@ -0,0 +1,280 @@
/**
* Controls the security of pad access
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
var settings = require("../utils/Settings")
var randomString = CommonCode.require('/pad_utils').randomString;
/**
* This function controlls the access to a pad, it checks if the user can access a pad.
* @param padID the pad the user wants to access
* @param sesssionID the session the user has (set via api)
* @param token the token of the author (randomly generated at client side, used for public pads)
* @param password the password the user has given to access this pad, can be null
* @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
*/
exports.checkAccess = function (padID, sessionID, token, password, callback)
{
var statusObject;
// a valid session is required (api-only mode)
if(settings.requireSession)
{
// no sessionID, access is denied
if(!sessionID)
{
callback(null, {accessStatus: "deny"});
return;
}
}
// a session is not required, so we'll check if it's a public pad
else
{
// it's not a group pad, means we can grant access
if(padID.indexOf("$") == -1)
{
//get author for this token
authorManager.getAuthor4Token(token, function(err, author)
{
if(ERR(err, callback)) return;
// assume user has access
statusObject = {accessStatus: "grant", authorID: author};
// user can't create pads
if(settings.editOnly)
{
// check if pad exists
padManager.doesPadExists(padID, function(err, exists)
{
if(ERR(err, callback)) return;
// pad doesn't exist - user can't have access
if(!exists) statusObject.accessStatus = "deny";
// grant or deny access, with author of token
callback(null, statusObject);
});
}
// user may create new pads - no need to check anything
else
{
// grant access, with author of token
callback(null, statusObject);
}
})
//don't continue
return;
}
}
var groupID = padID.split("$")[0];
var padExists = false;
var validSession = false;
var sessionAuthor;
var tokenAuthor;
var isPublic;
var isPasswordProtected;
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
async.series([
//get basic informations from the database
function(callback)
{
async.parallel([
//does pad exists
function(callback)
{
padManager.doesPadExists(padID, function(err, exists)
{
if(ERR(err, callback)) return;
padExists = exists;
callback();
});
},
//get informations about this session
function(callback)
{
sessionManager.getSessionInfo(sessionID, function(err, sessionInfo)
{
//skip session validation if the session doesn't exists
if(err && err.message == "sessionID does not exist")
{
callback();
return;
}
if(ERR(err, callback)) return;
var now = Math.floor(new Date().getTime()/1000);
//is it for this group? and is validUntil still ok? --> validSession
if(sessionInfo.groupID == groupID && sessionInfo.validUntil > now)
{
validSession = true;
}
sessionAuthor = sessionInfo.authorID;
callback();
});
},
//get author for token
function(callback)
{
//get author for this token
authorManager.getAuthor4Token(token, function(err, author)
{
if(ERR(err, callback)) return;
tokenAuthor = author;
callback();
});
}
], callback);
},
//get more informations of this pad, if avaiable
function(callback)
{
//skip this if the pad doesn't exists
if(padExists == false)
{
callback();
return;
}
padManager.getPad(padID, function(err, pad)
{
if(ERR(err, callback)) return;
//is it a public pad?
isPublic = pad.getPublicStatus();
//is it password protected?
isPasswordProtected = pad.isPasswordProtected();
//is password correct?
if(isPasswordProtected && password && pad.isCorrectPassword(password))
{
passwordStatus = "correct";
}
callback();
});
},
function(callback)
{
//- a valid session for this group is avaible AND pad exists
if(validSession && padExists)
{
//- the pad is not password protected
if(!isPasswordProtected)
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
//- the pad is password protected and password is correct
else if(isPasswordProtected && passwordStatus == "correct")
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
//- the pad is password protected but wrong password given
else if(isPasswordProtected && passwordStatus == "wrong")
{
//--> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"};
}
//- the pad is password protected but no password given
else if(isPasswordProtected && passwordStatus == "notGiven")
{
//--> ask for password
statusObject = {accessStatus: "needPassword"};
}
else
{
throw new Error("Ops, something wrong happend");
}
}
//- a valid session for this group avaible but pad doesn't exists
else if(validSession && !padExists)
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
//--> deny access if user isn't allowed to create the pad
if(settings.editOnly) statusObject.accessStatus = "deny";
}
// there is no valid session avaiable AND pad exists
else if(!validSession && padExists)
{
//-- its public and not password protected
if(isPublic && !isPasswordProtected)
{
//--> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
}
//- its public and password protected and password is correct
else if(isPublic && isPasswordProtected && passwordStatus == "correct")
{
//--> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
}
//- its public and the pad is password protected but wrong password given
else if(isPublic && isPasswordProtected && passwordStatus == "wrong")
{
//--> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"};
}
//- its public and the pad is password protected but no password given
else if(isPublic && isPasswordProtected && passwordStatus == "notGiven")
{
//--> ask for password
statusObject = {accessStatus: "needPassword"};
}
//- its not public
else if(!isPublic)
{
//--> deny access
statusObject = {accessStatus: "deny"};
}
else
{
throw new Error("Ops, something wrong happend");
}
}
// there is no valid session avaiable AND pad doesn't exists
else
{
//--> deny access
statusObject = {accessStatus: "deny"};
}
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, statusObject);
});
}

View file

@ -0,0 +1,367 @@
/**
* The Session Manager provides functions to manage session in the database
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var randomString = CommonCode.require('/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var groupMangager = require("./GroupManager");
var authorMangager = require("./AuthorManager");
exports.doesSessionExist = function(sessionID, callback)
{
//check if the database entry of this session exists
db.get("session:" + sessionID, function (err, session)
{
if(ERR(err, callback)) return;
callback(null, session != null);
});
}
/**
* Creates a new session between an author and a group
*/
exports.createSession = function(groupID, authorID, validUntil, callback)
{
var sessionID;
async.series([
//check if group exists
function(callback)
{
groupMangager.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
}
//everything is fine, continue
else
{
callback();
}
});
},
//check if author exists
function(callback)
{
authorMangager.doesAuthorExists(authorID, function(err, exists)
{
if(ERR(err, callback)) return;
//author does not exist
if(exists == false)
{
callback(new customError("authorID does not exist","apierror"));
}
//everything is fine, continue
else
{
callback();
}
});
},
//check validUntil and create the session db entry
function(callback)
{
//check if rev is a number
if(typeof validUntil != "number")
{
//try to parse the number
if(!isNaN(parseInt(validUntil)))
{
validUntil = parseInt(validUntil);
}
else
{
callback(new customError("validUntil is not a number","apierror"));
return;
}
}
//ensure this is not a negativ number
if(validUntil < 0)
{
callback(new customError("validUntil is a negativ number","apierror"));
return;
}
//ensure this is not a float value
if(!is_int(validUntil))
{
callback(new customError("validUntil is a float value","apierror"));
return;
}
//check if validUntil is in the future
if(Math.floor(new Date().getTime()/1000) > validUntil)
{
callback(new customError("validUntil is in the past","apierror"));
return;
}
//generate sessionID
sessionID = "s." + randomString(16);
//set the session into the database
db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil});
callback();
},
//set the group2sessions entry
function(callback)
{
//get the entry
db.get("group2sessions:" + groupID, function(err, group2sessions)
{
if(ERR(err, callback)) return;
//the entry doesn't exist so far, let's create it
if(group2sessions == null)
{
group2sessions = {sessionIDs : {}};
}
//add the entry for this session
group2sessions.sessionIDs[sessionID] = 1;
//save the new element back
db.set("group2sessions:" + groupID, group2sessions);
callback();
});
},
//set the author2sessions entry
function(callback)
{
//get the entry
db.get("author2sessions:" + authorID, function(err, author2sessions)
{
if(ERR(err, callback)) return;
//the entry doesn't exist so far, let's create it
if(author2sessions == null)
{
author2sessions = {sessionIDs : {}};
}
//add the entry for this session
author2sessions.sessionIDs[sessionID] = 1;
//save the new element back
db.set("author2sessions:" + authorID, author2sessions);
callback();
});
}
], function(err)
{
if(ERR(err, callback)) return;
//return error and sessionID
callback(null, {sessionID: sessionID});
})
}
exports.getSessionInfo = function(sessionID, callback)
{
//check if the database entry of this session exists
db.get("session:" + sessionID, function (err, session)
{
if(ERR(err, callback)) return;
//session does not exists
if(session == null)
{
callback(new customError("sessionID does not exist","apierror"))
}
//everything is fine, return the sessioninfos
else
{
callback(null, session);
}
});
}
/**
* Deletes a session
*/
exports.deleteSession = function(sessionID, callback)
{
var authorID, groupID;
var group2sessions, author2sessions;
async.series([
function(callback)
{
//get the session entry
db.get("session:" + sessionID, function (err, session)
{
if(ERR(err, callback)) return;
//session does not exists
if(session == null)
{
callback(new customError("sessionID does not exist","apierror"))
}
//everything is fine, return the sessioninfos
else
{
authorID = session.authorID;
groupID = session.groupID;
callback();
}
});
},
//get the group2sessions entry
function(callback)
{
db.get("group2sessions:" + groupID, function (err, _group2sessions)
{
if(ERR(err, callback)) return;
group2sessions = _group2sessions;
callback();
});
},
//get the author2sessions entry
function(callback)
{
db.get("author2sessions:" + authorID, function (err, _author2sessions)
{
if(ERR(err, callback)) return;
author2sessions = _author2sessions;
callback();
});
},
//remove the values from the database
function(callback)
{
//remove the session
db.remove("session:" + sessionID);
//remove session from group2sessions
delete group2sessions.sessionIDs[sessionID];
db.set("group2sessions:" + groupID, group2sessions);
//remove session from author2sessions
delete author2sessions.sessionIDs[sessionID];
db.set("author2sessions:" + authorID, author2sessions);
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback();
})
}
exports.listSessionsOfGroup = function(groupID, callback)
{
groupMangager.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
}
//everything is fine, continue
else
{
listSessionsWithDBKey("group2sessions:" + groupID, callback);
}
});
}
exports.listSessionsOfAuthor = function(authorID, callback)
{
authorMangager.doesAuthorExists(authorID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("authorID does not exist","apierror"));
}
//everything is fine, continue
else
{
listSessionsWithDBKey("author2sessions:" + authorID, callback);
}
});
}
//this function is basicly the code listSessionsOfAuthor and listSessionsOfGroup has in common
function listSessionsWithDBKey (dbkey, callback)
{
var sessions;
async.series([
function(callback)
{
//get the group2sessions entry
db.get(dbkey, function(err, sessionObject)
{
if(ERR(err, callback)) return;
sessions = sessionObject ? sessionObject.sessionIDs : null;
callback();
});
},
function(callback)
{
//collect all sessionIDs in an arrary
var sessionIDs = [];
for (var i in sessions)
{
sessionIDs.push(i);
}
//foreach trough the sessions and get the sessioninfos
async.forEach(sessionIDs, function(sessionID, callback)
{
exports.getSessionInfo(sessionID, function(err, sessionInfo)
{
if(ERR(err, callback)) return;
sessions[sessionID] = sessionInfo;
callback();
});
}, callback);
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, sessions);
});
}
//checks if a number is an int
function is_int(value)
{
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
}

949
src/node/easysync_tests.js Normal file
View file

@ -0,0 +1,949 @@
/**
* I found this tests in the old Etherpad and used it to test if the Changeset library can be run on node.js.
* It has no use for ep-lite, but I thought I keep it cause it may help someone to understand the Changeset library
* https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2_tests.js
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('./utils/common_code');
var Changeset = CommonCode.require("/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
function random() {
this.nextInt = function (maxValue) {
return Math.floor(Math.random() * maxValue);
}
this.nextDouble = function (maxValue) {
return Math.random();
}
}
function runTests() {
function print(str) {
console.log(str);
}
function assert(code, optMsg) {
if (!eval(code)) throw new Error("FALSE: " + (optMsg || code));
}
function literal(v) {
if ((typeof v) == "string") {
return '"' + v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n') + '"';
} else
return JSON.stringify(v);
}
function assertEqualArrays(a, b) {
assert("JSON.stringify(" + literal(a) + ") == JSON.stringify(" + literal(b) + ")");
}
function assertEqualStrings(a, b) {
assert(literal(a) + " == " + literal(b));
}
function throughIterator(opsStr) {
var iter = Changeset.opIterator(opsStr);
var assem = Changeset.opAssembler();
while (iter.hasNext()) {
assem.append(iter.next());
}
return assem.toString();
}
function throughSmartAssembler(opsStr) {
var iter = Changeset.opIterator(opsStr);
var assem = Changeset.smartOpAssembler();
while (iter.hasNext()) {
assem.append(iter.next());
}
assem.endDocument();
return assem.toString();
}
(function () {
print("> throughIterator");
var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
assert("throughIterator(" + literal(x) + ") == " + literal(x));
})();
(function () {
print("> throughSmartAssembler");
var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
assert("throughSmartAssembler(" + literal(x) + ") == " + literal(x));
})();
function applyMutations(mu, arrayOfArrays) {
arrayOfArrays.forEach(function (a) {
var result = mu[a[0]].apply(mu, a.slice(1));
if (a[0] == 'remove' && a[3]) {
assertEqualStrings(a[3], result);
}
});
}
function mutationsToChangeset(oldLen, arrayOfArrays) {
var assem = Changeset.smartOpAssembler();
var op = Changeset.newOp();
var bank = Changeset.stringAssembler();
var oldPos = 0;
var newLen = 0;
arrayOfArrays.forEach(function (a) {
if (a[0] == 'skip') {
op.opcode = '=';
op.chars = a[1];
op.lines = (a[2] || 0);
assem.append(op);
oldPos += op.chars;
newLen += op.chars;
} else if (a[0] == 'remove') {
op.opcode = '-';
op.chars = a[1];
op.lines = (a[2] || 0);
assem.append(op);
oldPos += op.chars;
} else if (a[0] == 'insert') {
op.opcode = '+';
bank.append(a[1]);
op.chars = a[1].length;
op.lines = (a[2] || 0);
assem.append(op);
newLen += op.chars;
}
});
newLen += oldLen - oldPos;
assem.endDocument();
return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString());
}
function runMutationTest(testId, origLines, muts, correct) {
print("> runMutationTest#" + testId);
var lines = origLines.slice();
var mu = Changeset.textLinesMutator(lines);
applyMutations(mu, muts);
mu.close();
assertEqualArrays(correct, lines);
var inText = origLines.join('');
var cs = mutationsToChangeset(inText.length, muts);
lines = origLines.slice();
Changeset.mutateTextLines(cs, lines);
assertEqualArrays(correct, lines);
var correctText = correct.join('');
//print(literal(cs));
var outText = Changeset.applyToText(cs, inText);
assertEqualStrings(correctText, outText);
}
runMutationTest(1, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
['remove', 1, 0, "a"],
['insert', "tu"],
['remove', 1, 0, "p"],
['skip', 4, 1],
['skip', 7, 1],
['insert', "cream\npie\n", 2],
['skip', 2],
['insert', "bot"],
['insert', "\n", 1],
['insert', "bu"],
['skip', 3],
['remove', 3, 1, "ge\n"],
['remove', 6, 0, "duffle"]
], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
runMutationTest(2, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
['remove', 1, 0, "a"],
['remove', 1, 0, "p"],
['insert', "tu"],
['skip', 11, 2],
['insert', "cream\npie\n", 2],
['skip', 2],
['insert', "bot"],
['insert', "\n", 1],
['insert', "bu"],
['skip', 3],
['remove', 3, 1, "ge\n"],
['remove', 6, 0, "duffle"]
], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
runMutationTest(3, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
['remove', 6, 1, "apple\n"],
['skip', 15, 2],
['skip', 6],
['remove', 1, 1, "\n"],
['remove', 8, 0, "eggplant"],
['skip', 1, 1]
], ["banana\n", "cabbage\n", "duffle\n"]);
runMutationTest(4, ["15\n"], [
['skip', 1],
['insert', "\n2\n3\n4\n", 4],
['skip', 2, 1]
], ["1\n", "2\n", "3\n", "4\n", "5\n"]);
runMutationTest(5, ["1\n", "2\n", "3\n", "4\n", "5\n"], [
['skip', 1],
['remove', 7, 4, "\n2\n3\n4\n"],
['skip', 2, 1]
], ["15\n"]);
runMutationTest(6, ["123\n", "abc\n", "def\n", "ghi\n", "xyz\n"], [
['insert', "0"],
['skip', 4, 1],
['skip', 4, 1],
['remove', 8, 2, "def\nghi\n"],
['skip', 4, 1]
], ["0123\n", "abc\n", "xyz\n"]);
runMutationTest(7, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
['remove', 6, 1, "apple\n"],
['skip', 15, 2, true],
['skip', 6, 0, true],
['remove', 1, 1, "\n"],
['remove', 8, 0, "eggplant"],
['skip', 1, 1, true]
], ["banana\n", "cabbage\n", "duffle\n"]);
function poolOrArray(attribs) {
if (attribs.getAttrib) {
return attribs; // it's already an attrib pool
} else {
// assume it's an array of attrib strings to be split and added
var p = AttributePoolFactory.createAttributePool();
attribs.forEach(function (kv) {
p.putAttrib(kv.split(','));
});
return p;
}
}
function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) {
print("> applyToAttribution#" + testId);
var p = poolOrArray(attribs);
var result = Changeset.applyToAttribution(
Changeset.checkRep(cs), inAttr, p);
assertEqualStrings(outCorrect, result);
}
// turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n
runApplyToAttributionTest(1, ['bold,', 'bold,true'], "Z:7>3-1*0=1*1=1=3+4$abcd", "+1*1+1|1+5", "+1*1+1|1+8");
// turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n"
runApplyToAttributionTest(2, ['bold,', 'bold,true'], "Z:g<4*1|1=6*1=5-4$", "|2+g", "*1|1+6*1+5|1+1");
(function () {
print("> mutatorHasMore");
var lines = ["1\n", "2\n", "3\n", "4\n"];
var mu;
mu = Changeset.textLinesMutator(lines);
assert(mu.hasMore() + ' == true');
mu.skip(8, 4);
assert(mu.hasMore() + ' == false');
mu.close();
assert(mu.hasMore() + ' == false');
// still 1,2,3,4
mu = Changeset.textLinesMutator(lines);
assert(mu.hasMore() + ' == true');
mu.remove(2, 1);
assert(mu.hasMore() + ' == true');
mu.skip(2, 1);
assert(mu.hasMore() + ' == true');
mu.skip(2, 1);
assert(mu.hasMore() + ' == true');
mu.skip(2, 1);
assert(mu.hasMore() + ' == false');
mu.insert("5\n", 1);
assert(mu.hasMore() + ' == false');
mu.close();
assert(mu.hasMore() + ' == false');
// 2,3,4,5 now
mu = Changeset.textLinesMutator(lines);
assert(mu.hasMore() + ' == true');
mu.remove(6, 3);
assert(mu.hasMore() + ' == true');
mu.remove(2, 1);
assert(mu.hasMore() + ' == false');
mu.insert("hello\n", 1);
assert(mu.hasMore() + ' == false');
mu.close();
assert(mu.hasMore() + ' == false');
})();
function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) {
print("> runMutateAttributionTest#" + testId);
var p = poolOrArray(attribs);
var alines2 = Array.prototype.slice.call(alines);
var result = Changeset.mutateAttributionLines(
Changeset.checkRep(cs), alines2, p);
assertEqualArrays(outCorrect, alines2);
print("> runMutateAttributionTest#" + testId + ".applyToAttribution");
function removeQuestionMarks(a) {
return a.replace(/\?/g, '');
}
var inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks));
var correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks));
var mergedResult = Changeset.applyToAttribution(cs, inMerged, p);
assertEqualStrings(correctMerged, mergedResult);
}
// turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n
runMutateAttributionTest(1, ["bold,true"], "Z:c>0|1=4=1*0=1$", ["|1+4", "|1+4", "|1+4"], ["|1+4", "+1*0+1|1+2", "|1+4"]);
// make a document bold
runMutateAttributionTest(2, ["bold,true"], "Z:c>0*0|3=c$", ["|1+4", "|1+4", "|1+4"], ["*0|1+4", "*0|1+4", "*0|1+4"]);
// clear bold on document
runMutateAttributionTest(3, ["bold,", "bold,true"], "Z:c>0*0|3=c$", ["*1+1+1*1+1|1+1", "+1*1+1|1+2", "*1+1+1*1+1|1+1"], ["|1+4", "|1+4", "|1+4"]);
// add a character on line 3 of a document with 5 blank lines, and make sure
// the optimization that skips purely-kept lines is working; if any attribution string
// with a '?' is parsed it will cause an error.
runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]);
var testPoolWithChars = (function () {
var p = AttributePoolFactory.createAttributePool();
p.putAttrib(['char', 'newline']);
for (var i = 1; i < 36; i++) {
p.putAttrib(['char', Changeset.numToString(i)]);
}
p.putAttrib(['char', '']);
return p;
})();
// based on runMutationTest#1
runMutateAttributionTest(5, testPoolWithChars, "Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$" + "tucream\npie\nbot\nbu", ["*a+1*p+2*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1", "*d+1*u+1*f+2*l+1*e+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"], ["*t+1*u+1*p+1*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "|1+6", "|1+4", "*c+1*a+1*b+1*o+1*t+1*0|1+1", "*b+1*u+1*b+2*a+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"]);
// based on runMutationTest#3
runMutateAttributionTest(6, testPoolWithChars, "Z:11<f|1-6|2=f=6|1-1-8$", ["*a|1+6", "*b|1+7", "*c|1+8", "*d|1+7", "*e|1+9"], ["*b|1+7", "*c|1+8", "*d+6*e|1+1"]);
// based on runMutationTest#4
runMutateAttributionTest(7, testPoolWithChars, "Z:3>7=1|4+7$\n2\n3\n4\n", ["*1+1*5|1+2"], ["*1+1|1+1", "|1+2", "|1+2", "|1+2", "*5|1+2"]);
// based on runMutationTest#5
runMutateAttributionTest(8, testPoolWithChars, "Z:a<7=1|4-7$", ["*1|1+2", "*2|1+2", "*3|1+2", "*4|1+2", "*5|1+2"], ["*1+1*5|1+2"]);
// based on runMutationTest#6
runMutateAttributionTest(9, testPoolWithChars, "Z:k<7*0+1*10|2=8|2-8$0", ["*1+1*2+1*3+1|1+1", "*a+1*b+1*c+1|1+1", "*d+1*e+1*f+1|1+1", "*g+1*h+1*i+1|1+1", "?*x+1*y+1*z+1|1+1"], ["*0+1|1+4", "|1+4", "?*x+1*y+1*z+1|1+1"]);
runMutateAttributionTest(10, testPoolWithChars, "Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd", ["|1+3", "|1+3"], ["|1+5", "+2*0+1|1+2"]);
runMutateAttributionTest(11, testPoolWithChars, "Z:s>1|1=4=6|1+1$\n", ["*0|1+4", "*0|1+8", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"], ["*0|1+4", "*0+6|1+1", "*0|1+2", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"]);
function randomInlineString(len, rand) {
var assem = Changeset.stringAssembler();
for (var i = 0; i < len; i++) {
assem.append(String.fromCharCode(rand.nextInt(26) + 97));
}
return assem.toString();
}
function randomMultiline(approxMaxLines, approxMaxCols, rand) {
var numParts = rand.nextInt(approxMaxLines * 2) + 1;
var txt = Changeset.stringAssembler();
txt.append(rand.nextInt(2) ? '\n' : '');
for (var i = 0; i < numParts; i++) {
if ((i % 2) == 0) {
if (rand.nextInt(10)) {
txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand));
} else {
txt.append('\n');
}
} else {
txt.append('\n');
}
}
return txt.toString();
}
function randomStringOperation(numCharsLeft, rand) {
var result;
switch (rand.nextInt(9)) {
case 0:
{
// insert char
result = {
insert: randomInlineString(1, rand)
};
break;
}
case 1:
{
// delete char
result = {
remove: 1
};
break;
}
case 2:
{
// skip char
result = {
skip: 1
};
break;
}
case 3:
{
// insert small
result = {
insert: randomInlineString(rand.nextInt(4) + 1, rand)
};
break;
}
case 4:
{
// delete small
result = {
remove: rand.nextInt(4) + 1
};
break;
}
case 5:
{
// skip small
result = {
skip: rand.nextInt(4) + 1
};
break;
}
case 6:
{
// insert multiline;
result = {
insert: randomMultiline(5, 20, rand)
};
break;
}
case 7:
{
// delete multiline
result = {
remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
};
break;
}
case 8:
{
// skip multiline
result = {
skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
};
break;
}
case 9:
{
// delete to end
result = {
remove: numCharsLeft
};
break;
}
case 10:
{
// skip to end
result = {
skip: numCharsLeft
};
break;
}
}
var maxOrig = numCharsLeft - 1;
if ('remove' in result) {
result.remove = Math.min(result.remove, maxOrig);
} else if ('skip' in result) {
result.skip = Math.min(result.skip, maxOrig);
}
return result;
}
function randomTwoPropAttribs(opcode, rand) {
// assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
if (opcode == '-' || rand.nextInt(3)) {
return '';
} else if (rand.nextInt(3)) {
if (opcode == '+' || rand.nextInt(2)) {
return '*' + Changeset.numToString(rand.nextInt(2) * 2 + 1);
} else {
return '*' + Changeset.numToString(rand.nextInt(2) * 2);
}
} else {
if (opcode == '+' || rand.nextInt(4) == 0) {
return '*1*3';
} else {
return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)];
}
}
}
function randomTestChangeset(origText, rand, withAttribs) {
var charBank = Changeset.stringAssembler();
var textLeft = origText; // always keep final newline
var outTextAssem = Changeset.stringAssembler();
var opAssem = Changeset.smartOpAssembler();
var oldLen = origText.length;
var nextOp = Changeset.newOp();
function appendMultilineOp(opcode, txt) {
nextOp.opcode = opcode;
if (withAttribs) {
nextOp.attribs = randomTwoPropAttribs(opcode, rand);
}
txt.replace(/\n|[^\n]+/g, function (t) {
if (t == '\n') {
nextOp.chars = 1;
nextOp.lines = 1;
opAssem.append(nextOp);
} else {
nextOp.chars = t.length;
nextOp.lines = 0;
opAssem.append(nextOp);
}
return '';
});
}
function doOp() {
var o = randomStringOperation(textLeft.length, rand);
if (o.insert) {
var txt = o.insert;
charBank.append(txt);
outTextAssem.append(txt);
appendMultilineOp('+', txt);
} else if (o.skip) {
var txt = textLeft.substring(0, o.skip);
textLeft = textLeft.substring(o.skip);
outTextAssem.append(txt);
appendMultilineOp('=', txt);
} else if (o.remove) {
var txt = textLeft.substring(0, o.remove);
textLeft = textLeft.substring(o.remove);
appendMultilineOp('-', txt);
}
}
while (textLeft.length > 1) doOp();
for (var i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
var outText = outTextAssem.toString() + '\n';
opAssem.endDocument();
var cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString());
Changeset.checkRep(cs);
return [cs, outText];
}
function testCompose(randomSeed) {
var rand = new random();
print("> testCompose#" + randomSeed);
var p = AttributePoolFactory.createAttributePool();
var startText = randomMultiline(10, 20, rand) + '\n';
var x1 = randomTestChangeset(startText, rand);
var change1 = x1[0];
var text1 = x1[1];
var x2 = randomTestChangeset(text1, rand);
var change2 = x2[0];
var text2 = x2[1];
var x3 = randomTestChangeset(text2, rand);
var change3 = x3[0];
var text3 = x3[1];
//print(literal(Changeset.toBaseTen(startText)));
//print(literal(Changeset.toBaseTen(change1)));
//print(literal(Changeset.toBaseTen(change2)));
var change12 = Changeset.checkRep(Changeset.compose(change1, change2, p));
var change23 = Changeset.checkRep(Changeset.compose(change2, change3, p));
var change123 = Changeset.checkRep(Changeset.compose(change12, change3, p));
var change123a = Changeset.checkRep(Changeset.compose(change1, change23, p));
assertEqualStrings(change123, change123a);
assertEqualStrings(text2, Changeset.applyToText(change12, startText));
assertEqualStrings(text3, Changeset.applyToText(change23, text1));
assertEqualStrings(text3, Changeset.applyToText(change123, startText));
}
for (var i = 0; i < 30; i++) testCompose(i);
(function simpleComposeAttributesTest() {
print("> simpleComposeAttributesTest");
var p = AttributePoolFactory.createAttributePool();
p.putAttrib(['bold', '']);
p.putAttrib(['bold', 'true']);
var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x");
var cs2 = Changeset.checkRep("Z:3>0*0|1=3$");
var cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p));
assertEqualStrings("Z:2>1+1*0|1=2$x", cs12);
})();
(function followAttributesTest() {
var p = AttributePoolFactory.createAttributePool();
p.putAttrib(['x', '']);
p.putAttrib(['x', 'abc']);
p.putAttrib(['x', 'def']);
p.putAttrib(['y', '']);
p.putAttrib(['y', 'abc']);
p.putAttrib(['y', 'def']);
function testFollow(a, b, afb, bfa, merge) {
assertEqualStrings(afb, Changeset.followAttributes(a, b, p));
assertEqualStrings(bfa, Changeset.followAttributes(b, a, p));
assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p));
assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p));
}
testFollow('', '', '', '', '');
testFollow('*0', '', '', '*0', '*0');
testFollow('*0', '*0', '', '', '*0');
testFollow('*0', '*1', '', '*0', '*0');
testFollow('*1', '*2', '', '*1', '*1');
testFollow('*0*1', '', '', '*0*1', '*0*1');
testFollow('*0*4', '*2*3', '*3', '*0', '*0*3');
testFollow('*0*4', '*2', '', '*0*4', '*0*4');
})();
function testFollow(randomSeed) {
var rand = new random();
print("> testFollow#" + randomSeed);
var p = AttributePoolFactory.createAttributePool();
var startText = randomMultiline(10, 20, rand) + '\n';
var cs1 = randomTestChangeset(startText, rand)[0];
var cs2 = randomTestChangeset(startText, rand)[0];
var afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p));
var bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p));
var merge1 = Changeset.checkRep(Changeset.compose(cs1, afb));
var merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa));
assertEqualStrings(merge1, merge2);
}
for (var i = 0; i < 30; i++) testFollow(i);
function testSplitJoinAttributionLines(randomSeed) {
var rand = new random();
print("> testSplitJoinAttributionLines#" + randomSeed);
var doc = randomMultiline(10, 20, rand) + '\n';
function stringToOps(str) {
var assem = Changeset.mergingOpAssembler();
var o = Changeset.newOp('+');
o.chars = 1;
for (var i = 0; i < str.length; i++) {
var c = str.charAt(i);
o.lines = (c == '\n' ? 1 : 0);
o.attribs = (c == 'a' || c == 'b' ? '*' + c : '');
assem.append(o);
}
return assem.toString();
}
var theJoined = stringToOps(doc);
var theSplit = doc.match(/[^\n]*\n/g).map(stringToOps);
assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc));
assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit));
}
for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i);
(function testMoveOpsToNewPool() {
print("> testMoveOpsToNewPool");
var pool1 = AttributePoolFactory.createAttributePool();
var pool2 = AttributePoolFactory.createAttributePool();
pool1.putAttrib(['baz', 'qux']);
pool1.putAttrib(['foo', 'bar']);
pool2.putAttrib(['foo', 'bar']);
assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab');
assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1');
})();
(function testMakeSplice() {
print("> testMakeSplice");
var t = "a\nb\nc\n";
var t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, "def"), t);
assertEqualStrings("a\nb\ncdef\n", t2);
})();
(function testToSplices() {
print("> testToSplices");
var cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk');
var correctSplices = [
[5, 8, "123456789"],
[9, 17, "abcdefghijk"]
];
assertEqualArrays(correctSplices, Changeset.toSplices(cs));
})();
function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) {
print("> testCharacterRangeFollow#" + testId);
var cs = Changeset.checkRep(cs);
assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter));
}
testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [7, 10], false, [14, 15]);
testCharacterRangeFollow(2, "Z:bc<6|x=b4|2-6$", [400, 407], false, [400, 401]);
testCharacterRangeFollow(3, "Z:4>0-3+3$abc", [0, 3], false, [3, 3]);
testCharacterRangeFollow(4, "Z:4>0-3+3$abc", [0, 3], true, [0, 0]);
testCharacterRangeFollow(5, "Z:5>1+1=1-3+3$abcd", [1, 4], false, [5, 5]);
testCharacterRangeFollow(6, "Z:5>1+1=1-3+3$abcd", [1, 4], true, [2, 2]);
testCharacterRangeFollow(7, "Z:5>1+1=1-3+3$abcd", [0, 6], false, [1, 7]);
testCharacterRangeFollow(8, "Z:5>1+1=1-3+3$abcd", [0, 3], false, [1, 2]);
testCharacterRangeFollow(9, "Z:5>1+1=1-3+3$abcd", [2, 5], false, [5, 6]);
testCharacterRangeFollow(10, "Z:2>1+1$a", [0, 0], false, [1, 1]);
testCharacterRangeFollow(11, "Z:2>1+1$a", [0, 0], true, [0, 0]);
(function testOpAttributeValue() {
print("> testOpAttributeValue");
var p = AttributePoolFactory.createAttributePool();
p.putAttrib(['name', 'david']);
p.putAttrib(['color', 'green']);
assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p));
assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p));
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p));
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p));
assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p));
assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p));
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p));
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p));
})();
function testAppendATextToAssembler(testId, atext, correctOps) {
print("> testAppendATextToAssembler#" + testId);
var assem = Changeset.smartOpAssembler();
Changeset.appendATextToAssembler(atext, assem);
assertEqualStrings(correctOps, assem.toString());
}
testAppendATextToAssembler(1, {
text: "\n",
attribs: "|1+1"
}, "");
testAppendATextToAssembler(2, {
text: "\n\n",
attribs: "|2+2"
}, "|1+1");
testAppendATextToAssembler(3, {
text: "\n\n",
attribs: "*x|2+2"
}, "*x|1+1");
testAppendATextToAssembler(4, {
text: "\n\n",
attribs: "*x|1+1|1+1"
}, "*x|1+1");
testAppendATextToAssembler(5, {
text: "foo\n",
attribs: "|1+4"
}, "+3");
testAppendATextToAssembler(6, {
text: "\nfoo\n",
attribs: "|2+5"
}, "|1+1+3");
testAppendATextToAssembler(7, {
text: "\nfoo\n",
attribs: "*x|2+5"
}, "*x|1+1*x+3");
testAppendATextToAssembler(8, {
text: "\n\n\nfoo\n",
attribs: "|2+2*x|2+5"
}, "|2+2*x|1+1*x+3");
function testMakeAttribsString(testId, pool, opcode, attribs, correctString) {
print("> testMakeAttribsString#" + testId);
var p = poolOrArray(pool);
var str = Changeset.makeAttribsString(opcode, attribs, p);
assertEqualStrings(correctString, str);
}
testMakeAttribsString(1, ['bold,'], '+', [
['bold', '']
], '');
testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [
['bold', '']
], '*1');
testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [
['abc', 'def'],
['bold', 'true']
], '*0*1');
testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [
['bold', 'true'],
['abc', 'def']
], '*0*1');
function testSubattribution(testId, astr, start, end, correctOutput) {
print("> testSubattribution#" + testId);
var str = Changeset.subattribution(astr, start, end);
assertEqualStrings(correctOutput, str);
}
testSubattribution(1, "+1", 0, 0, "");
testSubattribution(2, "+1", 0, 1, "+1");
testSubattribution(3, "+1", 0, undefined, "+1");
testSubattribution(4, "|1+1", 0, 0, "");
testSubattribution(5, "|1+1", 0, 1, "|1+1");
testSubattribution(6, "|1+1", 0, undefined, "|1+1");
testSubattribution(7, "*0+1", 0, 0, "");
testSubattribution(8, "*0+1", 0, 1, "*0+1");
testSubattribution(9, "*0+1", 0, undefined, "*0+1");
testSubattribution(10, "*0|1+1", 0, 0, "");
testSubattribution(11, "*0|1+1", 0, 1, "*0|1+1");
testSubattribution(12, "*0|1+1", 0, undefined, "*0|1+1");
testSubattribution(13, "*0+2+1*1+3", 0, 1, "*0+1");
testSubattribution(14, "*0+2+1*1+3", 0, 2, "*0+2");
testSubattribution(15, "*0+2+1*1+3", 0, 3, "*0+2+1");
testSubattribution(16, "*0+2+1*1+3", 0, 4, "*0+2+1*1+1");
testSubattribution(17, "*0+2+1*1+3", 0, 5, "*0+2+1*1+2");
testSubattribution(18, "*0+2+1*1+3", 0, 6, "*0+2+1*1+3");
testSubattribution(19, "*0+2+1*1+3", 0, 7, "*0+2+1*1+3");
testSubattribution(20, "*0+2+1*1+3", 0, undefined, "*0+2+1*1+3");
testSubattribution(21, "*0+2+1*1+3", 1, undefined, "*0+1+1*1+3");
testSubattribution(22, "*0+2+1*1+3", 2, undefined, "+1*1+3");
testSubattribution(23, "*0+2+1*1+3", 3, undefined, "*1+3");
testSubattribution(24, "*0+2+1*1+3", 4, undefined, "*1+2");
testSubattribution(25, "*0+2+1*1+3", 5, undefined, "*1+1");
testSubattribution(26, "*0+2+1*1+3", 6, undefined, "");
testSubattribution(27, "*0+2+1*1|1+3", 0, 1, "*0+1");
testSubattribution(28, "*0+2+1*1|1+3", 0, 2, "*0+2");
testSubattribution(29, "*0+2+1*1|1+3", 0, 3, "*0+2+1");
testSubattribution(30, "*0+2+1*1|1+3", 0, 4, "*0+2+1*1+1");
testSubattribution(31, "*0+2+1*1|1+3", 0, 5, "*0+2+1*1+2");
testSubattribution(32, "*0+2+1*1|1+3", 0, 6, "*0+2+1*1|1+3");
testSubattribution(33, "*0+2+1*1|1+3", 0, 7, "*0+2+1*1|1+3");
testSubattribution(34, "*0+2+1*1|1+3", 0, undefined, "*0+2+1*1|1+3");
testSubattribution(35, "*0+2+1*1|1+3", 1, undefined, "*0+1+1*1|1+3");
testSubattribution(36, "*0+2+1*1|1+3", 2, undefined, "+1*1|1+3");
testSubattribution(37, "*0+2+1*1|1+3", 3, undefined, "*1|1+3");
testSubattribution(38, "*0+2+1*1|1+3", 4, undefined, "*1|1+2");
testSubattribution(39, "*0+2+1*1|1+3", 5, undefined, "*1|1+1");
testSubattribution(40, "*0+2+1*1|1+3", 1, 5, "*0+1+1*1+2");
testSubattribution(41, "*0+2+1*1|1+3", 2, 6, "+1*1|1+3");
testSubattribution(42, "*0+2+1*1+3", 2, 6, "+1*1+3");
function testFilterAttribNumbers(testId, cs, filter, correctOutput) {
print("> testFilterAttribNumbers#" + testId);
var str = Changeset.filterAttribNumbers(cs, filter);
assertEqualStrings(correctOutput, str);
}
testFilterAttribNumbers(1, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
return (n % 2) == 0;
}, "*0+1+2+3+4*2+5*0*2*c+6");
testFilterAttribNumbers(2, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
return (n % 2) == 1;
}, "*1+1+2+3*1+4+5*1*b+6");
function testInverse(testId, cs, lines, alines, pool, correctOutput) {
print("> testInverse#" + testId);
pool = poolOrArray(pool);
var str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool);
assertEqualStrings(correctOutput, str);
}
// take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--"
testInverse(1, "Z:9>0=1*0=1*1=1=2*0=2*1|1=2$", null, ["+4*1+5"], ['bold,', 'bold,true'], "Z:9>0=2*0=1=2*1=2$");
function testMutateTextLines(testId, cs, lines, correctLines) {
print("> testMutateTextLines#" + testId);
var a = lines.slice();
Changeset.mutateTextLines(cs, a);
assertEqualArrays(correctLines, a);
}
testMutateTextLines(1, "Z:4<1|1-2-1|1+1+1$\nc", ["a\n", "b\n"], ["\n", "c\n"]);
testMutateTextLines(2, "Z:4>0|1-2-1|2+3$\nc\n", ["a\n", "b\n"], ["\n", "c\n", "\n"]);
function testInverseRandom(randomSeed) {
var rand = new random();
print("> testInverseRandom#" + randomSeed);
var p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']);
var startText = randomMultiline(10, 20, rand) + '\n';
var alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText);
var lines = startText.slice(0, -1).split('\n').map(function (s) {
return s + '\n';
});
var stylifier = randomTestChangeset(startText, rand, true)[0];
//print(alines.join('\n'));
Changeset.mutateAttributionLines(stylifier, alines, p);
//print(stylifier);
//print(alines.join('\n'));
Changeset.mutateTextLines(stylifier, lines);
var changeset = randomTestChangeset(lines.join(''), rand, true)[0];
var inverseChangeset = Changeset.inverse(changeset, lines, alines, p);
var origLines = lines.slice();
var origALines = alines.slice();
Changeset.mutateTextLines(changeset, lines);
Changeset.mutateAttributionLines(changeset, alines, p);
//print(origALines.join('\n'));
//print(changeset);
//print(inverseChangeset);
//print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n'));
//print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n'));
//print(alines.join('\n'));
Changeset.mutateTextLines(inverseChangeset, lines);
Changeset.mutateAttributionLines(inverseChangeset, alines, p);
//print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n'));
assertEqualArrays(origLines, lines);
assertEqualArrays(origALines, alines);
}
for (var i = 0; i < 30; i++) testInverseRandom(i);
}
runTests();

View file

@ -0,0 +1,161 @@
/**
* The API Handler handles all API http requests
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var fs = require("fs");
var api = require("../db/API");
var padManager = require("../db/PadManager");
var randomString = CommonCode.require('/pad_utils').randomString;
//ensure we have an apikey
var apikey = null;
try
{
apikey = fs.readFileSync("../APIKEY.txt","utf8");
}
catch(e)
{
apikey = randomString(32);
fs.writeFileSync("../APIKEY.txt",apikey,"utf8");
}
//a list of all functions
var functions = {
"createGroup" : [],
"createGroupIfNotExistsFor" : ["groupMapper"],
"deleteGroup" : ["groupID"],
"listPads" : ["groupID"],
"createPad" : ["padID", "text"],
"createGroupPad" : ["groupID", "padName", "text"],
"createAuthor" : ["name"],
"createAuthorIfNotExistsFor": ["authorMapper" , "name"],
"createSession" : ["groupID", "authorID", "validUntil"],
"deleteSession" : ["sessionID"],
"getSessionInfo" : ["sessionID"],
"listSessionsOfGroup" : ["groupID"],
"listSessionsOfAuthor" : ["authorID"],
"getText" : ["padID", "rev"],
"setText" : ["padID", "text"],
"getHTML" : ["padID", "rev"],
"setHTML" : ["padID", "html"],
"getRevisionsCount" : ["padID"],
"deletePad" : ["padID"],
"getReadOnlyID" : ["padID"],
"setPublicStatus" : ["padID", "publicStatus"],
"getPublicStatus" : ["padID"],
"setPassword" : ["padID", "password"],
"isPasswordProtected" : ["padID"]
};
/**
* Handles a HTTP API call
* @param functionName the name of the called function
* @param fields the params of the called function
* @req express request object
* @res express response object
*/
exports.handle = function(functionName, fields, req, res)
{
//check the api key!
if(fields["apikey"] != apikey.trim())
{
res.send({code: 4, message: "no or wrong API Key", data: null});
return;
}
//check if this is a valid function name
var isKnownFunctionname = false;
for(var knownFunctionname in functions)
{
if(knownFunctionname == functionName)
{
isKnownFunctionname = true;
break;
}
}
//say goodbye if this is a unkown function
if(!isKnownFunctionname)
{
res.send({code: 3, message: "no such function", data: null});
return;
}
//sanitize any pad id's before continuing
if(fields["padID"])
{
padManager.sanitizePadId(fields["padID"], function(padId)
{
fields["padID"] = padId;
callAPI(functionName, fields, req, res);
});
}
else if(fields["padName"])
{
padManager.sanitizePadId(fields["padName"], function(padId)
{
fields["padName"] = padId;
callAPI(functionName, fields, req, res);
});
}
else
{
callAPI(functionName, fields, req, res);
}
}
//calls the api function
function callAPI(functionName, fields, req, res)
{
//put the function parameters in an array
var functionParams = [];
for(var i=0;i<functions[functionName].length;i++)
{
functionParams.push(fields[functions[functionName][i]]);
}
//add a callback function to handle the response
functionParams.push(function(err, data)
{
// no error happend, everything is fine
if(err == null)
{
if(!data)
data = null;
res.send({code: 0, message: "ok", data: data});
}
// parameters were wrong and the api stopped execution, pass the error
else if(err.name == "apierror")
{
res.send({code: 1, message: err.message, data: null});
}
//an unkown error happend
else
{
res.send({code: 2, message: "internal error", data: null});
ERR(err);
}
});
//call the api function
api[functionName](functionParams[0],functionParams[1],functionParams[2],functionParams[3],functionParams[4]);
}

View file

@ -0,0 +1,165 @@
/**
* Handles the export requests
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var exporthtml = require("../utils/ExportHtml");
var exportdokuwiki = require("../utils/ExportDokuWiki");
var padManager = require("../db/PadManager");
var async = require("async");
var fs = require("fs");
var settings = require('../utils/Settings');
var os = require('os');
//load abiword only if its enabled
if(settings.abiword != null)
var abiword = require("../utils/Abiword");
var tempDirectory = "/tmp";
//tempDirectory changes if the operating system is windows
if(os.type().indexOf("Windows") > -1)
{
tempDirectory = process.env.TEMP;
}
/**
* do a requested export
*/
exports.doExport = function(req, res, padId, type)
{
//tell the browser that this is a downloadable file
res.attachment(padId + "." + type);
//if this is a plain text export, we can do this directly
if(type == "txt")
{
padManager.getPad(padId, function(err, pad)
{
ERR(err);
if(req.params.rev){
pad.getInternalRevisionAText(req.params.rev, function(junk, text)
{
res.send(text.text ? text.text : null);
});
}
else
{
res.send(pad.text());
}
});
}
else if(type == 'dokuwiki')
{
var randNum;
var srcFile, destFile;
async.series([
//render the dokuwiki document
function(callback)
{
exportdokuwiki.getPadDokuWikiDocument(padId, req.params.rev, function(err, dokuwiki)
{
res.send(dokuwiki);
callback("stop");
});
},
], function(err)
{
if(err && err != "stop") throw err;
});
}
else
{
var html;
var randNum;
var srcFile, destFile;
async.series([
//render the html document
function(callback)
{
exporthtml.getPadHTMLDocument(padId, req.params.rev, false, function(err, _html)
{
if(ERR(err, callback)) return;
html = _html;
callback();
});
},
//decide what to do with the html export
function(callback)
{
//if this is a html export, we can send this from here directly
if(type == "html")
{
res.send(html);
callback("stop");
}
else //write the html export to a file
{
randNum = Math.floor(Math.random()*0xFFFFFFFF);
srcFile = tempDirectory + "/eplite_export_" + randNum + ".html";
fs.writeFile(srcFile, html, callback);
}
},
//send the convert job to abiword
function(callback)
{
//ensure html can be collected by the garbage collector
html = null;
destFile = tempDirectory + "/eplite_export_" + randNum + "." + type;
abiword.convertFile(srcFile, destFile, type, callback);
},
//send the file
function(callback)
{
res.sendfile(destFile, null, callback);
},
//clean up temporary files
function(callback)
{
async.parallel([
function(callback)
{
fs.unlink(srcFile, callback);
},
function(callback)
{
//100ms delay to accomidate for slow windows fs
if(os.type().indexOf("Windows") > -1)
{
setTimeout(function()
{
fs.unlink(destFile, callback);
}, 100);
}
else
{
fs.unlink(destFile, callback);
}
}
], callback);
}
], function(err)
{
if(err && err != "stop") ERR(err);
})
}
};

View file

@ -0,0 +1,191 @@
/**
* Handles the import requests
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var padManager = require("../db/PadManager");
var padMessageHandler = require("./PadMessageHandler");
var async = require("async");
var fs = require("fs");
var settings = require('../utils/Settings');
var formidable = require('formidable');
var os = require("os");
//load abiword only if its enabled
if(settings.abiword != null)
var abiword = require("../utils/Abiword");
var tempDirectory = "/tmp/";
//tempDirectory changes if the operating system is windows
if(os.type().indexOf("Windows") > -1)
{
tempDirectory = process.env.TEMP;
}
/**
* do a requested import
*/
exports.doImport = function(req, res, padId)
{
//pipe to a file
//convert file to text via abiword
//set text in the pad
var srcFile, destFile;
var pad;
var text;
async.series([
//save the uploaded file to /tmp
function(callback)
{
var form = new formidable.IncomingForm();
form.keepExtensions = true;
form.uploadDir = tempDirectory;
form.parse(req, function(err, fields, files)
{
//the upload failed, stop at this point
if(err || files.file === undefined)
{
console.warn("Uploading Error: " + err.stack);
callback("uploadFailed");
}
//everything ok, continue
else
{
//save the path of the uploaded file
srcFile = files.file.path;
callback();
}
});
},
//ensure this is a file ending we know, else we change the file ending to .txt
//this allows us to accept source code files like .c or .java
function(callback)
{
var fileEnding = srcFile.split(".")[1].toLowerCase();
var knownFileEndings = ["txt", "doc", "docx", "pdf", "odt", "html", "htm"];
//find out if this is a known file ending
var fileEndingKnown = false;
for(var i in knownFileEndings)
{
if(fileEnding == knownFileEndings[i])
{
fileEndingKnown = true;
}
}
//if the file ending is known, continue as normal
if(fileEndingKnown)
{
callback();
}
//we need to rename this file with a .txt ending
else
{
var oldSrcFile = srcFile;
srcFile = srcFile.split(".")[0] + ".txt";
fs.rename(oldSrcFile, srcFile, callback);
}
},
//convert file to text
function(callback)
{
var randNum = Math.floor(Math.random()*0xFFFFFFFF);
destFile = tempDirectory + "eplite_import_" + randNum + ".txt";
abiword.convertFile(srcFile, destFile, "txt", callback);
},
//get the pad object
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
if(ERR(err, callback)) return;
pad = _pad;
callback();
});
},
//read the text
function(callback)
{
fs.readFile(destFile, "utf8", function(err, _text)
{
if(ERR(err, callback)) return;
text = _text;
//node on windows has a delay on releasing of the file lock.
//We add a 100ms delay to work around this
if(os.type().indexOf("Windows") > -1)
{
setTimeout(function()
{
callback();
}, 100);
}
else
{
callback();
}
});
},
//change text of the pad and broadcast the changeset
function(callback)
{
pad.setText(text);
padMessageHandler.updatePadClients(pad, callback);
},
//clean up temporary files
function(callback)
{
async.parallel([
function(callback)
{
fs.unlink(srcFile, callback);
},
function(callback)
{
fs.unlink(destFile, callback);
}
], callback);
}
], function(err)
{
//the upload failed, there is nothing we can do, send a 500
if(err == "uploadFailed")
{
res.send(500);
return;
}
ERR(err);
//close the connection
res.send("ok");
});
}

View file

@ -0,0 +1,926 @@
/**
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var async = require("async");
var padManager = require("../db/PadManager");
var Changeset = CommonCode.require("/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
var authorManager = require("../db/AuthorManager");
var readOnlyManager = require("../db/ReadOnlyManager");
var settings = require('../utils/Settings');
var securityManager = require("../db/SecurityManager");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
/**
* A associative array that translates a session to a pad
*/
var session2pad = {};
/**
* A associative array that saves which sessions belong to a pad
*/
var pad2sessions = {};
/**
* A associative array that saves some general informations about a session
* key = sessionId
* values = author, rev
* rev = That last revision that was send to this client
* author = the author name of this session
*/
var sessioninfos = {};
/**
* Saves the Socket class we need to send and recieve data from the client
*/
var socketio;
/**
* This Method is called by server.js to tell the message handler on which socket it should send
* @param socket_io The Socket
*/
exports.setSocketIO = function(socket_io)
{
socketio=socket_io;
}
/**
* Handles the connection of a new user
* @param client the new client
*/
exports.handleConnect = function(client)
{
//Initalize session2pad and sessioninfos for this new session
session2pad[client.id]=null;
sessioninfos[client.id]={};
}
/**
* Kicks all sessions from a pad
* @param client the new client
*/
exports.kickSessionsFromPad = function(padID)
{
//skip if there is nobody on this pad
if(!pad2sessions[padID])
return;
//disconnect everyone from this pad
for(var i in pad2sessions[padID])
{
socketio.sockets.sockets[pad2sessions[padID][i]].json.send({disconnect:"deleted"});
}
}
/**
* Handles the disconnection of a user
* @param client the client that leaves
*/
exports.handleDisconnect = function(client)
{
//save the padname of this session
var sessionPad=session2pad[client.id];
//if this connection was already etablished with a handshake, send a disconnect message to the others
if(sessioninfos[client.id] && sessioninfos[client.id].author)
{
var author = sessioninfos[client.id].author;
//get the author color out of the db
authorManager.getAuthorColorId(author, function(err, color)
{
ERR(err);
//prepare the notification for the other users on the pad, that this user left
var messageToTheOtherUsers = {
"type": "COLLABROOM",
"data": {
type: "USER_LEAVE",
userInfo: {
"ip": "127.0.0.1",
"colorId": color,
"userAgent": "Anonymous",
"userId": author
}
}
};
//Go trough all user that are still on the pad, and send them the USER_LEAVE message
for(i in pad2sessions[sessionPad])
{
socketio.sockets.sockets[pad2sessions[sessionPad][i]].json.send(messageToTheOtherUsers);
}
});
}
//Go trough all sessions of this pad, search and destroy the entry of this client
for(i in pad2sessions[sessionPad])
{
if(pad2sessions[sessionPad][i] == client.id)
{
pad2sessions[sessionPad].splice(i, 1);
break;
}
}
//Delete the session2pad and sessioninfos entrys of this session
delete session2pad[client.id];
delete sessioninfos[client.id];
}
/**
* Handles a message from a user
* @param client the client that send this message
* @param message the message from the client
*/
exports.handleMessage = function(client, message)
{
if(message == null)
{
messageLogger.warn("Message is null!");
return;
}
if(!message.type)
{
messageLogger.warn("Message has no type attribute!");
return;
}
//Check what type of message we get and delegate to the other methodes
if(message.type == "CLIENT_READY")
{
handleClientReady(client, message);
}
else if(message.type == "COLLABROOM" &&
message.data.type == "USER_CHANGES")
{
handleUserChanges(client, message);
}
else if(message.type == "COLLABROOM" &&
message.data.type == "USERINFO_UPDATE")
{
handleUserInfoUpdate(client, message);
}
else if(message.type == "COLLABROOM" &&
message.data.type == "CHAT_MESSAGE")
{
handleChatMessage(client, message);
}
else if(message.type == "COLLABROOM" &&
message.data.type == "CLIENT_MESSAGE" &&
message.data.payload.type == "suggestUserName")
{
handleSuggestUserName(client, message);
}
//if the message type is unknown, throw an exception
else
{
messageLogger.warn("Dropped message, unknown Message Type " + message.type);
}
}
/**
* Handles a Chat Message
* @param client the client that send this message
* @param message the message from the client
*/
function handleChatMessage(client, message)
{
var time = new Date().getTime();
var userId = sessioninfos[client.id].author;
var text = message.data.text;
var padId = session2pad[client.id];
var pad;
var userName;
async.series([
//get the pad
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
if(ERR(err, callback)) return;
pad = _pad;
callback();
});
},
function(callback)
{
authorManager.getAuthorName(userId, function(err, _userName)
{
if(ERR(err, callback)) return;
userName = _userName;
callback();
});
},
//save the chat message and broadcast it
function(callback)
{
//save the chat message
pad.appendChatMessage(text, userId, time);
var msg = {
type: "COLLABROOM",
data: {
type: "CHAT_MESSAGE",
userId: userId,
userName: userName,
time: time,
text: text
}
};
//broadcast the chat message to everyone on the pad
for(var i in pad2sessions[padId])
{
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(msg);
}
callback();
}
], function(err)
{
ERR(err);
});
}
/**
* Handles a handleSuggestUserName, that means a user have suggest a userName for a other user
* @param client the client that send this message
* @param message the message from the client
*/
function handleSuggestUserName(client, message)
{
//check if all ok
if(message.data.payload.newName == null)
{
messageLogger.warn("Dropped message, suggestUserName Message has no newName!");
return;
}
if(message.data.payload.unnamedId == null)
{
messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!");
return;
}
var padId = session2pad[client.id];
//search the author and send him this message
for(var i in pad2sessions[padId])
{
if(sessioninfos[pad2sessions[padId][i]].author == message.data.payload.unnamedId)
{
socketio.sockets.sockets[pad2sessions[padId][i]].send(message);
break;
}
}
}
/**
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
* @param client the client that send this message
* @param message the message from the client
*/
function handleUserInfoUpdate(client, message)
{
//check if all ok
if(message.data.userInfo.colorId == null)
{
messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!");
return;
}
//Find out the author name of this session
var author = sessioninfos[client.id].author;
//Tell the authorManager about the new attributes
authorManager.setAuthorColorId(author, message.data.userInfo.colorId);
authorManager.setAuthorName(author, message.data.userInfo.name);
var padId = session2pad[client.id];
//set a null name, when there is no name set. cause the client wants it null
if(message.data.userInfo.name == null)
{
message.data.userInfo.name = null;
}
//The Client don't know about a USERINFO_UPDATE, it can handle only new user_newinfo, so change the message type
message.data.type = "USER_NEWINFO";
//Send the other clients on the pad the update message
for(var i in pad2sessions[padId])
{
if(pad2sessions[padId][i] != client.id)
{
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(message);
}
}
}
/**
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
* This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly
* Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
* @param client the client that send this message
* @param message the message from the client
*/
function handleUserChanges(client, message)
{
//check if all ok
if(message.data.baseRev == null)
{
messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!");
return;
}
if(message.data.apool == null)
{
messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!");
return;
}
if(message.data.changeset == null)
{
messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!");
return;
}
//get all Vars we need
var baseRev = message.data.baseRev;
var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool);
var changeset = message.data.changeset;
var r, apool, pad;
async.series([
//get the pad
function(callback)
{
padManager.getPad(session2pad[client.id], function(err, value)
{
if(ERR(err, callback)) return;
pad = value;
callback();
});
},
//create the changeset
function(callback)
{
//ex. _checkChangesetAndPool
//Copied from Etherpad, don't know what it does exactly
try
{
//this looks like a changeset check, it throws errors sometimes
Changeset.checkRep(changeset);
Changeset.eachAttribNumber(changeset, function(n) {
if (! wireApool.getAttrib(n)) {
throw "Attribute pool is missing attribute "+n+" for changeset "+changeset;
}
});
}
//there is an error in this changeset, so just refuse it
catch(e)
{
console.warn("Can't apply USER_CHANGES "+changeset+", cause it faild checkRep");
client.json.send({disconnect:"badChangeset"});
return;
}
//ex. adoptChangesetAttribs
//Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
//ex. applyUserChanges
apool = pad.pool;
r = baseRev;
//https://github.com/caolan/async#whilst
async.whilst(
function() { return r < pad.getHeadRevisionNumber(); },
function(callback)
{
r++;
pad.getRevisionChangeset(r, function(err, c)
{
if(ERR(err, callback)) return;
changeset = Changeset.follow(c, changeset, false, apool);
callback(null);
});
},
//use the callback of the series function
callback
);
},
//do correction changesets, and send it to all users
function (callback)
{
var prevText = pad.text();
if (Changeset.oldLen(changeset) != prevText.length)
{
console.warn("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length);
client.json.send({disconnect:"badChangeset"});
callback();
return;
}
var thisAuthor = sessioninfos[client.id].author;
pad.appendRevision(changeset, thisAuthor);
var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool);
if (correctionChangeset) {
pad.appendRevision(correctionChangeset);
}
if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, 0, "\n");
pad.appendRevision(nlChangeset);
}
exports.updatePadClients(pad, callback);
}
], function(err)
{
ERR(err);
});
}
exports.updatePadClients = function(pad, callback)
{
//skip this step if noone is on this pad
if(!pad2sessions[pad.id])
{
callback();
return;
}
//go trough all sessions on this pad
async.forEach(pad2sessions[pad.id], function(session, callback)
{
var lastRev = sessioninfos[session].rev;
//https://github.com/caolan/async#whilst
//send them all new changesets
async.whilst(
function (){ return lastRev < pad.getHeadRevisionNumber()},
function(callback)
{
var author, revChangeset;
var r = ++lastRev;
async.parallel([
function (callback)
{
pad.getRevisionAuthor(r, function(err, value)
{
if(ERR(err, callback)) return;
author = value;
callback();
});
},
function (callback)
{
pad.getRevisionChangeset(r, function(err, value)
{
if(ERR(err, callback)) return;
revChangeset = value;
callback();
});
}
], function(err)
{
if(ERR(err, callback)) return;
// next if session has not been deleted
if(sessioninfos[session] == null)
{
callback(null);
return;
}
if(author == sessioninfos[session].author)
{
socketio.sockets.sockets[session].json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}});
}
else
{
var forWire = Changeset.prepareForWire(revChangeset, pad.pool);
var wireMsg = {"type":"COLLABROOM","data":{type:"NEW_CHANGES", newRev:r,
changeset: forWire.translated,
apool: forWire.pool,
author: author}};
socketio.sockets.sockets[session].json.send(wireMsg);
}
callback(null);
});
},
callback
);
if(sessioninfos[session] != null)
{
sessioninfos[session].rev = pad.getHeadRevisionNumber();
}
},callback);
}
/**
* Copied from the Etherpad Source Code. Don't know what this methode does excatly...
*/
function _correctMarkersInPad(atext, apool) {
var text = atext.text;
// collect char positions of line markers (e.g. bullets) in new atext
// that aren't at the start of a line
var badMarkers = [];
var iter = Changeset.opIterator(atext.attribs);
var offset = 0;
while (iter.hasNext()) {
var op = iter.next();
var listValue = Changeset.opAttributeValue(op, 'list', apool);
if (listValue) {
for(var i=0;i<op.chars;i++) {
if (offset > 0 && text.charAt(offset-1) != '\n') {
badMarkers.push(offset);
}
offset++;
}
}
else {
offset += op.chars;
}
}
if (badMarkers.length == 0) {
return null;
}
// create changeset that removes these bad markers
offset = 0;
var builder = Changeset.builder(text.length);
badMarkers.forEach(function(pos) {
builder.keepText(text.substring(offset, pos));
builder.remove(1);
offset = pos+1;
});
return builder.toString();
}
/**
* Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
* and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
* @param client the client that send this message
* @param message the message from the client
*/
function handleClientReady(client, message)
{
//check if all ok
if(!message.token)
{
messageLogger.warn("Dropped message, CLIENT_READY Message has no token!");
return;
}
if(!message.padId)
{
messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!");
return;
}
if(!message.protocolVersion)
{
messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!");
return;
}
if(message.protocolVersion != 2)
{
messageLogger.warn("Dropped message, CLIENT_READY Message has a unknown protocolVersion '" + message.protocolVersion + "'!");
return;
}
var author;
var authorName;
var authorColorId;
var pad;
var historicalAuthorData = {};
var readOnlyId;
var chatMessages;
async.series([
//check permissions
function(callback)
{
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
{
if(ERR(err, callback)) return;
//access was granted
if(statusObject.accessStatus == "grant")
{
author = statusObject.authorID;
callback();
}
//no access, send the client a message that tell him why
else
{
client.json.send({accessStatus: statusObject.accessStatus})
}
});
},
//get all authordata of this new user
function(callback)
{
async.parallel([
//get colorId
function(callback)
{
authorManager.getAuthorColorId(author, function(err, value)
{
if(ERR(err, callback)) return;
authorColorId = value;
callback();
});
},
//get author name
function(callback)
{
authorManager.getAuthorName(author, function(err, value)
{
if(ERR(err, callback)) return;
authorName = value;
callback();
});
},
function(callback)
{
padManager.getPad(message.padId, function(err, value)
{
if(ERR(err, callback)) return;
pad = value;
callback();
});
},
function(callback)
{
readOnlyManager.getReadOnlyId(message.padId, function(err, value)
{
if(ERR(err, callback)) return;
readOnlyId = value;
callback();
});
}
], callback);
},
//these db requests all need the pad object
function(callback)
{
var authors = pad.getAllAuthors();
async.parallel([
//get all author data out of the database
function(callback)
{
async.forEach(authors, function(authorId, callback)
{
authorManager.getAuthor(authorId, function(err, author)
{
if(ERR(err, callback)) return;
delete author.timestamp;
historicalAuthorData[authorId] = author;
callback();
});
}, callback);
},
//get the latest chat messages
function(callback)
{
pad.getLastChatMessages(100, function(err, _chatMessages)
{
if(ERR(err, callback)) return;
chatMessages = _chatMessages;
callback();
});
}
], callback);
},
function(callback)
{
//Check if this author is already on the pad, if yes, kick the other sessions!
if(pad2sessions[message.padId])
{
for(var i in pad2sessions[message.padId])
{
if(sessioninfos[pad2sessions[message.padId][i]].author == author)
{
socketio.sockets.sockets[pad2sessions[message.padId][i]].json.send({disconnect:"userdup"});
}
}
}
//Save in session2pad that this session belonges to this pad
var sessionId=String(client.id);
session2pad[sessionId] = message.padId;
//check if there is already a pad2sessions entry, if not, create one
if(!pad2sessions[message.padId])
{
pad2sessions[message.padId] = [];
}
//Saves in pad2sessions that this session belongs to this pad
pad2sessions[message.padId].push(sessionId);
//prepare all values for the wire
var atext = Changeset.cloneAText(pad.atext);
var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
var apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated;
var clientVars = {
"accountPrivs": {
"maxRevisions": 100
},
"initialRevisionList": [],
"initialOptions": {
"guestPolicy": "deny"
},
"collab_client_vars": {
"initialAttributedText": atext,
"clientIp": "127.0.0.1",
//"clientAgent": "Anonymous Agent",
"padId": message.padId,
"historicalAuthorData": historicalAuthorData,
"apool": apool,
"rev": pad.getHeadRevisionNumber(),
"globalPadId": message.padId
},
"colorPalette": ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"],
"clientIp": "127.0.0.1",
"userIsGuest": true,
"userColor": authorColorId,
"padId": message.padId,
"initialTitle": "Pad: " + message.padId,
"opts": {},
"chatHistory": chatMessages,
"numConnectedUsers": pad2sessions[message.padId].length,
"isProPad": false,
"readOnlyId": readOnlyId,
"serverTimestamp": new Date().getTime(),
"globalPadId": message.padId,
"userId": author,
"cookiePrefsToSet": {
"fullWidth": false,
"hideSidebar": false
},
"abiwordAvailable": settings.abiwordAvailable(),
"hooks": {}
}
//Add a username to the clientVars if one avaiable
if(authorName != null)
{
clientVars.userName = authorName;
}
if(sessioninfos[client.id] !== undefined)
{
//This is a reconnect, so we don't have to send the client the ClientVars again
if(message.reconnect == true)
{
//Save the revision in sessioninfos, we take the revision from the info the client send to us
sessioninfos[client.id].rev = message.client_rev;
}
//This is a normal first connect
else
{
//Send the clientVars to the Client
client.json.send(clientVars);
//Save the revision in sessioninfos
sessioninfos[client.id].rev = pad.getHeadRevisionNumber();
}
//Save the revision and the author id in sessioninfos
sessioninfos[client.id].author = author;
}
//prepare the notification for the other users on the pad, that this user joined
var messageToTheOtherUsers = {
"type": "COLLABROOM",
"data": {
type: "USER_NEWINFO",
userInfo: {
"ip": "127.0.0.1",
"colorId": authorColorId,
"userAgent": "Anonymous",
"userId": author
}
}
};
//Add the authorname of this new User, if avaiable
if(authorName != null)
{
messageToTheOtherUsers.data.userInfo.name = authorName;
}
//Run trough all sessions of this pad
async.forEach(pad2sessions[message.padId], function(sessionID, callback)
{
var author, socket, sessionAuthorName, sessionAuthorColorId;
//Since sessioninfos might change while being enumerated, check if the
//sessionID is still assigned to a valid session
if(sessioninfos[sessionID] !== undefined &&
socketio.sockets.sockets[sessionID] !== undefined){
author = sessioninfos[sessionID].author;
socket = socketio.sockets.sockets[sessionID];
}else {
// If the sessionID is not valid, callback();
callback();
return;
}
async.series([
//get the authorname & colorId
function(callback)
{
async.parallel([
function(callback)
{
authorManager.getAuthorColorId(author, function(err, value)
{
if(ERR(err, callback)) return;
sessionAuthorColorId = value;
callback();
})
},
function(callback)
{
authorManager.getAuthorName(author, function(err, value)
{
if(ERR(err, callback)) return;
sessionAuthorName = value;
callback();
})
}
],callback);
},
function (callback)
{
//Jump over, if this session is the connection session
if(sessionID != client.id)
{
//Send this Session the Notification about the new user
socket.json.send(messageToTheOtherUsers);
//Send the new User a Notification about this other user
var messageToNotifyTheClientAboutTheOthers = {
"type": "COLLABROOM",
"data": {
type: "USER_NEWINFO",
userInfo: {
"ip": "127.0.0.1",
"colorId": sessionAuthorColorId,
"name": sessionAuthorName,
"userAgent": "Anonymous",
"userId": author
}
}
};
client.json.send(messageToNotifyTheClientAboutTheOthers);
}
}
], callback);
}, callback);
}
],function(err)
{
ERR(err);
});
}

View file

@ -0,0 +1,163 @@
/**
* This is the Socket.IO Router. It routes the Messages between the
* components of the Server. The components are at the moment: pad and timeslider
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
var securityManager = require("../db/SecurityManager");
/**
* Saves all components
* key is the component name
* value is the component module
*/
var components = {};
var socket;
/**
* adds a component
*/
exports.addComponent = function(moduleName, module)
{
//save the component
components[moduleName] = module;
//give the module the socket
module.setSocketIO(socket);
}
/**
* sets the socket.io and adds event functions for routing
*/
exports.setSocketIO = function(_socket)
{
//save this socket internaly
socket = _socket;
socket.sockets.on('connection', function(client)
{
var clientAuthorized = false;
//wrap the original send function to log the messages
client._send = client.send;
client.send = function(message)
{
messageLogger.info("to " + client.id + ": " + stringifyWithoutPassword(message));
client._send(message);
}
//tell all components about this connect
for(var i in components)
{
components[i].handleConnect(client);
}
//try to handle the message of this client
function handleMessage(message)
{
if(message.component && components[message.component])
{
//check if component is registered in the components array
if(components[message.component])
{
messageLogger.info("from " + client.id + ": " + stringifyWithoutPassword(message));
components[message.component].handleMessage(client, message);
}
}
else
{
messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
}
}
client.on('message', function(message)
{
if(message.protocolVersion && message.protocolVersion != 2)
{
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
return;
}
//client is authorized, everything ok
if(clientAuthorized)
{
handleMessage(message);
}
//try to authorize the client
else
{
//this message has everything to try an authorization
if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined)
{
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
{
ERR(err);
//access was granted, mark the client as authorized and handle the message
if(statusObject.accessStatus == "grant")
{
clientAuthorized = true;
handleMessage(message);
}
//no access, send the client a message that tell him why
else
{
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
client.json.send({accessStatus: statusObject.accessStatus});
}
});
}
//drop message
else
{
messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message));
}
}
});
client.on('disconnect', function()
{
//tell all components about this disconnect
for(var i in components)
{
components[i].handleDisconnect(client);
}
});
});
}
//returns a stringified representation of a message, removes the password
//this ensures there are no passwords in the log
function stringifyWithoutPassword(message)
{
var newMessage = {};
for(var i in message)
{
if(i == "password" && message[i] != null)
newMessage["password"] = "xxx";
else
newMessage[i]=message[i];
}
return JSON.stringify(newMessage);
}

View file

@ -0,0 +1,529 @@
/**
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var async = require("async");
var padManager = require("../db/PadManager");
var Changeset = CommonCode.require("/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
var settings = require('../utils/Settings');
var authorManager = require("../db/AuthorManager");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
/**
* Saves the Socket class we need to send and recieve data from the client
*/
var socketio;
/**
* This Method is called by server.js to tell the message handler on which socket it should send
* @param socket_io The Socket
*/
exports.setSocketIO = function(socket_io)
{
socketio=socket_io;
}
/**
* Handles the connection of a new user
* @param client the new client
*/
exports.handleConnect = function(client)
{
}
/**
* Handles the disconnection of a user
* @param client the client that leaves
*/
exports.handleDisconnect = function(client)
{
}
/**
* Handles a message from a user
* @param client the client that send this message
* @param message the message from the client
*/
exports.handleMessage = function(client, message)
{
//Check what type of message we get and delegate to the other methodes
if(message.type == "CLIENT_READY")
{
handleClientReady(client, message);
}
else if(message.type == "CHANGESET_REQ")
{
handleChangesetRequest(client, message);
}
//if the message type is unkown, throw an exception
else
{
messageLogger.warn("Dropped message, unknown Message Type: '" + message.type + "'");
}
}
function handleClientReady(client, message)
{
if(message.padId == null)
{
messageLogger.warn("Dropped message, changeset request has no padId!");
return;
}
//send the timeslider client the clientVars, with this values its able to start
createTimesliderClientVars (message.padId, function(err, clientVars)
{
ERR(err);
client.json.send({type: "CLIENT_VARS", data: clientVars});
})
}
/**
* Handles a request for a rough changeset, the timeslider client needs it
*/
function handleChangesetRequest(client, message)
{
//check if all ok
if(message.data == null)
{
messageLogger.warn("Dropped message, changeset request has no data!");
return;
}
if(message.padId == null)
{
messageLogger.warn("Dropped message, changeset request has no padId!");
return;
}
if(message.data.granularity == null)
{
messageLogger.warn("Dropped message, changeset request has no granularity!");
return;
}
if(message.data.start == null)
{
messageLogger.warn("Dropped message, changeset request has no start!");
return;
}
if(message.data.requestID == null)
{
messageLogger.warn("Dropped message, changeset request has no requestID!");
return;
}
var granularity = message.data.granularity;
var start = message.data.start;
var end = start + (100 * granularity);
var padId = message.padId;
//build the requested rough changesets and send them back
getChangesetInfo(padId, start, end, granularity, function(err, changesetInfo)
{
ERR(err);
var data = changesetInfo;
data.requestID = message.data.requestID;
client.json.send({type: "CHANGESET_REQ", data: data});
});
}
function createTimesliderClientVars (padId, callback)
{
var clientVars = {
viewId: padId,
colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"],
sliderEnabled : true,
supportsSlider: true,
savedRevisions: [],
padIdForUrl: padId,
fullWidth: false,
disableRightBar: false,
initialChangesets: [],
abiwordAvailable: settings.abiwordAvailable(),
hooks: [],
initialStyledContents: {}
};
var pad;
var initialChangesets = [];
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
if(ERR(err, callback)) return;
pad = _pad;
callback();
});
},
//get all authors and add them to
function(callback)
{
var historicalAuthorData = {};
//get all authors out of the attribut pool
var authors = pad.getAllAuthors();
//get all author data out of the database
async.forEach(authors, function(authorId, callback)
{
authorManager.getAuthor(authorId, function(err, author)
{
if(ERR(err, callback)) return;
historicalAuthorData[authorId] = author;
callback();
});
}, function(err)
{
if(ERR(err, callback)) return;
//add historicalAuthorData to the clientVars and continue
clientVars.historicalAuthorData = historicalAuthorData;
clientVars.initialStyledContents.historicalAuthorData = historicalAuthorData;
callback();
});
},
//get the timestamp of the last revision
function(callback)
{
pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date)
{
if(ERR(err, callback)) return;
clientVars.currentTime = date;
callback();
});
},
function(callback)
{
//get the head revision Number
var lastRev = pad.getHeadRevisionNumber();
//add the revNum to the client Vars
clientVars.revNum = lastRev;
clientVars.totalRevs = lastRev;
var atext = Changeset.cloneAText(pad.atext);
var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
var apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated;
clientVars.initialStyledContents.apool = apool;
clientVars.initialStyledContents.atext = atext;
var granularities = [100, 10, 1];
//get the latest rough changesets
async.forEach(granularities, function(granularity, callback)
{
var topGranularity = granularity*10;
getChangesetInfo(padId, Math.floor(lastRev / topGranularity)*topGranularity,
Math.floor(lastRev / topGranularity)*topGranularity+topGranularity, granularity,
function(err, changeset)
{
if(ERR(err, callback)) return;
clientVars.initialChangesets.push(changeset);
callback();
});
}, callback);
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, clientVars);
});
}
/**
* Tries to rebuild the getChangestInfo function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
*/
function getChangesetInfo(padId, startNum, endNum, granularity, callback)
{
var forwardsChangesets = [];
var backwardsChangesets = [];
var timeDeltas = [];
var apool = AttributePoolFactory.createAttributePool();
var pad;
var composedChangesets = {};
var revisionDate = [];
var lines;
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
if(ERR(err, callback)) return;
pad = _pad;
callback();
});
},
function(callback)
{
//calculate the last full endnum
var lastRev = pad.getHeadRevisionNumber();
if (endNum > lastRev+1) {
endNum = lastRev+1;
}
endNum = Math.floor(endNum / granularity)*granularity;
var compositesChangesetNeeded = [];
var revTimesNeeded = [];
//figure out which composite Changeset and revTimes we need, to load them in bulk
var compositeStart = startNum;
while (compositeStart < endNum)
{
var compositeEnd = compositeStart + granularity;
//add the composite Changeset we needed
compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd});
//add the t1 time we need
revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1);
//add the t2 time we need
revTimesNeeded.push(compositeEnd - 1);
compositeStart += granularity;
}
//get all needed db values parallel
async.parallel([
function(callback)
{
//get all needed composite Changesets
async.forEach(compositesChangesetNeeded, function(item, callback)
{
composePadChangesets(padId, item.start, item.end, function(err, changeset)
{
if(ERR(err, callback)) return;
composedChangesets[item.start + "/" + item.end] = changeset;
callback();
});
}, callback);
},
function(callback)
{
//get all needed revision Dates
async.forEach(revTimesNeeded, function(revNum, callback)
{
pad.getRevisionDate(revNum, function(err, revDate)
{
if(ERR(err, callback)) return;
revisionDate[revNum] = Math.floor(revDate/1000);
callback();
});
}, callback);
},
//get the lines
function(callback)
{
getPadLines(padId, startNum-1, function(err, _lines)
{
if(ERR(err, callback)) return;
lines = _lines;
callback();
});
}
], callback);
},
//doesn't know what happens here excatly :/
function(callback)
{
var compositeStart = startNum;
while (compositeStart < endNum)
{
if (compositeStart + granularity > endNum)
{
break;
}
var compositeEnd = compositeStart + granularity;
var forwards = composedChangesets[compositeStart + "/" + compositeEnd];
var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
Changeset.mutateTextLines(forwards, lines.textlines);
var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
var t1, t2;
if (compositeStart == 0)
{
t1 = revisionDate[0];
}
else
{
t1 = revisionDate[compositeStart - 1];
}
t2 = revisionDate[compositeEnd - 1];
timeDeltas.push(t2 - t1);
forwardsChangesets.push(forwards2);
backwardsChangesets.push(backwards2);
compositeStart += granularity;
}
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, {forwardsChangesets: forwardsChangesets,
backwardsChangesets: backwardsChangesets,
apool: apool.toJsonable(),
actualEndNum: endNum,
timeDeltas: timeDeltas,
start: startNum,
granularity: granularity });
});
}
/**
* Tries to rebuild the getPadLines function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263
*/
function getPadLines(padId, revNum, callback)
{
var atext;
var result = {};
var pad;
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
if(ERR(err, callback)) return;
pad = _pad;
callback();
});
},
//get the atext
function(callback)
{
if(revNum >= 0)
{
pad.getInternalRevisionAText(revNum, function(err, _atext)
{
if(ERR(err, callback)) return;
atext = _atext;
callback();
});
}
else
{
atext = Changeset.makeAText("\n");
callback(null);
}
},
function(callback)
{
result.textlines = Changeset.splitTextLines(atext.text);
result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text);
callback(null);
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, result);
});
}
/**
* Tries to rebuild the composePadChangeset function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
*/
function composePadChangesets(padId, startNum, endNum, callback)
{
var pad;
var changesets = [];
var changeset;
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
if(ERR(err, callback)) return;
pad = _pad;
callback();
});
},
//fetch all changesets we need
function(callback)
{
var changesetsNeeded=[];
//create a array for all changesets, we will
//replace the values with the changeset later
for(var r=startNum;r<endNum;r++)
{
changesetsNeeded.push(r);
}
//get all changesets
async.forEach(changesetsNeeded, function(revNum,callback)
{
pad.getRevisionChangeset(revNum, function(err, value)
{
if(ERR(err, callback)) return;
changesets[revNum] = value;
callback();
});
},callback);
},
//compose Changesets
function(callback)
{
changeset = changesets[startNum];
var pool = pad.apool();
for(var r=startNum+1;r<endNum;r++)
{
var cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
}
callback(null);
}
],
//return err and changeset
function(err)
{
if(ERR(err, callback)) return;
callback(null, changeset);
});
}

View file

@ -0,0 +1,58 @@
var log4js = require('log4js');
var apiLogger = log4js.getLogger("API");
var formidable = require('formidable');
var apiHandler = require('../../handler/APIHandler');
//This is for making an api call, collecting all post information and passing it to the apiHandler
exports.apiCaller = function(req, res, fields) {
res.header("Content-Type", "application/json; charset=utf-8");
apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
//wrap the send function so we can log the response
res._send = res.send;
res.send = function (response) {
response = JSON.stringify(response);
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
//is this a jsonp call, if yes, add the function call
if(req.query.jsonp)
response = req.query.jsonp + "(" + response + ")";
res._send(response);
}
//call the api handler
apiHandler.handle(req.params.func, fields, req, res);
}
exports.expressCreateServer = function (hook_name, args, cb) {
//This is a api GET call, collect all post informations and pass it to the apiHandler
args.app.get('/api/1/:func', function (req, res) {
apiCaller(req, res, req.query)
});
//This is a api POST call, collect all post informations and pass it to the apiHandler
args.app.post('/api/1/:func', function(req, res) {
new formidable.IncomingForm().parse(req, function (err, fields, files) {
apiCaller(req, res, fields)
});
});
//The Etherpad client side sends information about how a disconnect happen
args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) {
new formidable.IncomingForm().parse(req, function(err, fields, files) {
console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
res.end("OK");
});
});
//The Etherpad client side sends information about client side javscript errors
args.app.post('/jserror', function(req, res) {
new formidable.IncomingForm().parse(req, function(err, fields, files) {
console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo);
res.end("OK");
});
});
}

View file

@ -0,0 +1,52 @@
var os = require("os");
var db = require('../../db/DB');
exports.onShutdown = false;
exports.gracefulShutdown = function(err) {
if(err && err.stack) {
console.error(err.stack);
} else if(err) {
console.error(err);
}
//ensure there is only one graceful shutdown running
if(exports.onShutdown) return;
exports.onShutdown = true;
console.log("graceful shutdown...");
//stop the http server
exports.app.close();
//do the db shutdown
db.db.doShutdown(function() {
console.log("db sucessfully closed.");
process.exit(0);
});
setTimeout(function(){
process.exit(1);
}, 3000);
}
exports.expressCreateServer = function (hook_name, args, cb) {
exports.app = args.app;
args.app.error(function(err, req, res, next){
res.send(500);
console.error(err.stack ? err.stack : err.toString());
exports.gracefulShutdown();
});
//connect graceful shutdown with sigint and uncaughtexception
if(os.type().indexOf("Windows") == -1) {
//sigint is so far not working on windows
//https://github.com/joyent/node/issues/1553
process.on('SIGINT', exports.gracefulShutdown);
}
process.on('uncaughtException', exports.gracefulShutdown);
}

View file

@ -0,0 +1,41 @@
var hasPadAccess = require("../../padaccess");
var settings = require('../../utils/Settings');
var exportHandler = require('../../handler/ExportHandler');
var importHandler = require('../../handler/ImportHandler');
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) {
var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"];
//send a 404 if we don't support this filetype
if (types.indexOf(req.params.type) == -1) {
next();
return;
}
//if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.abiword == null &&
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
return;
}
res.header("Access-Control-Allow-Origin", "*");
hasPadAccess(req, res, function() {
exportHandler.doExport(req, res, req.params.pad, req.params.type);
});
});
//handle import requests
args.app.post('/p/:pad/import', function(req, res, next) {
//if abiword is disabled, skip handling this request
if(settings.abiword == null) {
next();
return;
}
hasPadAccess(req, res, function() {
importHandler.doImport(req, res, req.params.pad);
});
});
}

View file

@ -0,0 +1,6 @@
var minify = require('../../utils/Minify');
exports.expressCreateServer = function (hook_name, args, cb) {
//serve minified files
args.app.get('/minified/:filename', minify.minifyJS);
}

View file

@ -0,0 +1,65 @@
var async = require('async');
var ERR = require("async-stacktrace");
var readOnlyManager = require("../../db/ReadOnlyManager");
var hasPadAccess = require("../../padaccess");
var exporthtml = require("../../utils/ExportHtml");
exports.expressCreateServer = function (hook_name, args, cb) {
//serve read only pad
args.app.get('/ro/:id', function(req, res)
{
var html;
var padId;
var pad;
async.series([
//translate the read only pad to a padId
function(callback)
{
readOnlyManager.getPadId(req.params.id, function(err, _padId)
{
if(ERR(err, callback)) return;
padId = _padId;
//we need that to tell hasPadAcess about the pad
req.params.pad = padId;
callback();
});
},
//render the html document
function(callback)
{
//return if the there is no padId
if(padId == null)
{
callback("notfound");
return;
}
hasPadAccess(req, res, function()
{
//render the html document
exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
{
if(ERR(err, callback)) return;
html = _html;
callback();
});
});
}
], function(err)
{
//throw any unexpected error
if(err && err != "notfound")
ERR(err);
if(err == "notfound")
res.send('404 - Not Found', 404);
else
res.send(html);
});
});
}

View file

@ -0,0 +1,29 @@
var padManager = require('../../db/PadManager');
exports.expressCreateServer = function (hook_name, args, cb) {
//redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', function (req, res, next, padId) {
//ensure the padname is valid and the url doesn't end with a /
if(!padManager.isValidPadId(padId) || /\/$/.test(req.url))
{
res.send('Such a padname is forbidden', 404);
}
else
{
padManager.sanitizePadId(padId, function(sanitizedPadId) {
//the pad id was sanitized, so we redirect to the sanitized version
if(sanitizedPadId != padId)
{
var real_path = req.path.replace(/^\/p\/[^\/]+/, '/p/' + sanitizedPadId);
res.header('Location', real_path);
res.send('You should be redirected to <a href="' + real_path + '">' + real_path + '</a>', 302);
}
//the pad id was fine, so just render it
else
{
next();
}
});
}
});
}

View file

@ -0,0 +1,49 @@
var log4js = require('log4js');
var socketio = require('socket.io');
var settings = require('../../utils/Settings');
var socketIORouter = require("../../handler/SocketIORouter");
var hooks = require("../../pluginfw/hooks");
var padMessageHandler = require("../../handler/PadMessageHandler");
var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler");
exports.expressCreateServer = function (hook_name, args, cb) {
//init socket.io and redirect all requests to the MessageHandler
var io = socketio.listen(args.app);
//this is only a workaround to ensure it works with all browers behind a proxy
//we should remove this when the new socket.io version is more stable
io.set('transports', ['xhr-polling']);
var socketIOLogger = log4js.getLogger("socket.io");
io.set('logger', {
debug: function (str)
{
socketIOLogger.debug.apply(socketIOLogger, arguments);
},
info: function (str)
{
socketIOLogger.info.apply(socketIOLogger, arguments);
},
warn: function (str)
{
socketIOLogger.warn.apply(socketIOLogger, arguments);
},
error: function (str)
{
socketIOLogger.error.apply(socketIOLogger, arguments);
},
});
//minify socket.io javascript
if(settings.minify)
io.enable('browser client minification');
//Initalize the Socket.IO Router
socketIORouter.setSocketIO(io);
socketIORouter.addComponent("pad", padMessageHandler);
socketIORouter.addComponent("timeslider", timesliderMessageHandler);
hooks.callAll("socketio", {"app": args.app, "io": io});
}

View file

@ -0,0 +1,48 @@
var path = require('path');
exports.expressCreateServer = function (hook_name, args, cb) {
//serve index.html under /
args.app.get('/', function(req, res)
{
var filePath = path.normalize(__dirname + "/../../../static/index.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve robots.txt
args.app.get('/robots.txt', function(req, res)
{
var filePath = path.normalize(__dirname + "/../../../static/robots.txt");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve favicon.ico
args.app.get('/favicon.ico', function(req, res)
{
var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico");
res.sendfile(filePath, { maxAge: exports.maxAge }, function(err)
{
//there is no custom favicon, send the default favicon
if(err)
{
filePath = path.normalize(__dirname + "/../../../static/favicon.ico");
res.sendfile(filePath, { maxAge: exports.maxAge });
}
});
});
//serve pad.html under /p
args.app.get('/p/:pad', function(req, res, next)
{
var filePath = path.normalize(__dirname + "/../../../static/pad.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', function(req, res, next)
{
var filePath = path.normalize(__dirname + "/../../../static/timeslider.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
}

View file

@ -0,0 +1,32 @@
var path = require('path');
var minify = require('../../utils/Minify');
var plugins = require("../../pluginfw/plugins");
exports.expressCreateServer = function (hook_name, args, cb) {
//serve static files
args.app.get('/static/js/require-kernel.js', function (req, res, next) {
res.header("Content-Type","application/javascript; charset: utf-8");
res.write(minify.requireDefinition());
res.end();
});
/* Handle paths like "/static/js/plugins/pluginomatic_myplugin/test.js"
by rewriting it to ROOT_PATH_OF_MYPLUGIN/static/js/test.js,
commonly ETHERPAD_ROOT/node_modules/pluginomatic_myplugin/static/js/test.js
*/
args.app.get(/^\/static\/([^\/]+)\/plugins\/([^\/]+)\/(.*)/, function(req, res) {
var type_dir = req.params[0].replace(/\.\./g, '').split("?")[0];
var plugin_name = req.params[1];
var url = req.params[2].replace(/\.\./g, '').split("?")[0];
var filePath = path.normalize(path.join(plugins.plugins[plugin_name].package.path, "static", type_dir, url));
res.sendfile(filePath, { maxAge: exports.maxAge });
});
// Handle normal static files
args.app.get('/static/*', function(req, res) {
var filePath = path.normalize(__dirname + "/../../.." +
req.url.replace(/\.\./g, '').split("?")[0]);
res.sendfile(filePath, { maxAge: exports.maxAge });
});
}

View file

@ -0,0 +1,36 @@
var express = require('express');
var log4js = require('log4js');
var httpLogger = log4js.getLogger("http");
var settings = require('../../utils/Settings');
//checks for basic http auth
exports.basicAuth = function (req, res, next) {
if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) {
// fetch login and password
if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() == settings.httpAuth) {
next();
return;
}
}
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
if (req.headers.authorization) {
setTimeout(function () {
res.send('Authentication required', 401);
}, 1000);
} else {
res.send('Authentication required', 401);
}
}
exports.expressConfigure = function (hook_name, args, cb) {
// Activate http basic auth if it has been defined in settings.json
if(settings.httpAuth != null) args.app.use(exports.basicAuth);
// If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158.
// Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway.
if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR"))
args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
args.app.use(express.cookieParser());
}

21
src/node/padaccess.js Normal file
View file

@ -0,0 +1,21 @@
var ERR = require("async-stacktrace");
var securityManager = require('./db/SecurityManager');
//checks for padAccess
module.exports = function (req, res, callback) {
// FIXME: Why is this ever undefined??
if (req.cookies === undefined) req.cookies = {};
securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj) {
if(ERR(err, callback)) return;
//there is access, continue
if(accessObj.accessStatus == "grant") {
callback();
//no access
} else {
res.send("403 - Can't touch this", 403);
}
});
}

View file

@ -0,0 +1,58 @@
var plugins = require("./plugins");
var async = require("async");
var hookCallWrapper = function (hook, hook_name, args, cb) {
if (cb === undefined) cb = function (x) { return x; };
try {
return hook.hook_fn(hook_name, args, cb);
} catch (ex) {
console.error([hook_name, hook.part.full_name, ex]);
}
}
/* Don't use Array.concat as it flatterns arrays within the array */
exports.flatten = function (lst) {
var res = [];
if (lst != undefined && lst != null) {
for (var i = 0; i < lst.length; i++) {
if (lst[i] != undefined && lst[i] != null) {
for (var j = 0; j < lst[i].length; j++) {
res.push(lst[i][j]);
}
}
}
}
return res;
}
exports.callAll = function (hook_name, args) {
if (plugins.hooks[hook_name] === undefined) return [];
return exports.flatten(plugins.hooks[hook_name].map(function (hook) {
return hookCallWrapper(hook, hook_name, args);
}));
}
exports.aCallAll = function (hook_name, args, cb) {
if (plugins.hooks[hook_name] === undefined) cb([]);
async.map(
plugins.hooks[hook_name],
function (hook, cb) {
hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); });
},
function (err, res) {
cb(exports.flatten(res));
}
);
}
exports.callFirst = function (hook_name, args) {
if (plugins.hooks[hook_name][0] === undefined) return [];
return exports.flatten(hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args));
}
exports.aCallFirst = function (hook_name, args, cb) {
if (plugins.hooks[hook_name][0] === undefined) cb([]);
hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(exports.flatten(res)); });
}

View file

@ -0,0 +1,159 @@
var npm = require("npm/lib/npm.js");
var readInstalled = require("npm/lib/utils/read-installed.js");
var relativize = require("npm/lib/utils/relativize.js");
var readJson = require("npm/lib/utils/read-json.js");
var path = require("path");
var async = require("async");
var fs = require("fs");
var tsort = require("./tsort");
var util = require("util");
exports.prefix = 'pluginomatic_';
exports.loaded = false;
exports.plugins = {};
exports.parts = [];
exports.hooks = {};
exports.ensure = function (cb) {
if (!exports.loaded)
exports.update(cb);
else
cb();
}
exports.formatPlugins = function () {
return Object.keys(exports.plugins).join(", ");
}
exports.formatParts = function () {
return exports.parts.map(function (part) { return part.full_name; }).join("\n");
}
exports.formatHooks = function () {
var res = [];
Object.keys(exports.hooks).forEach(function (hook_name) {
exports.hooks[hook_name].forEach(function (hook) {
res.push(hook.hook_name + ": " + hook.hook_fn_name + " from " + hook.part.full_name);
});
});
return res.join("\n");
}
exports.update = function (cb) {
exports.getPackages(function (er, packages) {
var parts = [];
var plugins = {};
// Load plugin metadata pluginomatic.json
async.forEach(
Object.keys(packages),
function (plugin_name, cb) {
exports.loadPlugin(packages, plugin_name, plugins, parts, cb);
},
function (err) {
exports.plugins = plugins;
exports.parts = exports.sortParts(parts);
exports.hooks = exports.extractHooks(exports.parts);
exports.loaded = true;
cb(err);
}
);
});
}
exports.getPackages = function (cb) {
// Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that
var dir = path.resolve(npm.dir, '..');
readInstalled(dir, function (er, data) {
if (er) cb(er, null);
var packages = {};
function flatten(deps) {
Object.keys(deps).forEach(function (name) {
if (name.indexOf(exports.prefix) == 0) {
packages[name] = deps[name];
}
if (deps[name].dependencies !== undefined)
flatten(deps[name].dependencies);
});
}
flatten([data]);
cb(null, packages);
});
}
exports.extractHooks = function (parts) {
var hooks = {};
parts.forEach(function (part) {
Object.keys(part.hooks || {}).forEach(function (hook_name) {
if (hooks[hook_name] === undefined) hooks[hook_name] = [];
var hook_fn_name = part.hooks[hook_name];
var hook_fn = exports.loadFn(part.hooks[hook_name]);
if (hook_fn) {
hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part});
} else {
console.error("Unable to load hook function for " + part.full_name + " for hook " + hook_name + ": " + part.hooks[hook_name]);
}
});
});
return hooks;
}
exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) {
var plugin_path = path.resolve(packages[plugin_name].path, "pluginomatic.json");
fs.readFile(
plugin_path,
function (er, data) {
if (er) {
console.error("Unable to load plugin definition file " + plugin_path);
return cb();
}
try {
var plugin = JSON.parse(data);
plugin.package = packages[plugin_name];
plugins[plugin_name] = plugin;
plugin.parts.forEach(function (part) {
part.plugin = plugin_name;
part.full_name = plugin_name + "/" + part.name;
parts[part.full_name] = part;
});
} catch (ex) {
console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString());
}
cb();
}
);
}
exports.partsToParentChildList = function (parts) {
var res = [];
Object.keys(parts).forEach(function (name) {
(parts[name].post || []).forEach(function (child_name) {
res.push([name, child_name]);
});
(parts[name].pre || []).forEach(function (parent_name) {
res.push([parent_name, name]);
});
if (!parts[name].pre && !parts[name].post) {
res.push([name, ":" + name]); // Include apps with no dependency info
}
});
return res;
}
exports.sortParts = function(parts) {
return tsort(
exports.partsToParentChildList(parts)
).filter(
function (name) { return parts[name] !== undefined; }
).map(
function (name) { return parts[name]; }
);
};
exports.loadFn = function (path) {
var x = path.split(":");
var fn = require(x[0]);
x[1].split(".").forEach(function (name) {
fn = fn[name];
});
return fn;
}

View file

@ -0,0 +1,42 @@
var plugins = require('./plugins');
var path = require('path');
var SERVER_JS_SRC = path.normalize(path.join(__dirname, '../../'));
var CLIENT_JS_SRC = path.normalize(path.join(__dirname, '../../static/js'));
global.ep_require = function (url) {
if (url.indexOf("/plugins/") == 0) {
/* Handle paths like "/plugins/pluginomatic_myplugin/test.js"
by rewriting it to ROOT_PATH_OF_MYPLUGIN/test.js,
commonly ETHERPAD_ROOT/node_modules/pluginomatic_myplugin/test.js
*/
url = url.split("/");
url.splice(0, 1);
var plugin_name = url.splice(0, 1)[0];
url = url.join("/");
url = path.normalize(path.join(plugins.plugins[plugin_name].package.path, url));
} else if (url.indexOf("/") == 0) {
/* Handle all non-plugin paths for files in / */
url = path.normalize(path.join(SERVER_JS_SRC, url))
}
return require(url);
}
global.ep_client_require = function (url) {
if (url.indexOf("/plugins/") == 0) {
/* Handle paths like "/plugins/pluginomatic_myplugin/test.js"
by rewriting it to ROOT_PATH_OF_MYPLUGIN/static/js/test.js,
commonly ETHERPAD_ROOT/node_modules/pluginomatic_myplugin/static/js/test.js
For more information see hooks/express/static.js
*/
url = url.split("/");
url.splice(0, 2);
var plugin_name = url.splice(0, 1)[0];
url = url.join("/");
url = path.normalize(path.join(plugins.plugins[plugin_name].package.path, "static/js", url));
} else if (url.indexOf("/") == 0) {
/* Handle all non-plugin paths for files in /static */
url = path.normalize(path.join(CLIENT_JS_SRC, url))
}
return require(url);
}

112
src/node/pluginfw/tsort.js Normal file
View file

@ -0,0 +1,112 @@
/**
* general topological sort
* from https://gist.github.com/1232505
* @author SHIN Suzuki (shinout310@gmail.com)
* @param Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
*
* @returns Array : topological sorted list of IDs
**/
function tsort(edges) {
var nodes = {}, // hash: stringified id of the node => { id: id, afters: lisf of ids }
sorted = [], // sorted list of IDs ( returned value )
visited = {}; // hash: id of already visited node => true
var Node = function(id) {
this.id = id;
this.afters = [];
}
// 1. build data structures
edges.forEach(function(v) {
var from = v[0], to = v[1];
if (!nodes[from]) nodes[from] = new Node(from);
if (!nodes[to]) nodes[to] = new Node(to);
nodes[from].afters.push(to);
});
// 2. topological sort
Object.keys(nodes).forEach(function visit(idstr, ancestors) {
var node = nodes[idstr],
id = node.id;
// if already exists, do nothing
if (visited[idstr]) return;
if (!Array.isArray(ancestors)) ancestors = [];
ancestors.push(id);
visited[idstr] = true;
node.afters.forEach(function(afterID) {
if (ancestors.indexOf(afterID) >= 0) // if already in ancestors, a closed chain exists.
throw new Error('closed chain : ' + afterID + ' is in ' + id);
visit(afterID.toString(), ancestors.map(function(v) { return v })); // recursive call
});
sorted.unshift(id);
});
return sorted;
}
/**
* TEST
**/
function tsortTest() {
// example 1: success
var edges = [
[1, 2],
[1, 3],
[2, 4],
[3, 4]
];
var sorted = tsort(edges);
console.log(sorted);
// example 2: failure ( A > B > C > A )
edges = [
['A', 'B'],
['B', 'C'],
['C', 'A']
];
try {
sorted = tsort(edges);
}
catch (e) {
console.log(e.message);
}
// example 3: generate random edges
var max = 100, iteration = 30;
function randomInt(max) {
return Math.floor(Math.random() * max) + 1;
}
edges = (function() {
var ret = [], i = 0;
while (i++ < iteration) ret.push( [randomInt(max), randomInt(max)] );
return ret;
})();
try {
sorted = tsort(edges);
console.log("succeeded", sorted);
}
catch (e) {
console.log("failed", e.message);
}
}
// for node.js
if (typeof exports == 'object' && exports === this) {
module.exports = tsort;
if (process.argv[1] === __filename) tsortTest();
}

96
src/node/server.js Normal file
View file

@ -0,0 +1,96 @@
/**
* This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server.
* Static file Requests are answered directly from this module, Socket.IO messages are passed
* to MessageHandler and minfied requests are passed to minified.
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var log4js = require('log4js');
var fs = require('fs');
var settings = require('./utils/Settings');
var db = require('./db/DB');
var async = require('async');
var express = require('express');
var path = require('path');
require("./pluginfw/require"); // Load globals
var plugins = require("./pluginfw/plugins");
var hooks = require("./pluginfw/hooks");
//try to get the git version
var version = "";
try
{
var rootPath = path.normalize(__dirname + "/../")
var ref = fs.readFileSync(rootPath + ".git/HEAD", "utf-8");
var refPath = rootPath + ".git/" + ref.substring(5, ref.indexOf("\n"));
version = fs.readFileSync(refPath, "utf-8");
version = version.substring(0, 7);
console.log("Your Etherpad Lite git version is " + version);
}
catch(e)
{
console.warn("Can't get git version for server header\n" + e.message)
}
console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues")
var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)";
//cache 6 hours
exports.maxAge = 1000*60*60*6;
//set loglevel
log4js.setGlobalLogLevel(settings.loglevel);
async.waterfall([
//initalize the database
function (callback)
{
db.init(callback);
},
plugins.update,
function (callback) {
console.log("Installed plugins: " + plugins.formatPlugins());
console.log("Installed parts:\n" + plugins.formatParts());
console.log("Installed hooks:\n" + plugins.formatHooks());
callback();
},
//initalize the http server
function (callback)
{
//create server
var app = express.createServer();
hooks.callAll("expressCreateServer", {"app": app});
app.use(function (req, res, next) {
res.header("Server", serverName);
next();
});
app.configure(function() { hooks.callAll("expressConfigure", {"app": app}); });
//let the server listen
app.listen(settings.port, settings.ip);
console.log("Server is listening at " + settings.ip + ":" + settings.port);
callback(null);
}
]);

144
src/node/utils/Abiword.js Normal file
View file

@ -0,0 +1,144 @@
/**
* Controls the communication with the Abiword application
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var util = require('util');
var spawn = require('child_process').spawn;
var async = require("async");
var settings = require("./Settings");
var os = require('os');
var doConvertTask;
//on windows we have to spawn a process for each convertion, cause the plugin abicommand doesn't exist on this platform
if(os.type().indexOf("Windows") > -1)
{
var stdoutBuffer = "";
doConvertTask = function(task, callback)
{
//span an abiword process to perform the conversion
var abiword = spawn(settings.abiword, ["--to=" + task.destFile, task.srcFile]);
//delegate the processing of stdout to another function
abiword.stdout.on('data', function (data)
{
//add data to buffer
stdoutBuffer+=data.toString();
});
//append error messages to the buffer
abiword.stderr.on('data', function (data)
{
stdoutBuffer += data.toString();
});
//throw exceptions if abiword is dieing
abiword.on('exit', function (code)
{
if(code != 0) {
throw "Abiword died with exit code " + code;
}
if(stdoutBuffer != "")
{
console.log(stdoutBuffer);
}
callback();
});
}
exports.convertFile = function(srcFile, destFile, type, callback)
{
doConvertTask({"srcFile": srcFile, "destFile": destFile, "type": type}, callback);
};
}
//on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout
//thats much faster, about factor 10
else
{
//spawn the abiword process
var abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]);
//append error messages to the buffer
abiword.stderr.on('data', function (data)
{
stdoutBuffer += data.toString();
});
//throw exceptions if abiword is dieing
abiword.on('exit', function (code)
{
throw "Abiword died with exit code " + code;
});
//delegate the processing of stdout to a other function
abiword.stdout.on('data',onAbiwordStdout);
var stdoutCallback = null;
var stdoutBuffer = "";
var firstPrompt = true;
function onAbiwordStdout(data)
{
//add data to buffer
stdoutBuffer+=data.toString();
//we're searching for the prompt, cause this means everything we need is in the buffer
if(stdoutBuffer.search("AbiWord:>") != -1)
{
//filter the feedback message
var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer;
//reset the buffer
stdoutBuffer = "";
//call the callback with the error message
//skip the first prompt
if(stdoutCallback != null && !firstPrompt)
{
stdoutCallback(err);
stdoutCallback = null;
}
firstPrompt = false;
}
}
doConvertTask = function(task, callback)
{
abiword.stdin.write("convert " + task.srcFile + " " + task.destFile + " " + task.type + "\n");
//create a callback that calls the task callback and the caller callback
stdoutCallback = function (err)
{
callback();
task.callback(err);
};
}
//Queue with the converts we have to do
var queue = async.queue(doConvertTask, 1);
exports.convertFile = function(srcFile, destFile, type, callback)
{
queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback});
};
}

38
src/node/utils/Cli.js Normal file
View file

@ -0,0 +1,38 @@
/**
* The CLI module handles command line parameters
*/
/*
* 2012 Jordan Hollinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an
"AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// An object containing the parsed command-line options
exports.argv = {};
var argv = process.argv.slice(2);
var arg, prevArg;
// Loop through args
for ( var i = 0; i < argv.length; i++ ) {
arg = argv[i];
// Override location of settings.json file
if ( prevArg == '--settings' || prevArg == '-s' ) {
exports.argv.settings = arg;
}
prevArg = arg;
}

View file

@ -0,0 +1,345 @@
/**
* Copyright 2011 Adrian Lang
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var async = require("async");
var CommonCode = require('./common_code');
var Changeset = CommonCode.require("/Changeset");
var padManager = require("../db/PadManager");
function getPadDokuWiki(pad, revNum, callback)
{
var atext = pad.atext;
var dokuwiki;
async.waterfall([
// fetch revision atext
function (callback)
{
if (revNum != undefined)
{
pad.getInternalRevisionAText(revNum, function (err, revisionAtext)
{
atext = revisionAtext;
callback(err);
});
}
else
{
callback(null);
}
},
// convert atext to dokuwiki text
function (callback)
{
dokuwiki = getDokuWikiFromAtext(pad, atext);
callback(null);
}],
// run final callback
function (err)
{
callback(err, dokuwiki);
});
}
function getDokuWikiFromAtext(pad, atext)
{
var apool = pad.apool();
var textLines = atext.text.slice(0, -1).split('\n');
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
var tags = ['======', '=====', '**', '//', '__', 'del>'];
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
var anumMap = {};
props.forEach(function (propName, i)
{
var propTrueNum = apool.putAttrib([propName, true], true);
if (propTrueNum >= 0)
{
anumMap[propTrueNum] = i;
}
});
function getLineDokuWiki(text, attribs)
{
var propVals = [false, false, false];
var ENTER = 1;
var STAY = 2;
var LEAVE = 0;
// Use order of tags (b/i/u) as order of nesting, for simplicity
// and decent nesting. For example,
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
var taker = Changeset.stringIterator(text);
var assem = Changeset.stringAssembler();
function emitOpenTag(i)
{
if (tags[i].indexOf('>') !== -1) {
assem.append('<');
}
assem.append(tags[i]);
}
function emitCloseTag(i)
{
if (tags[i].indexOf('>') !== -1) {
assem.append('</');
}
assem.append(tags[i]);
}
var urls = _findURLs(text);
var idx = 0;
function processNextChars(numChars)
{
if (numChars <= 0)
{
return;
}
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars));
idx += numChars;
while (iter.hasNext())
{
var o = iter.next();
var propChanged = false;
Changeset.eachAttribNumber(o.attribs, function (a)
{
if (a in anumMap)
{
var i = anumMap[a]; // i = 0 => bold, etc.
if (!propVals[i])
{
propVals[i] = ENTER;
propChanged = true;
}
else
{
propVals[i] = STAY;
}
}
});
for (var i = 0; i < propVals.length; i++)
{
if (propVals[i] === true)
{
propVals[i] = LEAVE;
propChanged = true;
}
else if (propVals[i] === STAY)
{
propVals[i] = true; // set it back
}
}
// now each member of propVal is in {false,LEAVE,ENTER,true}
// according to what happens at start of span
if (propChanged)
{
// leaving bold (e.g.) also leaves italics, etc.
var left = false;
for (var i = 0; i < propVals.length; i++)
{
var v = propVals[i];
if (!left)
{
if (v === LEAVE)
{
left = true;
}
}
else
{
if (v === true)
{
propVals[i] = STAY; // tag will be closed and re-opened
}
}
}
for (var i = propVals.length - 1; i >= 0; i--)
{
if (propVals[i] === LEAVE)
{
emitCloseTag(i);
propVals[i] = false;
}
else if (propVals[i] === STAY)
{
emitCloseTag(i);
}
}
for (var i = 0; i < propVals.length; i++)
{
if (propVals[i] === ENTER || propVals[i] === STAY)
{
emitOpenTag(i);
propVals[i] = true;
}
}
// propVals is now all {true,false} again
} // end if (propChanged)
var chars = o.chars;
if (o.lines)
{
chars--; // exclude newline at end of line, if present
}
var s = taker.take(chars);
assem.append(_escapeDokuWiki(s));
} // end iteration over spans in line
for (var i = propVals.length - 1; i >= 0; i--)
{
if (propVals[i])
{
emitCloseTag(i);
propVals[i] = false;
}
}
} // end processNextChars
if (urls)
{
urls.forEach(function (urlData)
{
var startIndex = urlData[0];
var url = urlData[1];
var urlLength = url.length;
processNextChars(startIndex - idx);
assem.append('[[');
// Do not use processNextChars since a link does not contain syntax and
// needs no escaping
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + urlLength));
idx += urlLength;
assem.append(taker.take(iter.next().chars));
assem.append(']]');
});
}
processNextChars(text.length - idx);
return assem.toString() + "\n";
} // end getLineDokuWiki
var pieces = [];
for (var i = 0; i < textLines.length; i++)
{
var line = _analyzeLine(textLines[i], attribLines[i], apool);
var lineContent = getLineDokuWiki(line.text, line.aline);
if (line.listLevel && lineContent)
{
pieces.push(new Array(line.listLevel + 1).join(' ') + '* ');
}
pieces.push(lineContent);
}
return pieces.join('');
}
function _analyzeLine(text, aline, apool)
{
var line = {};
// identify list
var lineMarker = 0;
line.listLevel = 0;
if (aline)
{
var opIter = Changeset.opIterator(aline);
if (opIter.hasNext())
{
var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
if (listType)
{
lineMarker = 1;
listType = /([a-z]+)([12345678])/.exec(listType);
if (listType)
{
line.listTypeName = listType[1];
line.listLevel = Number(listType[2]);
}
}
}
}
if (lineMarker)
{
line.text = text.substring(1);
line.aline = Changeset.subattribution(aline, 1);
}
else
{
line.text = text;
line.aline = aline;
}
return line;
}
exports.getPadDokuWikiDocument = function (padId, revNum, callback)
{
padManager.getPad(padId, function (err, pad)
{
if (err)
{
callback(err);
return;
}
getPadDokuWiki(pad, revNum, callback);
});
}
function _escapeDokuWiki(s)
{
s = s.replace(/(\/\/|\*\*|__)/g, '%%$1%%');
return s;
}
// copied from ACE
var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
var _REGEX_SPACE = /\s/;
var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')');
var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g');
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
function _findURLs(text)
{
_REGEX_URL.lastIndex = 0;
var urls = null;
var execResult;
while ((execResult = _REGEX_URL.exec(text)))
{
urls = (urls || []);
var startIndex = execResult.index;
var url = execResult[0];
urls.push([startIndex, url]);
}
return urls;
}

View file

@ -0,0 +1,595 @@
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var CommonCode = require('./common_code');
var async = require("async");
var Changeset = CommonCode.require("/Changeset");
var padManager = require("../db/PadManager");
var ERR = require("async-stacktrace");
var Security = CommonCode.require('/security');
function getPadPlainText(pad, revNum)
{
var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext());
var textLines = atext.text.slice(0, -1).split('\n');
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
var apool = pad.pool();
var pieces = [];
for (var i = 0; i < textLines.length; i++)
{
var line = _analyzeLine(textLines[i], attribLines[i], apool);
if (line.listLevel)
{
var numSpaces = line.listLevel * 2 - 1;
var bullet = '*';
pieces.push(new Array(numSpaces + 1).join(' '), bullet, ' ', line.text, '\n');
}
else
{
pieces.push(line.text, '\n');
}
}
return pieces.join('');
}
function getPadHTML(pad, revNum, callback)
{
var atext = pad.atext;
var html;
async.waterfall([
// fetch revision atext
function (callback)
{
if (revNum != undefined)
{
pad.getInternalRevisionAText(revNum, function (err, revisionAtext)
{
if(ERR(err, callback)) return;
atext = revisionAtext;
callback();
});
}
else
{
callback(null);
}
},
// convert atext to html
function (callback)
{
html = getHTMLFromAtext(pad, atext);
callback(null);
}],
// run final callback
function (err)
{
if(ERR(err, callback)) return;
callback(null, html);
});
}
exports.getPadHTML = getPadHTML;
function getHTMLFromAtext(pad, atext)
{
var apool = pad.apool();
var textLines = atext.text.slice(0, -1).split('\n');
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
var tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
var anumMap = {};
props.forEach(function (propName, i)
{
var propTrueNum = apool.putAttrib([propName, true], true);
if (propTrueNum >= 0)
{
anumMap[propTrueNum] = i;
}
});
function getLineHTML(text, attribs)
{
var propVals = [false, false, false];
var ENTER = 1;
var STAY = 2;
var LEAVE = 0;
// Use order of tags (b/i/u) as order of nesting, for simplicity
// and decent nesting. For example,
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
var taker = Changeset.stringIterator(text);
var assem = Changeset.stringAssembler();
var openTags = [];
function emitOpenTag(i)
{
openTags.unshift(i);
assem.append('<');
assem.append(tags[i]);
assem.append('>');
}
function emitCloseTag(i)
{
openTags.shift();
assem.append('</');
assem.append(tags[i]);
assem.append('>');
}
function orderdCloseTags(tags2close)
{
for(var i=0;i<openTags.length;i++)
{
for(var j=0;j<tags2close.length;j++)
{
if(tags2close[j] == openTags[i])
{
emitCloseTag(tags2close[j]);
i--;
break;
}
}
}
}
var urls = _findURLs(text);
var idx = 0;
function processNextChars(numChars)
{
if (numChars <= 0)
{
return;
}
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars));
idx += numChars;
while (iter.hasNext())
{
var o = iter.next();
var propChanged = false;
Changeset.eachAttribNumber(o.attribs, function (a)
{
if (a in anumMap)
{
var i = anumMap[a]; // i = 0 => bold, etc.
if (!propVals[i])
{
propVals[i] = ENTER;
propChanged = true;
}
else
{
propVals[i] = STAY;
}
}
});
for (var i = 0; i < propVals.length; i++)
{
if (propVals[i] === true)
{
propVals[i] = LEAVE;
propChanged = true;
}
else if (propVals[i] === STAY)
{
propVals[i] = true; // set it back
}
}
// now each member of propVal is in {false,LEAVE,ENTER,true}
// according to what happens at start of span
if (propChanged)
{
// leaving bold (e.g.) also leaves italics, etc.
var left = false;
for (var i = 0; i < propVals.length; i++)
{
var v = propVals[i];
if (!left)
{
if (v === LEAVE)
{
left = true;
}
}
else
{
if (v === true)
{
propVals[i] = STAY; // tag will be closed and re-opened
}
}
}
var tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--)
{
if (propVals[i] === LEAVE)
{
//emitCloseTag(i);
tags2close.push(i);
propVals[i] = false;
}
else if (propVals[i] === STAY)
{
//emitCloseTag(i);
tags2close.push(i);
}
}
orderdCloseTags(tags2close);
for (var i = 0; i < propVals.length; i++)
{
if (propVals[i] === ENTER || propVals[i] === STAY)
{
emitOpenTag(i);
propVals[i] = true;
}
}
// propVals is now all {true,false} again
} // end if (propChanged)
var chars = o.chars;
if (o.lines)
{
chars--; // exclude newline at end of line, if present
}
var s = taker.take(chars);
//removes the characters with the code 12. Don't know where they come
//from but they break the abiword parser and are completly useless
s = s.replace(String.fromCharCode(12), "");
assem.append(_encodeWhitespace(Security.escapeHTML(s)));
} // end iteration over spans in line
var tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--)
{
if (propVals[i])
{
tags2close.push(i);
propVals[i] = false;
}
}
orderdCloseTags(tags2close);
} // end processNextChars
if (urls)
{
urls.forEach(function (urlData)
{
var startIndex = urlData[0];
var url = urlData[1];
var urlLength = url.length;
processNextChars(startIndex - idx);
assem.append('<a href="' + Security.escapeHTMLAttribute(url) + '">');
processNextChars(urlLength);
assem.append('</a>');
});
}
processNextChars(text.length - idx);
return _processSpaces(assem.toString());
} // end getLineHTML
var pieces = [];
// Need to deal with constraints imposed on HTML lists; can
// only gain one level of nesting at once, can't change type
// mid-list, etc.
// People might use weird indenting, e.g. skip a level,
// so we want to do something reasonable there. We also
// want to deal gracefully with blank lines.
// => keeps track of the parents level of indentation
var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...]
for (var i = 0; i < textLines.length; i++)
{
var line = _analyzeLine(textLines[i], attribLines[i], apool);
var lineContent = getLineHTML(line.text, line.aline);
if (line.listLevel)//If we are inside a list
{
// do list stuff
var whichList = -1; // index into lists or -1
if (line.listLevel)
{
whichList = lists.length;
for (var j = lists.length - 1; j >= 0; j--)
{
if (line.listLevel <= lists[j][0])
{
whichList = j;
}
}
}
if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line
{
lists.push([line.listLevel, line.listTypeName]);
if(line.listTypeName == "number")
{
pieces.push('<ol class="'+line.listTypeName+'"><li>', lineContent || '<br>');
}
else
{
pieces.push('<ul class="'+line.listTypeName+'"><li>', lineContent || '<br>');
}
}
//the following code *seems* dead after my patch.
//I keep it just in case I'm wrong...
/*else if (whichList == -1)//means we are not inside a list
{
if (line.text)
{
console.log('trace 1');
// non-blank line, end all lists
if(line.listTypeName == "number")
{
pieces.push(new Array(lists.length + 1).join('</li></ol>'));
}
else
{
pieces.push(new Array(lists.length + 1).join('</li></ul>'));
}
lists.length = 0;
pieces.push(lineContent, '<br>');
}
else
{
console.log('trace 2');
pieces.push('<br><br>');
}
}*/
else//means we are getting closer to the lowest level of indentation
{
while (whichList < lists.length - 1)
{
if(lists[lists.length - 1][1] == "number")
{
pieces.push('</li></ol>');
}
else
{
pieces.push('</li></ul>');
}
lists.length--;
}
pieces.push('</li><li>', lineContent || '<br>');
}
}
else//outside any list
{
while (lists.length > 0)//if was in a list: close it before
{
if(lists[lists.length - 1][1] == "number")
{
pieces.push('</li></ol>');
}
else
{
pieces.push('</li></ul>');
}
lists.length--;
}
pieces.push(lineContent, '<br>');
}
}
for (var k = lists.length - 1; k >= 0; k--)
{
if(lists[k][1] == "number")
{
pieces.push('</li></ol>');
}
else
{
pieces.push('</li></ul>');
}
}
return pieces.join('');
}
function _analyzeLine(text, aline, apool)
{
var line = {};
// identify list
var lineMarker = 0;
line.listLevel = 0;
if (aline)
{
var opIter = Changeset.opIterator(aline);
if (opIter.hasNext())
{
var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
if (listType)
{
lineMarker = 1;
listType = /([a-z]+)([12345678])/.exec(listType);
if (listType)
{
line.listTypeName = listType[1];
line.listLevel = Number(listType[2]);
}
}
}
}
if (lineMarker)
{
line.text = text.substring(1);
line.aline = Changeset.subattribution(aline, 1);
}
else
{
line.text = text;
line.aline = aline;
}
return line;
}
exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
{
padManager.getPad(padId, function (err, pad)
{
if(ERR(err, callback)) return;
var head =
(noDocType ? '' : '<!doctype html>\n') +
'<html lang="en">\n' + (noDocType ? '' : '<head>\n' +
'<meta charset="utf-8">\n' +
'<style> * { font-family: arial, sans-serif;\n' +
'font-size: 13px;\n' +
'line-height: 17px; }' +
'ul.indent { list-style-type: none; }' +
'ol { list-style-type: decimal; }' +
'ol ol { list-style-type: lower-latin; }' +
'ol ol ol { list-style-type: lower-roman; }' +
'ol ol ol ol { list-style-type: decimal; }' +
'ol ol ol ol ol { list-style-type: lower-latin; }' +
'ol ol ol ol ol ol{ list-style-type: lower-roman; }' +
'ol ol ol ol ol ol ol { list-style-type: decimal; }' +
'ol ol ol ol ol ol ol ol{ list-style-type: lower-latin; }' +
'</style>\n' + '</head>\n') +
'<body>';
var foot = '</body>\n</html>\n';
getPadHTML(pad, revNum, function (err, html)
{
if(ERR(err, callback)) return;
callback(null, head + html + foot);
});
});
}
function _encodeWhitespace(s) {
return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c)
{
return "&#" +c.charCodeAt(0) + ";"
});
}
// copied from ACE
function _processSpaces(s)
{
var doesWrap = true;
if (s.indexOf("<") < 0 && !doesWrap)
{
// short-cut
return s.replace(/ /g, '&nbsp;');
}
var parts = [];
s.replace(/<[^>]*>?| |[^ <]+/g, function (m)
{
parts.push(m);
});
if (doesWrap)
{
var endOfLine = true;
var beforeSpace = false;
// last space in a run is normal, others are nbsp,
// end of line is nbsp
for (var i = parts.length - 1; i >= 0; i--)
{
var p = parts[i];
if (p == " ")
{
if (endOfLine || beforeSpace) parts[i] = '&nbsp;';
endOfLine = false;
beforeSpace = true;
}
else if (p.charAt(0) != "<")
{
endOfLine = false;
beforeSpace = false;
}
}
// beginning of line is nbsp
for (var i = 0; i < parts.length; i++)
{
var p = parts[i];
if (p == " ")
{
parts[i] = '&nbsp;';
break;
}
else if (p.charAt(0) != "<")
{
break;
}
}
}
else
{
for (var i = 0; i < parts.length; i++)
{
var p = parts[i];
if (p == " ")
{
parts[i] = '&nbsp;';
}
}
}
return parts.join('');
}
// copied from ACE
var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
var _REGEX_SPACE = /\s/;
var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')');
var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g');
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
function _findURLs(text)
{
_REGEX_URL.lastIndex = 0;
var urls = null;
var execResult;
while ((execResult = _REGEX_URL.exec(text)))
{
urls = (urls || []);
var startIndex = execResult.index;
var url = execResult[0];
urls.push([startIndex, url]);
}
return urls;
}

View file

@ -0,0 +1,93 @@
/**
* Copyright Yaco Sistemas S.L. 2011.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var jsdom = require('jsdom-nocontextifiy').jsdom;
var log4js = require('log4js');
var CommonCode = require('../utils/common_code');
var Changeset = CommonCode.require("/Changeset");
var contentcollector = CommonCode.require("/contentcollector");
var map = CommonCode.require("/ace2_common").map;
function setPadHTML(pad, html, callback)
{
var apiLogger = log4js.getLogger("ImportHtml");
// Clean the pad. This makes the rest of the code easier
// by several orders of magnitude.
pad.setText("");
var padText = pad.text();
// Parse the incoming HTML with jsdom
var doc = jsdom(html.replace(/>\n+</g, '><'));
apiLogger.debug('html:');
apiLogger.debug(html);
// Convert a dom tree into a list of lines and attribute liens
// using the content collector object
var cc = contentcollector.makeContentCollector(true, null, pad.pool);
cc.collectContent(doc.childNodes[0]);
var result = cc.finish();
apiLogger.debug('Lines:');
var i;
for (i = 0; i < result.lines.length; i += 1)
{
apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]);
apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]);
}
// Get the new plain text and its attributes
var newText = map(result.lines, function (e) {
return e + '\n';
}).join('');
apiLogger.debug('newText:');
apiLogger.debug(newText);
var newAttribs = result.lineAttribs.join('|1+1') + '|1+1';
function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ )
{
var attribsIter = Changeset.opIterator(attribs);
var textIndex = 0;
var newTextStart = 0;
var newTextEnd = newText.length - 1;
while (attribsIter.hasNext())
{
var op = attribsIter.next();
var nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd))
{
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
}
textIndex = nextIndex;
}
}
// create a new changeset with a helper builder object
var builder = Changeset.builder(1);
// assemble each line into the builder
eachAttribRun(newAttribs, function(start, end, attribs)
{
builder.insert(newText.substring(start, end), attribs);
});
// the changeset is ready!
var theChangeset = builder.toString();
apiLogger.debug('The changeset: ' + theChangeset);
pad.appendRevision(theChangeset);
}
exports.setPadHTML = setPadHTML;

338
src/node/utils/Minify.js Normal file
View file

@ -0,0 +1,338 @@
/**
* This Module manages all /minified/* requests. It controls the
* minification && compression of Javascript and CSS.
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var settings = require('./Settings');
var async = require('async');
var fs = require('fs');
var cleanCSS = require('clean-css');
var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify;
var path = require('path');
var Buffer = require('buffer').Buffer;
var zlib = require('zlib');
var RequireKernel = require('require-kernel');
var server = require('../server');
var os = require('os');
var ROOT_DIR = path.normalize(__dirname + "/../" );
var JS_DIR = ROOT_DIR + '../static/js/';
var CSS_DIR = ROOT_DIR + '../static/css/';
var CACHE_DIR = path.join(settings.root, 'var');
var TAR_PATH = path.join(__dirname, 'tar.json');
var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8'));
/**
* creates the minifed javascript for the given minified name
* @param req the Express request
* @param res the Express response
*/
exports.minifyJS = function(req, res, next)
{
var jsFilename = req.params['filename'];
//choose the js files we need
var jsFiles = undefined;
if (Object.prototype.hasOwnProperty.call(tar, jsFilename)) {
jsFiles = tar[jsFilename];
_handle(req, res, jsFilename, jsFiles)
} else {
// Not in tar list, but try anyways, if it fails, pass to `next`.
jsFiles = [jsFilename];
fs.stat(JS_DIR + jsFilename, function (error, stats) {
if (error || !stats.isFile()) {
next();
} else {
_handle(req, res, jsFilename, jsFiles);
}
});
}
}
function _handle(req, res, jsFilename, jsFiles) {
res.header("Content-Type","text/javascript");
//minifying is enabled
if(settings.minify)
{
var result = undefined;
var latestModification = 0;
async.series([
//find out the highest modification date
function(callback)
{
var folders2check = [CSS_DIR, JS_DIR];
//go trough this two folders
async.forEach(folders2check, function(path, callback)
{
//read the files in the folder
fs.readdir(path, function(err, files)
{
if(ERR(err, callback)) return;
//we wanna check the directory itself for changes too
files.push(".");
//go trough all files in this folder
async.forEach(files, function(filename, callback)
{
//get the stat data of this file
fs.stat(path + "/" + filename, function(err, stats)
{
if(ERR(err, callback)) return;
//get the modification time
var modificationTime = stats.mtime.getTime();
//compare the modification time to the highest found
if(modificationTime > latestModification)
{
latestModification = modificationTime;
}
callback();
});
}, callback);
});
}, callback);
},
function(callback)
{
//check the modification time of the minified js
fs.stat(CACHE_DIR + "/minified_" + jsFilename, function(err, stats)
{
if(err && err.code != "ENOENT")
{
ERR(err, callback);
return;
}
//there is no minfied file or there new changes since this file was generated, so continue generating this file
if((err && err.code == "ENOENT") || stats.mtime.getTime() < latestModification)
{
callback();
}
//the minified file is still up to date, stop minifying
else
{
callback("stop");
}
});
},
//load all js files
function (callback)
{
var values = [];
tarCode(
jsFiles
, function (content) {values.push(content)}
, function (err) {
if(ERR(err)) return;
result = values.join('');
callback();
});
},
//put all together and write it into a file
function(callback)
{
async.parallel([
//write the results plain in a file
function(callback)
{
fs.writeFile(CACHE_DIR + "/minified_" + jsFilename, result, "utf8", callback);
},
//write the results compressed in a file
function(callback)
{
zlib.gzip(result, function(err, compressedResult){
//weird gzip bug that returns 0 instead of null if everything is ok
err = err === 0 ? null : err;
if(ERR(err, callback)) return;
fs.writeFile(CACHE_DIR + "/minified_" + jsFilename + ".gz", compressedResult, callback);
});
}
],callback);
}
], function(err)
{
if(err && err != "stop")
{
if(ERR(err)) return;
}
//check if gzip is supported by this browser
var gzipSupport = req.header('Accept-Encoding', '').indexOf('gzip') != -1;
var pathStr;
if(gzipSupport && os.type().indexOf("Windows") == -1)
{
pathStr = path.normalize(CACHE_DIR + "/minified_" + jsFilename + ".gz");
res.header('Content-Encoding', 'gzip');
}
else
{
pathStr = path.normalize(CACHE_DIR + "/minified_" + jsFilename );
}
res.sendfile(pathStr, { maxAge: server.maxAge });
})
}
//minifying is disabled, so put the files together in one file
else
{
tarCode(
jsFiles
, function (content) {res.write(content)}
, function (err) {
if(ERR(err)) return;
res.end();
});
}
}
// find all includes in ace.js and embed them.
function getAceFile(callback) {
fs.readFile(JS_DIR + 'ace.js', "utf8", function(err, data) {
if(ERR(err, callback)) return;
// Find all includes in ace.js and embed them
var founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\([a-zA-Z0-9.\/_"-]+\)/gi);
if (!settings.minify) {
founds = [];
}
founds.push('$$INCLUDE_JS("../static/js/require-kernel.js")');
data += ';\n';
data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n';
//go trough all includes
async.forEach(founds, function (item, callback) {
var filename = item.match(/"([^"]*)"/)[1];
var type = item.match(/INCLUDE_([A-Z]+)/)[1];
var shortFilename = (filename.match(/^..\/static\/js\/(.*)$/, '')||[])[1];
//read the included files
if (shortFilename) {
if (shortFilename == 'require-kernel.js') {
// the kernel isnt actually on the file system.
handleEmbed(null, requireDefinition());
} else {
var contents = '';
tarCode(tar[shortFilename] || shortFilename
, function (content) {
contents += content;
}
, function () {
handleEmbed(null, contents);
}
);
}
} else {
fs.readFile(ROOT_DIR + filename, "utf8", handleEmbed);
}
function handleEmbed(error, data_) {
if (error) {
return; // Don't bother to include it.
}
if (settings.minify) {
if (type == "JS") {
try {
data_ = compressJS([data_]);
} catch (e) {
// Ignore, include uncompresseed, which will break in browser.
}
} else {
data_ = compressCSS([data_]);
}
}
data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = '
+ JSON.stringify(data_) + ';\n';
callback();
}
}, function(error) {
callback(error, data);
});
});
}
exports.requireDefinition = requireDefinition;
function requireDefinition() {
return 'var require = ' + RequireKernel.kernelSource + ';\n';
}
function tarCode(jsFiles, write, callback) {
write('require.define({');
var initialEntry = true;
async.forEach(jsFiles, function (filename, callback){
if (filename == 'ace.js') {
getAceFile(handleFile);
} else {
fs.readFile(JS_DIR + filename, "utf8", handleFile);
}
function handleFile(err, data) {
if(ERR(err, callback)) return;
var srcPath = JSON.stringify('/' + filename);
var srcPathAbbv = JSON.stringify('/' + filename.replace(/\.js$/, ''));
if (!initialEntry) {
write('\n,');
} else {
initialEntry = false;
}
write(srcPath + ': ')
data = '(function (require, exports, module) {' + data + '})';
if (settings.minify) {
write(compressJS([data]));
} else {
write(data);
}
if (srcPath != srcPathAbbv) {
write('\n,' + srcPathAbbv + ': null');
}
callback();
}
}, function () {
write('});\n');
callback();
});
}
function compressJS(values)
{
var complete = values.join("\n");
var ast = jsp.parse(complete); // parse code and get the initial AST
ast = pro.ast_mangle(ast); // get a new AST with mangled names
ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
return pro.gen_code(ast); // compressed code here
}
function compressCSS(values)
{
var complete = values.join("\n");
return cleanCSS.process(complete);
}

141
src/node/utils/Settings.js Normal file
View file

@ -0,0 +1,141 @@
/**
* The Settings Modul reads the settings out of settings.json and provides
* this information to the other modules
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var fs = require("fs");
var os = require("os");
var path = require('path');
var argv = require('./Cli').argv;
var npm = require("npm/lib/npm.js");
/* Root path of the installation */
exports.root = path.normalize(path.join(npm.dir, ".."));
/**
* The IP ep-lite should listen to
*/
exports.ip = "0.0.0.0";
/**
* The Port ep-lite should listen to
*/
exports.port = 9001;
/*
* The Type of the database
*/
exports.dbType = "dirty";
/**
* This setting is passed with dbType to ueberDB to set up the database
*/
exports.dbSettings = { "filename" : path.join(exports.root, "var/dirty.db") };
/**
* The default Text of a new pad
*/
exports.defaultPadText = "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n";
/**
* A flag that requires any user to have a valid session (via the api) before accessing a pad
*/
exports.requireSession = false;
/**
* A flag that prevents users from creating new pads
*/
exports.editOnly = false;
/**
* A flag that shows if minification is enabled or not
*/
exports.minify = true;
/**
* The path of the abiword executable
*/
exports.abiword = null;
/**
* The log level of log4js
*/
exports.loglevel = "INFO";
/**
* Http basic auth, with "user:password" format
*/
exports.httpAuth = null;
//checks if abiword is avaiable
exports.abiwordAvailable = function()
{
if(exports.abiword != null)
{
return os.type().indexOf("Windows") != -1 ? "withoutPDF" : "yes";
}
else
{
return "no";
}
}
// Discover where the settings file lives
var settingsFilename = argv.settings || "settings.json";
if (settingsFilename.charAt(0) != '/') {
settingsFilename = path.normalize(path.join(root, settingsFilename));
}
//read the settings sync
var settingsStr = fs.readFileSync(settingsFilename).toString();
//remove all comments
settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,"");
//try to parse the settings
var settings;
try
{
settings = JSON.parse(settingsStr);
}
catch(e)
{
console.error("There is a syntax error in your settings.json file");
console.error(e.message);
process.exit(1);
}
//loop trough the settings
for(var i in settings)
{
//test if the setting start with a low character
if(i.charAt(0).search("[a-z]") !== 0)
{
console.warn("Settings should start with a low character: '" + i + "'");
}
//we know this setting, so we overwrite it
if(exports[i] !== undefined)
{
exports[i] = settings[i];
}
//this setting is unkown, output a warning and throw it away
else
{
console.warn("Unkown Setting: '" + i + "'");
console.warn("This setting doesn't exist or it was removed");
}
}

View file

@ -0,0 +1,24 @@
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var RequireKernel = require('require-kernel/');
var path = require('path');
var CLIENT_JS_SRC = path.normalize(__dirname + '/../../static/js/');
var client_require = RequireKernel.requireForPaths(
'file://' + (CLIENT_JS_SRC.charAt(0) == '/' ? '' : '/') + encodeURI(CLIENT_JS_SRC));
exports.require = client_require;

View file

@ -0,0 +1,17 @@
/*
This helper modules allows us to create different type of errors we can throw
*/
function customError(message, errorName)
{
this.name = errorName || "Error";
this.message = message;
var stackParts = new Error().stack.split("\n");
stackParts.splice(0,2);
stackParts.unshift(this.name + ": " + message);
this.stack = stackParts.join("\n");
}
customError.prototype = Error.prototype;
module.exports = customError;

70
src/node/utils/tar.json Normal file
View file

@ -0,0 +1,70 @@
{
"pad.js": [
"jquery.js"
, "security.js"
, "pad.js"
, "ace2_common.js"
, "pad_utils.js"
, "plugins.js"
, "undo-xpopup.js"
, "json2.js"
, "pad_cookie.js"
, "pad_editor.js"
, "pad_editbar.js"
, "pad_docbar.js"
, "pad_modals.js"
, "ace.js"
, "collab_client.js"
, "pad_userlist.js"
, "pad_impexp.js"
, "pad_savedrevs.js"
, "pad_connectionstatus.js"
, "chat.js"
, "excanvas.js"
, "farbtastic.js"
, "prefixfree.js"
]
, "timeslider.js": [
"jquery.js"
, "security.js"
, "plugins.js"
, "undo-xpopup.js"
, "json2.js"
, "colorutils.js"
, "draggable.js"
, "ace2_common.js"
, "pad_utils.js"
, "pad_cookie.js"
, "pad_editor.js"
, "pad_editbar.js"
, "pad_docbar.js"
, "pad_modals.js"
, "pad_savedrevs.js"
, "pad_impexp.js"
, "AttributePoolFactory.js"
, "Changeset.js"
, "domline.js"
, "linestylefilter.js"
, "cssmanager.js"
, "broadcast.js"
, "broadcast_slider.js"
, "broadcast_revisions.js"
, "timeslider.js"
]
, "ace2_inner.js": [
"ace2_common.js"
, "AttributePoolFactory.js"
, "Changeset.js"
, "security.js"
, "skiplist.js"
, "virtual_lines.js"
, "cssmanager.js"
, "colorutils.js"
, "undomodule.js"
, "contentcollector.js"
, "changesettracker.js"
, "linestylefilter.js"
, "domline.js"
, "ace2_inner.js"
]
}