diff --git a/.gitignore b/.gitignore
index ab91434bd..50cd6e212 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,6 @@ bin/abiword.exe
bin/node.exe
etherpad-lite-win.zip
var/dirty.db
-bin/convertSettings.json
\ No newline at end of file
+bin/convertSettings.json
+*~
+*.patch
\ No newline at end of file
diff --git a/README.md b/README.md
index e075d8212..b21cd8e82 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,14 @@ documented codebase makes it easier for developers to improve the code and contr
Etherpad Lite is optimized to be easy embeddable. It provides a [HTTP API](https://github.com/Pita/etherpad-lite/wiki/HTTP-API)
that allows your web application to manage pads, users and groups.
-You can use this [PHP Client](https://github.com/TomNomNom/etherpad-lite-client) to work with the API
-(If you don't want to use PHP, feel free to create a client for your favourite web development language).
+There are several clients in for this API:
+
+* [PHP](https://github.com/TomNomNom/etherpad-lite-client), thx to [TomNomNom](https://github.com/TomNomNom)
+* [.Net](https://github.com/ja-jo/EtherpadLiteDotNet), thx to [ja-jo](https://github.com/ja-jo)
+* [Node.js](https://github.com/tomassedovic/etherpad-lite-client-js), thx to [tomassedovic](https://github.com/tomassedovic)
+* [Ruby](https://github.com/jhollinger/ruby-etherpad-lite), thx to [jhollinger](https://github.com/jhollinger)
+* [Python](https://github.com/devjones/PyEtherpadLite), thx to [devjones](https://github.com/devjones)
+
There is also a [jQuery plugin](https://github.com/johnyma22/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website
**Online demo**
@@ -51,15 +57,16 @@ Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
**As root:**
- Install the dependencies. We need the gzip, git, curl, libssl develop libraries and python apt-get install gzip git-core curl python libssl-dev
+ Install the dependencies. We need gzip, git, curl, libssl develop libraries, python and gcc. For Debian/Ubuntu apt-get install gzip git-core curl python libssl-dev build-essential
+ For Fedora/CentOS yum install gzip git-core curl python openssl-dev && yum groupinstall "Development Tools"
+
Install node.js
- Download the latest 0.4.x node.js release from http://nodejs.org/#download
- Extract it with tar xf node-v0.4*
- Move into the node folder cd node-v0.4*
and build node with ./configure && make && make install
+ Download the latest 0.6.x node.js release from http://nodejs.org/#download
+ Extract it with tar xf node-v0.6*
+ Move into the node folder cd node-v0.6*
and build node with ./configure && make && make install
- Install npm curl http://npmjs.org/install.sh | sh
**As any user (we recommend creating a separate user called etherpad-lite):**
@@ -74,6 +81,8 @@ Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
## Next Steps
You can modify the settings in the file `settings.json`
+You should use a dedicated database such as "mysql" if you are planning on using etherpad-lite in a production environment, the "dirty" database driver is only for testing and/or development purposes.
+
You can update to the latest version with `git pull origin`. The next start with bin/run.sh will update the dependencies
Look at this wiki pages:
diff --git a/bin/buildForWindows.sh b/bin/buildForWindows.sh
index 1c693fa55..8e6ce4f6e 100755
--- a/bin/buildForWindows.sh
+++ b/bin/buildForWindows.sh
@@ -1,6 +1,6 @@
#!/bin/sh
-NODE_VERSION="0.5.4"
+NODE_VERSION="0.6.1"
#Move to the folder where ep-lite is installed
cd `dirname $0`
@@ -48,16 +48,6 @@ cp -rL node_modules node_modules_resolved
rm -rf node_modules
mv node_modules_resolved node_modules
-echo "remove sqlite, cause we can't use it with windows..."
-rm -rf node_modules/ueberDB/node_modules/sqlite3
-
-echo "replace log4js with a patched log4js, this log4js runs on windows too..."
-rm -rf node_modules/log4js/*
-wget https://github.com/Pita/log4js-node/zipball/master -O log4js.zip
-unzip log4js.zip
-mv Pita-log4js-node*/* node_modules/log4js
-rm -rf log4js.zip Pita-log4js-node*
-
echo "download windows node..."
cd bin
wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O node.exe
diff --git a/bin/convert.js b/bin/convert.js
index 94e9877a3..d410f7a4a 100644
--- a/bin/convert.js
+++ b/bin/convert.js
@@ -52,7 +52,7 @@ async.series([
{
log("get all padIds out of the database...");
- etherpadDB.query("SELECT ID FROM PAD_META LIMIT", [], function(err, _padIDs)
+ etherpadDB.query("SELECT ID FROM PAD_META", [], function(err, _padIDs)
{
padIDs = _padIDs;
callback(err);
@@ -153,11 +153,14 @@ function convertPad(padId, callback)
{
if(!err)
{
- //parse the pages
- for(var i=0,length=results.length;i /dev/null 2>&1 || {
exit 1
}
-#check node version
-NODE_VERSION=$(node --version)
-if [ ! $(echo $NODE_VERSION | cut -d "." -f 1-2) = "v0.4" ]; then
- echo "You're running a wrong version of node, you're using $NODE_VERSION, we need v0.4.x" >&2
- exit 1
-fi
-
#Is npm installed?
hash npm > /dev/null 2>&1 || {
echo "Please install npm ( http://npmjs.org )" >&2
@@ -54,9 +47,9 @@ npm install || {
echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true"
-NEEDED_VERSION="1.6.2"
+NEEDED_VERSION="1.7"
if [ -f "static/js/jquery.min.js" ]; then
- VERSION=$(cat static/js/jquery.min.js | head -n 2 | tail -n 1 | grep -o "v[0-9]*\.[0-9]*\.[0-9]*");
+ VERSION=$(cat static/js/jquery.min.js | head -n 3 | grep -o "v[0-9].[0-9]");
if [ ${VERSION#v} = $NEEDED_VERSION ]; then
DOWNLOAD_JQUERY="false"
diff --git a/node/db/API.js b/node/db/API.js
index 9912b098d..c40c49cac 100644
--- a/node/db/API.js
+++ b/node/db/API.js
@@ -25,6 +25,9 @@ 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*****/
@@ -169,6 +172,110 @@ exports.setText = function(padID, text, callback)
});
}
+/**
+getHTML(padID, [rev]) returns the html of a pad
+
+Example returns:
+
+{code: 0, message:"ok", data: {text:"Welcome Text "}}
+{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({stop: "rev is not a number"});
+ return;
+ }
+ }
+
+ if(rev !== undefined && rev < 0)
+ {
+ callback({stop: "rev is a negative number"});
+ return;
+ }
+
+ if(rev !== undefined && !is_int(rev))
+ {
+ callback({stop: "rev is a float value"});
+ return;
+ }
+
+ getPadSafe(padID, true, function(err, pad)
+ {
+ if(err)
+ {
+ callback(err);
+ return;
+ }
+
+ //the client asked for a special revision
+ if(rev !== undefined)
+ {
+ //check if this is a valid revision
+ if(rev > pad.getHeadRevisionNumber())
+ {
+ callback({stop: "rev is higher than the head revision of the pad"});
+ return;
+ }
+
+ //get the html of this revision
+ exportHtml.getPadHTML(pad, rev, function(err, html)
+ {
+ if(!err)
+ {
+ data = {html: html};
+ }
+ callback(err, data);
+ });
+ }
+ //the client wants the latest text, lets return it to him
+ else
+ {
+ exportHtml.getPadHTML(pad, undefined, function (err, html)
+ {
+ if(!err)
+ {
+ data = {html: html};
+ }
+ callback(err, data);
+ });
+ }
+ });
+}
+
+exports.setHTML = function(padID, html, callback)
+{
+ //get the pad
+ getPadSafe(padID, true, function(err, pad)
+ {
+ if(err)
+ {
+ callback(err);
+ 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 */
/*****************/
diff --git a/node/db/PadManager.js b/node/db/PadManager.js
index 8af299ccc..4e16c7c45 100644
--- a/node/db/PadManager.js
+++ b/node/db/PadManager.js
@@ -21,10 +21,20 @@
require("../db/Pad");
var db = require("./DB").db;
-/**
- * A Array with all known Pads
+/**
+ * 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.
*/
-globalPads = [];
+globalPads = {
+ get: function (name) { return this[':'+name]; },
+ set: function (name, value) { this[':'+name] = value; },
+ remove: function (name) { delete this[':'+name]; }
+};
/**
* Returns a Pad Object with the callback
@@ -65,7 +75,7 @@ exports.getPad = function(id, text, callback)
}
}
- var pad = globalPads[id];
+ var pad = globalPads.get(id);
//return pad if its already loaded
if(pad != null)
@@ -86,7 +96,7 @@ exports.getPad = function(id, text, callback)
}
else
{
- globalPads[id] = pad;
+ globalPads.set(id, pad);
callback(null, pad);
}
});
@@ -110,6 +120,6 @@ exports.isValidPadId = function(padId)
//removes a pad from the array
exports.unloadPad = function(padId)
{
- if(globalPads[padId])
- delete globalPads[padId];
+ if(globalPads.get(padId))
+ globalPads.remove(padId);
}
diff --git a/node/db/SecurityManager.js b/node/db/SecurityManager.js
index 7ad8f8d25..0f35d8735 100644
--- a/node/db/SecurityManager.js
+++ b/node/db/SecurityManager.js
@@ -23,6 +23,7 @@ var async = require("async");
var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
+var settings = require("../utils/Settings")
/**
* This function controlls the access to a pad, it checks if the user can access a pad.
@@ -34,18 +35,52 @@ var sessionManager = require("./SessionManager");
*/
exports.checkAccess = function (padID, sessionID, token, password, callback)
{
- // it's not a group pad, means we can grant access
- if(padID.indexOf("$") == -1)
+ var statusObject;
+
+ // a valid session is required (api-only mode)
+ if(settings.requireSession)
{
- //get author for this token
- authorManager.getAuthor4Token(token, function(err, author)
+ // no sessionID, access is denied
+ if(!sessionID)
{
- // grant access, with author of token
- callback(err, {accessStatus: "grant", authorID: author});
- })
-
- //don't continue
- return;
+ 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)
+ {
+ // 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)
+ {
+ // pad doesn't exist - user can't have access
+ if(!exists) statusObject.accessStatus = "deny";
+ // grant or deny access, with author of token
+ callback(err, statusObject);
+ });
+ }
+ // user may create new pads - no need to check anything
+ else
+ {
+ // grant access, with author of token
+ callback(err, statusObject);
+ }
+ })
+
+ //don't continue
+ return;
+ }
}
var groupID = padID.split("$")[0];
@@ -57,8 +92,6 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
var isPasswordProtected;
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
- var statusObject;
-
async.series([
//get basic informations from the database
function(callback)
@@ -180,6 +213,8 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
{
//--> 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)
diff --git a/node/handler/APIHandler.js b/node/handler/APIHandler.js
index 34d63f18d..2159cc406 100644
--- a/node/handler/APIHandler.js
+++ b/node/handler/APIHandler.js
@@ -50,6 +50,8 @@ var functions = {
"listSessionsOfAuthor" : ["authorID"],
"getText" : ["padID", "rev"],
"setText" : ["padID", "text"],
+ "getHTML" : ["padID", "rev"],
+ "setHTML" : ["padID", "html"],
"getRevisionsCount" : ["padID"],
"deletePad" : ["padID"],
"getReadOnlyID" : ["padID"],
@@ -69,7 +71,7 @@ var functions = {
exports.handle = function(functionName, fields, req, res)
{
//check the api key!
- if(fields["apikey"] != apikey)
+ if(fields["apikey"] != apikey.trim())
{
res.send({code: 4, message: "no or wrong API Key", data: null});
return;
diff --git a/node/handler/ExportHandler.js b/node/handler/ExportHandler.js
index a87219bb7..8cc74747d 100644
--- a/node/handler/ExportHandler.js
+++ b/node/handler/ExportHandler.js
@@ -81,10 +81,9 @@ exports.doExport = function(req, res, padId, type)
res.send(html);
callback("stop");
}
- //write the html export to a file
- else
+ else //write the html export to a file
{
- randNum = Math.floor(Math.random()*new Date().getTime());
+ randNum = Math.floor(Math.random()*0xFFFFFFFF);
srcFile = tempDirectory + "/eplite_export_" + randNum + ".html";
fs.writeFile(srcFile, html, callback);
}
@@ -114,10 +113,13 @@ exports.doExport = function(req, res, padId, type)
function(callback)
{
//100ms delay to accomidate for slow windows fs
- setTimeout(function()
- {
- fs.unlink(destFile, callback);
- }, 100);
+ if(os.type().indexOf("Windows") > -1)
+ {
+ setTimeout(function()
+ {
+ fs.unlink(destFile, callback);
+ }, 100);
+ }
}
], callback);
}
diff --git a/node/handler/ImportHandler.js b/node/handler/ImportHandler.js
index 8936e488a..283d105dd 100644
--- a/node/handler/ImportHandler.js
+++ b/node/handler/ImportHandler.js
@@ -61,10 +61,19 @@ exports.doImport = function(req, res, padId)
form.parse(req, function(err, fields, files)
{
- //save the path of the uploaded file
- srcFile = files.file.path;
-
- callback(err);
+ //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();
+ }
});
},
@@ -72,7 +81,7 @@ exports.doImport = function(req, res, padId)
//this allows us to accept source code files like .c or .java
function(callback)
{
- var fileEnding = srcFile.split(".")[1];
+ var fileEnding = srcFile.split(".")[1].toLowerCase();
var knownFileEndings = ["txt", "doc", "docx", "pdf", "odt", "html", "htm"];
//find out if this is a known file ending
@@ -103,7 +112,7 @@ exports.doImport = function(req, res, padId)
//convert file to text
function(callback)
{
- var randNum = Math.floor(Math.random()*new Date().getTime());
+ var randNum = Math.floor(Math.random()*0xFFFFFFFF);
destFile = tempDirectory + "eplite_import_" + randNum + ".txt";
abiword.convertFile(srcFile, destFile, "txt", callback);
},
@@ -127,10 +136,13 @@ exports.doImport = function(req, res, padId)
//node on windows has a delay on releasing of the file lock.
//We add a 100ms delay to work around this
- setTimeout(function()
- {
- callback(err);
- }, 100);
+ if(os.type().indexOf("Windows") > -1)
+ {
+ setTimeout(function()
+ {
+ callback(err);
+ }, 100);
+ }
});
},
@@ -157,6 +169,13 @@ exports.doImport = function(req, res, padId)
}
], function(err)
{
+ //the upload failed, there is nothing we can do, send a 500
+ if(err == "uploadFailed")
+ {
+ res.send(500);
+ return;
+ }
+
if(err) throw err;
//close the connection
diff --git a/node/handler/PadMessageHandler.js b/node/handler/PadMessageHandler.js
index f89f4934c..188da5aea 100644
--- a/node/handler/PadMessageHandler.js
+++ b/node/handler/PadMessageHandler.js
@@ -136,7 +136,7 @@ exports.handleDisconnect = function(client)
{
if(pad2sessions[sessionPad][i] == client.id)
{
- delete pad2sessions[sessionPad][i];
+ pad2sessions[sessionPad].splice(i, 1);
break;
}
}
@@ -190,10 +190,10 @@ exports.handleMessage = function(client, message)
{
handleSuggestUserName(client, message);
}
- //if the message type is unkown, throw an exception
+ //if the message type is unknown, throw an exception
else
{
- messageLogger.warn("Droped message, unkown Message Type " + message.type);
+ messageLogger.warn("Dropped message, unknown Message Type " + message.type);
}
}
@@ -272,12 +272,12 @@ function handleSuggestUserName(client, message)
//check if all ok
if(message.data.payload.newName == null)
{
- messageLogger.warn("Droped message, suggestUserName Message has no newName!");
+ messageLogger.warn("Dropped message, suggestUserName Message has no newName!");
return;
}
if(message.data.payload.unnamedId == null)
{
- messageLogger.warn("Droped message, suggestUserName Message has no unnamedId!");
+ messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!");
return;
}
@@ -304,7 +304,7 @@ function handleUserInfoUpdate(client, message)
//check if all ok
if(message.data.userInfo.colorId == null)
{
- messageLogger.warn("Droped message, USERINFO_UPDATE Message has no colorId!");
+ messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!");
return;
}
@@ -348,17 +348,17 @@ function handleUserChanges(client, message)
//check if all ok
if(message.data.baseRev == null)
{
- messageLogger.warn("Droped message, USER_CHANGES Message has no baseRev!");
+ messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!");
return;
}
if(message.data.apool == null)
{
- messageLogger.warn("Droped message, USER_CHANGES Message has no apool!");
+ messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!");
return;
}
if(message.data.changeset == null)
{
- messageLogger.warn("Droped message, USER_CHANGES Message has no changeset!");
+ messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!");
return;
}
@@ -600,22 +600,22 @@ function handleClientReady(client, message)
//check if all ok
if(!message.token)
{
- messageLogger.warn("Droped message, CLIENT_READY Message has no token!");
+ messageLogger.warn("Dropped message, CLIENT_READY Message has no token!");
return;
}
if(!message.padId)
{
- messageLogger.warn("Droped message, CLIENT_READY Message has no padId!");
+ messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!");
return;
}
if(!message.protocolVersion)
{
- messageLogger.warn("Droped message, CLIENT_READY Message has no protocolVersion!");
+ messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!");
return;
}
if(message.protocolVersion != 2)
{
- messageLogger.warn("Droped message, CLIENT_READY Message has a unkown protocolVersion '" + message.protocolVersion + "'!");
+ messageLogger.warn("Dropped message, CLIENT_READY Message has a unknown protocolVersion '" + message.protocolVersion + "'!");
return;
}
@@ -806,8 +806,20 @@ function handleClientReady(client, message)
clientVars.userName = authorName;
}
- //Send the clientVars to the Client
- client.json.send(clientVars);
+ //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].rev = pad.getHeadRevisionNumber();
diff --git a/node/handler/SocketIORouter.js b/node/handler/SocketIORouter.js
index 2d8f4f44d..00e3e4f3f 100644
--- a/node/handler/SocketIORouter.js
+++ b/node/handler/SocketIORouter.js
@@ -128,7 +128,7 @@ exports.setSocketIO = function(_socket)
//drop message
else
{
- messageLogger.warn("Droped message cause of bad permissions:" + stringifyWithoutPassword(message));
+ messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message));
}
}
});
diff --git a/node/handler/TimesliderMessageHandler.js b/node/handler/TimesliderMessageHandler.js
index a06ab54c8..86864977f 100644
--- a/node/handler/TimesliderMessageHandler.js
+++ b/node/handler/TimesliderMessageHandler.js
@@ -77,7 +77,7 @@ exports.handleMessage = function(client, message)
//if the message type is unkown, throw an exception
else
{
- messageLogger.warn("Droped message, unkown Message Type: '" + message.type + "'");
+ messageLogger.warn("Dropped message, unknown Message Type: '" + message.type + "'");
}
}
@@ -85,7 +85,7 @@ function handleClientReady(client, message)
{
if(message.padId == null)
{
- messageLogger.warn("Droped message, changeset request has no padId!");
+ messageLogger.warn("Dropped message, changeset request has no padId!");
return;
}
@@ -106,27 +106,27 @@ function handleChangesetRequest(client, message)
//check if all ok
if(message.data == null)
{
- messageLogger.warn("Droped message, changeset request has no data!");
+ messageLogger.warn("Dropped message, changeset request has no data!");
return;
}
if(message.padId == null)
{
- messageLogger.warn("Droped message, changeset request has no padId!");
+ messageLogger.warn("Dropped message, changeset request has no padId!");
return;
}
if(message.data.granularity == null)
{
- messageLogger.warn("Droped message, changeset request has no granularity!");
+ messageLogger.warn("Dropped message, changeset request has no granularity!");
return;
}
if(message.data.start == null)
{
- messageLogger.warn("Droped message, changeset request has no start!");
+ messageLogger.warn("Dropped message, changeset request has no start!");
return;
}
if(message.data.requestID == null)
{
- messageLogger.warn("Droped message, changeset request has no requestID!");
+ messageLogger.warn("Dropped message, changeset request has no requestID!");
return;
}
diff --git a/node/server.js b/node/server.js
index 2bebe6a24..08c09ab0e 100644
--- a/node/server.js
+++ b/node/server.js
@@ -91,7 +91,10 @@ async.waterfall([
var httpLogger = log4js.getLogger("http");
app.configure(function()
{
- app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
+ // 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"))
+ app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
app.use(express.cookieParser());
});
@@ -287,14 +290,13 @@ async.waterfall([
});
var apiLogger = log4js.getLogger("API");
-
- //This is a api call, collect all post informations and pass it to the apiHandler
- app.get('/api/1/:func', function(req, res)
- {
+
+ //This is for making an api call, collecting all post information and passing it to the apiHandler
+ var apiCaller = function(req, res, fields) {
res.header("Server", serverName);
res.header("Content-Type", "application/json; charset=utf-8");
- apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(req.query));
+ apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
//wrap the send function so we can log the response
res._send = res.send;
@@ -311,7 +313,22 @@ async.waterfall([
}
//call the api handler
- apiHandler.handle(req.params.func, req.query, req, res);
+ apiHandler.handle(req.params.func, fields, req, res);
+ }
+
+ //This is a api GET call, collect all post informations and pass it to the apiHandler
+ 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
+ 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
@@ -425,19 +442,19 @@ async.waterfall([
io.set('logger', {
debug: function (str)
{
- socketIOLogger.debug(str);
+ socketIOLogger.debug.apply(socketIOLogger, arguments);
},
info: function (str)
{
- socketIOLogger.info(str);
+ socketIOLogger.info.apply(socketIOLogger, arguments);
},
warn: function (str)
{
- socketIOLogger.warn(str);
+ socketIOLogger.warn.apply(socketIOLogger, arguments);
},
error: function (str)
{
- socketIOLogger.error(str);
+ socketIOLogger.error.apply(socketIOLogger, arguments);
},
});
diff --git a/node/utils/ExportHtml.js b/node/utils/ExportHtml.js
index dce156ec8..782bb7016 100644
--- a/node/utils/ExportHtml.js
+++ b/node/utils/ExportHtml.js
@@ -85,6 +85,8 @@ function getPadHTML(pad, revNum, callback)
});
}
+exports.getPadHTML = getPadHTML;
+
function getHTMLFromAtext(pad, atext)
{
var apool = pad.apool();
@@ -119,8 +121,10 @@ function getHTMLFromAtext(pad, atext)
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('>');
@@ -128,10 +132,27 @@ function getHTMLFromAtext(pad, atext)
function emitCloseTag(i)
{
+ openTags.shift();
assem.append('');
assem.append(tags[i]);
assem.append('>');
}
+
+ function orderdCloseTags(tags2close)
+ {
+ for(var i=0;i= 0; i--)
{
if (propVals[i] === LEAVE)
{
- emitCloseTag(i);
+ //emitCloseTag(i);
+ tags2close.push(i);
propVals[i] = false;
}
else if (propVals[i] === STAY)
{
- emitCloseTag(i);
+ //emitCloseTag(i);
+ tags2close.push(i);
}
}
+
+ orderdCloseTags(tags2close);
+
for (var i = 0; i < propVals.length; i++)
{
if (propVals[i] === ENTER || propVals[i] === STAY)
@@ -231,18 +259,27 @@ function getHTMLFromAtext(pad, atext)
{
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(_escapeHTML(s));
} // end iteration over spans in line
+
+ var tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--)
{
if (propVals[i])
{
- emitCloseTag(i);
+ tags2close.push(i);
propVals[i] = false;
}
}
+
+ orderdCloseTags(tags2close);
} // end processNextChars
if (urls)
{
diff --git a/node/utils/ImportHtml.js b/node/utils/ImportHtml.js
new file mode 100644
index 000000000..6441708e1
--- /dev/null
+++ b/node/utils/ImportHtml.js
@@ -0,0 +1,92 @@
+/**
+ * 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').jsdom;
+var log4js = require('log4js');
+
+var Changeset = require("./Changeset");
+var contentcollector = require("./contentcollector");
+var map = require("../../static/js/ace2_common.js").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+<'));
+ 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;
diff --git a/node/utils/Settings.js b/node/utils/Settings.js
index 1d855a53d..2aef834d6 100644
--- a/node/utils/Settings.js
+++ b/node/utils/Settings.js
@@ -42,6 +42,17 @@ exports.dbSettings = { "filename" : "../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
*/
diff --git a/node/utils/contentcollector.js b/node/utils/contentcollector.js
new file mode 100644
index 000000000..60bd0a6ef
--- /dev/null
+++ b/node/utils/contentcollector.js
@@ -0,0 +1,692 @@
+// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
+// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
+// %APPJET%: import("etherpad.admin.plugins");
+/**
+ * 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 Changeset = require("../utils/Changeset");
+
+var _MAX_LIST_LEVEL = 8;
+
+function sanitizeUnicode(s)
+{
+ return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?');
+}
+
+function makeContentCollector(collectStyles, browser, apool, domInterface, className2Author)
+{
+ browser = browser || {};
+
+ var plugins_;
+ if (typeof(plugins) != 'undefined')
+ {
+ plugins_ = plugins;
+ }
+ else
+ {
+ plugins_ = {callHook: function () {}};
+ }
+
+ var dom = domInterface || {
+ isNodeText: function(n)
+ {
+ return (n.nodeType == 3);
+ },
+ nodeTagName: function(n)
+ {
+ return n.tagName;
+ },
+ nodeValue: function(n)
+ {
+ return n.nodeValue;
+ },
+ nodeNumChildren: function(n)
+ {
+ return n.childNodes.length;
+ },
+ nodeChild: function(n, i)
+ {
+ return n.childNodes.item(i);
+ },
+ nodeProp: function(n, p)
+ {
+ return n[p];
+ },
+ nodeAttr: function(n, a)
+ {
+ return n.getAttribute(a);
+ },
+ optNodeInnerHTML: function(n)
+ {
+ return n.innerHTML;
+ }
+ };
+
+ var _blockElems = {
+ "div": 1,
+ "p": 1,
+ "pre": 1,
+ "li": 1
+ };
+
+ function isBlockElement(n)
+ {
+ return !!_blockElems[(dom.nodeTagName(n) || "").toLowerCase()];
+ }
+
+ function textify(str)
+ {
+ return sanitizeUnicode(
+ str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '));
+ }
+
+ function getAssoc(node, name)
+ {
+ return dom.nodeProp(node, "_magicdom_" + name);
+ }
+
+ var lines = (function()
+ {
+ var textArray = [];
+ var attribsArray = [];
+ var attribsBuilder = null;
+ var op = Changeset.newOp('+');
+ var self = {
+ length: function()
+ {
+ return textArray.length;
+ },
+ atColumnZero: function()
+ {
+ return textArray[textArray.length - 1] === "";
+ },
+ startNew: function()
+ {
+ textArray.push("");
+ self.flush(true);
+ attribsBuilder = Changeset.smartOpAssembler();
+ },
+ textOfLine: function(i)
+ {
+ return textArray[i];
+ },
+ appendText: function(txt, attrString)
+ {
+ textArray[textArray.length - 1] += txt;
+ //dmesg(txt+" / "+attrString);
+ op.attribs = attrString;
+ op.chars = txt.length;
+ attribsBuilder.append(op);
+ },
+ textLines: function()
+ {
+ return textArray.slice();
+ },
+ attribLines: function()
+ {
+ return attribsArray;
+ },
+ // call flush only when you're done
+ flush: function(withNewline)
+ {
+ if (attribsBuilder)
+ {
+ attribsArray.push(attribsBuilder.toString());
+ attribsBuilder = null;
+ }
+ }
+ };
+ self.startNew();
+ return self;
+ }());
+ var cc = {};
+
+ function _ensureColumnZero(state)
+ {
+ if (!lines.atColumnZero())
+ {
+ cc.startNewLine(state);
+ }
+ }
+ var selection, startPoint, endPoint;
+ var selStart = [-1, -1],
+ selEnd = [-1, -1];
+ var blockElems = {
+ "div": 1,
+ "p": 1,
+ "pre": 1
+ };
+
+ function _isEmpty(node, state)
+ {
+ // consider clean blank lines pasted in IE to be empty
+ if (dom.nodeNumChildren(node) == 0) return true;
+ if (dom.nodeNumChildren(node) == 1 && getAssoc(node, "shouldBeEmpty") && dom.optNodeInnerHTML(node) == " " && !getAssoc(node, "unpasted"))
+ {
+ if (state)
+ {
+ var child = dom.nodeChild(node, 0);
+ _reachPoint(child, 0, state);
+ _reachPoint(child, 1, state);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ function _pointHere(charsAfter, state)
+ {
+ var ln = lines.length() - 1;
+ var chr = lines.textOfLine(ln).length;
+ if (chr == 0 && state.listType && state.listType != 'none')
+ {
+ chr += 1; // listMarker
+ }
+ chr += charsAfter;
+ return [ln, chr];
+ }
+
+ function _reachBlockPoint(nd, idx, state)
+ {
+ if (!dom.isNodeText(nd)) _reachPoint(nd, idx, state);
+ }
+
+ function _reachPoint(nd, idx, state)
+ {
+ if (startPoint && nd == startPoint.node && startPoint.index == idx)
+ {
+ selStart = _pointHere(0, state);
+ }
+ if (endPoint && nd == endPoint.node && endPoint.index == idx)
+ {
+ selEnd = _pointHere(0, state);
+ }
+ }
+ cc.incrementFlag = function(state, flagName)
+ {
+ state.flags[flagName] = (state.flags[flagName] || 0) + 1;
+ }
+ cc.decrementFlag = function(state, flagName)
+ {
+ state.flags[flagName]--;
+ }
+ cc.incrementAttrib = function(state, attribName)
+ {
+ if (!state.attribs[attribName])
+ {
+ state.attribs[attribName] = 1;
+ }
+ else
+ {
+ state.attribs[attribName]++;
+ }
+ _recalcAttribString(state);
+ }
+ cc.decrementAttrib = function(state, attribName)
+ {
+ state.attribs[attribName]--;
+ _recalcAttribString(state);
+ }
+
+ function _enterList(state, listType)
+ {
+ var oldListType = state.listType;
+ state.listLevel = (state.listLevel || 0) + 1;
+ if (listType != 'none')
+ {
+ state.listNesting = (state.listNesting || 0) + 1;
+ }
+ state.listType = listType;
+ _recalcAttribString(state);
+ return oldListType;
+ }
+
+ function _exitList(state, oldListType)
+ {
+ state.listLevel--;
+ if (state.listType != 'none')
+ {
+ state.listNesting--;
+ }
+ state.listType = oldListType;
+ _recalcAttribString(state);
+ }
+
+ function _enterAuthor(state, author)
+ {
+ var oldAuthor = state.author;
+ state.authorLevel = (state.authorLevel || 0) + 1;
+ state.author = author;
+ _recalcAttribString(state);
+ return oldAuthor;
+ }
+
+ function _exitAuthor(state, oldAuthor)
+ {
+ state.authorLevel--;
+ state.author = oldAuthor;
+ _recalcAttribString(state);
+ }
+
+ function _recalcAttribString(state)
+ {
+ var lst = [];
+ for (var a in state.attribs)
+ {
+ if (state.attribs[a])
+ {
+ lst.push([a, 'true']);
+ }
+ }
+ if (state.authorLevel > 0)
+ {
+ var authorAttrib = ['author', state.author];
+ if (apool.putAttrib(authorAttrib, true) >= 0)
+ {
+ // require that author already be in pool
+ // (don't add authors from other documents, etc.)
+ lst.push(authorAttrib);
+ }
+ }
+ state.attribString = Changeset.makeAttribsString('+', lst, apool);
+ }
+
+ function _produceListMarker(state)
+ {
+ lines.appendText('*', Changeset.makeAttribsString('+', [
+ ['list', state.listType],
+ ['insertorder', 'first']
+ ], apool));
+ }
+ cc.startNewLine = function(state)
+ {
+ if (state)
+ {
+ var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0;
+ if (atBeginningOfLine && state.listType && state.listType != 'none')
+ {
+ _produceListMarker(state);
+ }
+ }
+ lines.startNew();
+ }
+ cc.notifySelection = function(sel)
+ {
+ if (sel)
+ {
+ selection = sel;
+ startPoint = selection.startPoint;
+ endPoint = selection.endPoint;
+ }
+ };
+ cc.doAttrib = function(state, na)
+ {
+ state.localAttribs = (state.localAttribs || []);
+ state.localAttribs.push(na);
+ cc.incrementAttrib(state, na);
+ };
+ cc.collectContent = function(node, state)
+ {
+ if (!state)
+ {
+ state = {
+ flags: { /*name -> nesting counter*/
+ },
+ localAttribs: null,
+ attribs: { /*name -> nesting counter*/
+ },
+ attribString: ''
+ };
+ }
+ var localAttribs = state.localAttribs;
+ state.localAttribs = null;
+ var isBlock = isBlockElement(node);
+ var isEmpty = _isEmpty(node, state);
+ if (isBlock) _ensureColumnZero(state);
+ var startLine = lines.length() - 1;
+ _reachBlockPoint(node, 0, state);
+ if (dom.isNodeText(node))
+ {
+ var txt = dom.nodeValue(node);
+ var rest = '';
+ var x = 0; // offset into original text
+ if (txt.length == 0)
+ {
+ if (startPoint && node == startPoint.node)
+ {
+ selStart = _pointHere(0, state);
+ }
+ if (endPoint && node == endPoint.node)
+ {
+ selEnd = _pointHere(0, state);
+ }
+ }
+ while (txt.length > 0)
+ {
+ var consumed = 0;
+ if (state.flags.preMode)
+ {
+ var firstLine = txt.split('\n', 1)[0];
+ consumed = firstLine.length + 1;
+ rest = txt.substring(consumed);
+ txt = firstLine;
+ }
+ else
+ { /* will only run this loop body once */
+ }
+ if (startPoint && node == startPoint.node && startPoint.index - x <= txt.length)
+ {
+ selStart = _pointHere(startPoint.index - x, state);
+ }
+ if (endPoint && node == endPoint.node && endPoint.index - x <= txt.length)
+ {
+ selEnd = _pointHere(endPoint.index - x, state);
+ }
+ var txt2 = txt;
+ if ((!state.flags.preMode) && /^[\r\n]*$/.exec(txt))
+ {
+ // prevents textnodes containing just "\n" from being significant
+ // in safari when pasting text, now that we convert them to
+ // spaces instead of removing them, because in other cases
+ // removing "\n" from pasted HTML will collapse words together.
+ txt2 = "";
+ }
+ var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0;
+ if (atBeginningOfLine)
+ {
+ // newlines in the source mustn't become spaces at beginning of line box
+ txt2 = txt2.replace(/^\n*/, '');
+ }
+ if (atBeginningOfLine && state.listType && state.listType != 'none')
+ {
+ _produceListMarker(state);
+ }
+ lines.appendText(textify(txt2), state.attribString);
+ x += consumed;
+ txt = rest;
+ if (txt.length > 0)
+ {
+ cc.startNewLine(state);
+ }
+ }
+ }
+ else
+ {
+ var tname = (dom.nodeTagName(node) || "").toLowerCase();
+ if (tname == "br")
+ {
+ cc.startNewLine(state);
+ }
+ else if (tname == "script" || tname == "style")
+ {
+ // ignore
+ }
+ else if (!isEmpty)
+ {
+ var styl = dom.nodeAttr(node, "style");
+ var cls = dom.nodeProp(node, "className");
+
+ var isPre = (tname == "pre");
+ if ((!isPre) && browser.safari)
+ {
+ isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
+ }
+ if (isPre) cc.incrementFlag(state, 'preMode');
+ var oldListTypeOrNull = null;
+ var oldAuthorOrNull = null;
+ if (collectStyles)
+ {
+ plugins_.callHook('collectContentPre', {
+ cc: cc,
+ state: state,
+ tname: tname,
+ styl: styl,
+ cls: cls
+ });
+ if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || tname == "strong")
+ {
+ cc.doAttrib(state, "bold");
+ }
+ if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || tname == "em")
+ {
+ cc.doAttrib(state, "italic");
+ }
+ if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || tname == "ins")
+ {
+ cc.doAttrib(state, "underline");
+ }
+ if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || tname == "del")
+ {
+ cc.doAttrib(state, "strikethrough");
+ }
+ if (tname == "ul")
+ {
+ var type;
+ var rr = cls && /(?:^| )list-(bullet[12345678])\b/.exec(cls);
+ type = rr && rr[1] || "bullet" + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
+ oldListTypeOrNull = (_enterList(state, type) || 'none');
+ }
+ else if ((tname == "div" || tname == "p") && cls && cls.match(/(?:^| )ace-line\b/))
+ {
+ oldListTypeOrNull = (_enterList(state, type) || 'none');
+ }
+ if (className2Author && cls)
+ {
+ var classes = cls.match(/\S+/g);
+ if (classes && classes.length > 0)
+ {
+ for (var i = 0; i < classes.length; i++)
+ {
+ var c = classes[i];
+ var a = className2Author(c);
+ if (a)
+ {
+ oldAuthorOrNull = (_enterAuthor(state, a) || 'none');
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ var nc = dom.nodeNumChildren(node);
+ for (var i = 0; i < nc; i++)
+ {
+ var c = dom.nodeChild(node, i);
+ cc.collectContent(c, state);
+ }
+
+ if (collectStyles)
+ {
+ plugins_.callHook('collectContentPost', {
+ cc: cc,
+ state: state,
+ tname: tname,
+ styl: styl,
+ cls: cls
+ });
+ }
+
+ if (isPre) cc.decrementFlag(state, 'preMode');
+ if (state.localAttribs)
+ {
+ for (var i = 0; i < state.localAttribs.length; i++)
+ {
+ cc.decrementAttrib(state, state.localAttribs[i]);
+ }
+ }
+ if (oldListTypeOrNull)
+ {
+ _exitList(state, oldListTypeOrNull);
+ }
+ if (oldAuthorOrNull)
+ {
+ _exitAuthor(state, oldAuthorOrNull);
+ }
+ }
+ }
+ if (!browser.msie)
+ {
+ _reachBlockPoint(node, 1, state);
+ }
+ if (isBlock)
+ {
+ if (lines.length() - 1 == startLine)
+ {
+ cc.startNewLine(state);
+ }
+ else
+ {
+ _ensureColumnZero(state);
+ }
+ }
+
+ if (browser.msie)
+ {
+ // in IE, a point immediately after a DIV appears on the next line
+ _reachBlockPoint(node, 1, state);
+ }
+
+ state.localAttribs = localAttribs;
+ };
+ // can pass a falsy value for end of doc
+ cc.notifyNextNode = function(node)
+ {
+ // an "empty block" won't end a line; this addresses an issue in IE with
+ // typing into a blank line at the end of the document. typed text
+ // goes into the body, and the empty line div still looks clean.
+ // it is incorporated as dirty by the rule that a dirty region has
+ // to end a line.
+ if ((!node) || (isBlockElement(node) && !_isEmpty(node)))
+ {
+ _ensureColumnZero(null);
+ }
+ };
+ // each returns [line, char] or [-1,-1]
+ var getSelectionStart = function()
+ {
+ return selStart;
+ };
+ var getSelectionEnd = function()
+ {
+ return selEnd;
+ };
+
+ // returns array of strings for lines found, last entry will be "" if
+ // last line is complete (i.e. if a following span should be on a new line).
+ // can be called at any point
+ cc.getLines = function()
+ {
+ return lines.textLines();
+ };
+
+ cc.finish = function()
+ {
+ lines.flush();
+ var lineAttribs = lines.attribLines();
+ var lineStrings = cc.getLines();
+
+ lineStrings.length--;
+ lineAttribs.length--;
+
+ var ss = getSelectionStart();
+ var se = getSelectionEnd();
+
+ function fixLongLines()
+ {
+ // design mode does not deal with with really long lines!
+ var lineLimit = 2000; // chars
+ var buffer = 10; // chars allowed over before wrapping
+ var linesWrapped = 0;
+ var numLinesAfter = 0;
+ for (var i = lineStrings.length - 1; i >= 0; i--)
+ {
+ var oldString = lineStrings[i];
+ var oldAttribString = lineAttribs[i];
+ if (oldString.length > lineLimit + buffer)
+ {
+ var newStrings = [];
+ var newAttribStrings = [];
+ while (oldString.length > lineLimit)
+ {
+ //var semiloc = oldString.lastIndexOf(';', lineLimit-1);
+ //var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit);
+ lengthToTake = lineLimit;
+ newStrings.push(oldString.substring(0, lengthToTake));
+ oldString = oldString.substring(lengthToTake);
+ newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake));
+ oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake);
+ }
+ if (oldString.length > 0)
+ {
+ newStrings.push(oldString);
+ newAttribStrings.push(oldAttribString);
+ }
+
+ function fixLineNumber(lineChar)
+ {
+ if (lineChar[0] < 0) return;
+ var n = lineChar[0];
+ var c = lineChar[1];
+ if (n > i)
+ {
+ n += (newStrings.length - 1);
+ }
+ else if (n == i)
+ {
+ var a = 0;
+ while (c > newStrings[a].length)
+ {
+ c -= newStrings[a].length;
+ a++;
+ }
+ n += a;
+ }
+ lineChar[0] = n;
+ lineChar[1] = c;
+ }
+ fixLineNumber(ss);
+ fixLineNumber(se);
+ linesWrapped++;
+ numLinesAfter += newStrings.length;
+
+ newStrings.unshift(i, 1);
+ lineStrings.splice.apply(lineStrings, newStrings);
+ newAttribStrings.unshift(i, 1);
+ lineAttribs.splice.apply(lineAttribs, newAttribStrings);
+ }
+ }
+ return {
+ linesWrapped: linesWrapped,
+ numLinesAfter: numLinesAfter
+ };
+ }
+ var wrapData = fixLongLines();
+
+ return {
+ selStart: ss,
+ selEnd: se,
+ linesWrapped: wrapData.linesWrapped,
+ numLinesAfter: wrapData.numLinesAfter,
+ lines: lineStrings,
+ lineAttribs: lineAttribs
+ };
+ }
+
+ return cc;
+}
+
+exports.makeContentCollector = makeContentCollector;
diff --git a/package.json b/package.json
index b90f789a7..78648f0a0 100644
--- a/package.json
+++ b/package.json
@@ -3,23 +3,24 @@
"description" : "A Etherpad based on node.js",
"homepage" : "https://github.com/Pita/etherpad-lite",
"keywords" : ["etherpad", "realtime", "collaborative", "editor"],
- "author" : "Peter 'Pita' Martischka ",
+ "author" : "Peter 'Pita' Martischka - Primary Technology Ltd",
"contributors": [
{ "name": "John McLear",
"name": "Hans Pinckaers",
"name": "Robin Buse"}
],
"dependencies" : {
- "socket.io" : "0.7.9",
- "ueberDB" : "0.1.2",
- "async" : "0.1.9",
- "joose" : "3.18.0",
- "express" : "2.4.5",
+ "socket.io" : "0.8.7",
+ "ueberDB" : "0.1.3",
+ "async" : "0.1.15",
+ "joose" : "3.50.0",
+ "express" : "2.5.0",
"clean-css" : "0.2.4",
- "uglify-js" : "1.0.7",
+ "uglify-js" : "1.1.1",
"gzip" : "0.1.0",
- "formidable" : "1.0.2",
- "log4js" : "0.3.8"
+ "formidable" : "1.0.7",
+ "log4js" : "0.3.9",
+ "jsdom" : "0.2.9"
},
"version" : "1.0.0"
}
diff --git a/settings.json.template b/settings.json.template
index a7afaecc9..da7b32a49 100644
--- a/settings.json.template
+++ b/settings.json.template
@@ -8,7 +8,8 @@
"ip": "0.0.0.0",
"port" : 9001,
- //The Type of the database. You can choose between sqlite and mysql
+ //The Type of the database. You can choose between dirty, sqlite and mysql
+ //You should use mysql or sqlite for anything else than testing or development
"dbType" : "dirty",
//the database specific settings
"dbSettings" : {
@@ -28,6 +29,12 @@
//the default text of a pad
"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",
+ /* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
+ "requireSession" : false,
+
+ /* Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. */
+ "editOnly" : false,
+
/* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly,
but makes it impossible to debug the javascript/css */
"minify" : true,
diff --git a/settings.json.template_windows b/settings.json.template_windows
index 235ec71a3..61f14dcea 100644
--- a/settings.json.template_windows
+++ b/settings.json.template_windows
@@ -28,6 +28,12 @@
//the default text of a pad
"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",
+ /* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
+ "requireSession" : false,
+
+ /* Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. */
+ "editOnly" : false,
+
/* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly,
but makes it impossible to debug the javascript/css */
"minify" : false,
diff --git a/static/css/iframe_editor.css b/static/css/iframe_editor.css
index 7be570112..86ca99117 100644
--- a/static/css/iframe_editor.css
+++ b/static/css/iframe_editor.css
@@ -32,6 +32,23 @@ ul.list-bullet6 { list-style-type: square; }
ul.list-bullet7 { list-style-type: disc; }
ul.list-bullet8 { list-style-type: circle; }
+ul.list-indent1 { margin-left: 1.5em; }
+ul.list-indent2 { margin-left: 3em; }
+ul.list-indent3 { margin-left: 4.5em; }
+ul.list-indent4 { margin-left: 6em; }
+ul.list-indent5 { margin-left: 7.5em; }
+ul.list-indent6 { margin-left: 9em; }
+ul.list-indent7 { margin-left: 10.5em; }
+ul.list-indent8 { margin-left: 12em; }
+
+ul.list-indent1 { list-style-type: none; }
+ul.list-indent2 { list-style-type: none; }
+ul.list-indent3 { list-style-type: none; }
+ul.list-indent4 { list-style-type: none; }
+ul.list-indent5 { list-style-type: none; }
+ul.list-indent6 { list-style-type: none; }
+ul.list-indent7 { list-style-type: none; }
+ul.list-indent8 { list-style-type: none; }
body {
margin: 0;
diff --git a/static/css/pad.css b/static/css/pad.css
index 45385a5b7..33fc49a91 100644
--- a/static/css/pad.css
+++ b/static/css/pad.css
@@ -93,7 +93,7 @@ a img
}
input[type="file"] {
- color: #fff;
+ color: #000;
}
#editbar ul li.separator
@@ -469,7 +469,7 @@ table#otheruserstable { display: none; }
.modaldialog.cboxreconnecting .modaldialog-inner,
.modaldialog.cboxconnecting .modaldialog-inner {
- background: url(static/img/connectingbar.gif) no-repeat center 60px;
+ background: url(../../static/img/connectingbar.gif) no-repeat center 60px;
height: 100px;
}
.modaldialog.cboxreconnecting {
@@ -786,11 +786,15 @@ padding: 10px;
border-radius: 6px;
}
-#embedcode, #readonlyUrl {
+#embedreadonly {
+float:right;
+}
+
+#embedcode, #readonlyUrl, #linkcode {
margin-left:10px;
}
-#embedinput, #readonlyInput{
+#embedinput, #readonlyInput, #linkinput {
width:375px;
height:24px;
display:inline;
@@ -1142,8 +1146,21 @@ width:33px !important;
color: #999;
}
+label[for=readonlyinput] {
+ margin: 0 10px 0 2px;
+}
+
@media screen and (max-width: 600px) {
#editbar ul li {
padding: 4px 1px;
}
-}
\ No newline at end of file
+}
+
+#qr_center {
+ margin: 10px 10px auto 0;
+ text-align: center;
+}
+
+#qrcode{
+ margin-left:10px;
+}
diff --git a/static/img/connectingbar.gif b/static/img/connectingbar.gif
new file mode 100644
index 000000000..34f54e90c
Binary files /dev/null and b/static/img/connectingbar.gif differ
diff --git a/static/index.html b/static/index.html
index e995cdb2f..2bed58f98 100644
--- a/static/index.html
+++ b/static/index.html
@@ -86,12 +86,13 @@
input[type="submit"]::-moz-focus-inner { border: 0 }
@-moz-document url-prefix() { input[type="submit"] { padding: 7px } }
+
New Pad
or create/open a Pad with the name
-
@@ -123,4 +124,5 @@
//start the costum js
if(typeof costumStart == "function") costumStart();
+