Merge remote branch 'upstream/master'

This commit is contained in:
John McLear 2011-12-04 14:46:00 +00:00
commit 66ea90208a
40 changed files with 1522 additions and 404 deletions

4
.gitignore vendored
View file

@ -6,4 +6,6 @@ bin/abiword.exe
bin/node.exe bin/node.exe
etherpad-lite-win.zip etherpad-lite-win.zip
var/dirty.db var/dirty.db
bin/convertSettings.json bin/convertSettings.json
*~
*.patch

View file

@ -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) 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. 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 There are several clients in for this API:
(If you don't want to use PHP, feel free to create a client for your favourite web development language).
* [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 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**<br> **Online demo**<br>
@ -51,15 +57,16 @@ Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
**As root:** **As root:**
<ol> <ol>
<li>Install the dependencies. We need the gzip, git, curl, libssl develop libraries and python <br><code>apt-get install gzip git-core curl python libssl-dev</code></li><br> <li>Install the dependencies. We need gzip, git, curl, libssl develop libraries, python and gcc. <br><i>For Debian/Ubuntu</i> <code>apt-get install gzip git-core curl python libssl-dev build-essential</code><br>
<i>For Fedora/CentOS</i> <code>yum install gzip git-core curl python openssl-dev && yum groupinstall "Development Tools"</code>
</li><br>
<li>Install node.js <li>Install node.js
<ol type="a"> <ol type="a">
<li>Download the latest <b>0.4.x</b> node.js release from <a href="http://nodejs.org/#download">http://nodejs.org/#download</a></li> <li>Download the latest <b>0.6.x</b> node.js release from <a href="http://nodejs.org/#download">http://nodejs.org/#download</a></li>
<li>Extract it with <code>tar xf node-v0.4*</code></li> <li>Extract it with <code>tar xf node-v0.6*</code></li>
<li>Move into the node folder <code>cd node-v0.4*</code> and build node with <code>./configure && make && make install</code></li> <li>Move into the node folder <code>cd node-v0.6*</code> and build node with <code>./configure && make && make install</code></li>
</ol> </ol>
</li> </li>
<li>Install npm <code>curl http://npmjs.org/install.sh | sh</code></li>
</ol> </ol>
**As any user (we recommend creating a separate user called etherpad-lite):** **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 ## Next Steps
You can modify the settings in the file `settings.json` 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 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: Look at this wiki pages:

View file

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
NODE_VERSION="0.5.4" NODE_VERSION="0.6.1"
#Move to the folder where ep-lite is installed #Move to the folder where ep-lite is installed
cd `dirname $0` cd `dirname $0`
@ -48,16 +48,6 @@ cp -rL node_modules node_modules_resolved
rm -rf node_modules rm -rf node_modules
mv node_modules_resolved 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..." echo "download windows node..."
cd bin cd bin
wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O node.exe wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O node.exe

View file

@ -52,7 +52,7 @@ async.series([
{ {
log("get all padIds out of the database..."); 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; padIDs = _padIDs;
callback(err); callback(err);
@ -153,11 +153,14 @@ function convertPad(padId, callback)
{ {
if(!err) if(!err)
{ {
//parse the pages try
for(var i=0,length=results.length;i<length;i++)
{ {
parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); //parse the pages
} for(var i=0,length=results.length;i<length;i++)
{
parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
}catch(e) {err = e}
} }
callback(err); callback(err);
@ -172,11 +175,14 @@ function convertPad(padId, callback)
{ {
if(!err) if(!err)
{ {
//parse the pages try
for(var i=0,length=results.length;i<length;i++)
{ {
parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false); //parse the pages
} for(var i=0,length=results.length;i<length;i++)
{
parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false);
}
}catch(e) {err = e}
} }
callback(err); callback(err);
@ -191,11 +197,14 @@ function convertPad(padId, callback)
{ {
if(!err) if(!err)
{ {
//parse the pages try
for(var i=0,length=results.length;i<length;i++)
{ {
parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); //parse the pages
} for(var i=0,length=results.length;i<length;i++)
{
parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
}catch(e) {err = e}
} }
callback(err); callback(err);
@ -210,7 +219,10 @@ function convertPad(padId, callback)
{ {
if(!err) if(!err)
{ {
apool=JSON.parse(results[0].JSON).x; try
{
apool=JSON.parse(results[0].JSON).x;
}catch(e) {err = e}
} }
callback(err); callback(err);
@ -225,11 +237,14 @@ function convertPad(padId, callback)
{ {
if(!err) if(!err)
{ {
//parse the pages try
for(var i=0, length=results.length;i<length;i++)
{ {
parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); //parse the pages
} for(var i=0, length=results.length;i<length;i++)
{
parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
}catch(e) {err = e}
} }
callback(err); callback(err);
@ -244,7 +259,10 @@ function convertPad(padId, callback)
{ {
if(!err) if(!err)
{ {
padmeta = JSON.parse(results[0].JSON).x; try
{
padmeta = JSON.parse(results[0].JSON).x;
}catch(e) {err = e}
} }
callback(err); callback(err);

View file

@ -1,5 +1,5 @@
{ {
"etherpad-db": "etherpadDB":
{ {
"host": "localhost", "host": "localhost",
"port": 3306, "port": 3306,

89
bin/extractPadData.js Normal file
View file

@ -0,0 +1,89 @@
/*
This is a debug tool. It helps to extract all datas of a pad and move it from an productive enviroment and to a develop enviroment to reproduce bugs there. It outputs a dirtydb file
*/
if(process.argv.length != 3)
{
console.error("Use: node extractPadData.js $PADID");
process.exit(1);
}
//get the padID
var padId = process.argv[2];
//initalize the database
var log4js = require("log4js");
log4js.setGlobalLogLevel("INFO");
var async = require("async");
var db = require('../node/db/DB');
var dirty = require("dirty")(padId + ".db");
var padManager;
var pad;
var neededDBValues = ["pad:"+padId];
async.series([
//intallize the database
function (callback)
{
db.init(callback);
},
//get the pad
function (callback)
{
padManager = require('../node/db/PadManager');
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
},
function (callback)
{
//add all authors
var authors = pad.getAllAuthors();
for(var i=0;i<authors.length;i++)
{
neededDBValues.push("globalAuthor:" + authors[i]);
}
//add all revisions
var revHead = pad.head;
for(var i=0;i<=revHead;i++)
{
neededDBValues.push("pad:"+padId+":revs:" + i);
}
//get all chat values
var chatHead = pad.chatHead;
for(var i=0;i<=chatHead;i++)
{
neededDBValues.push("pad:"+padId+":chat:" + i);
}
//get and set all values
async.forEach(neededDBValues, function(dbkey, callback)
{
db.db.db.wrappedDB.get(dbkey, function(err, dbvalue)
{
if(err) { callback(err); return}
dbvalue=JSON.parse(dbvalue);
dirty.set(dbkey, dbvalue, callback);
});
}, callback);
}
], function (err)
{
if(err) throw err;
else
{
console.log("finished");
process.exit();
}
});
//get the pad object
//get all revisions of this pad
//get all authors related to this pad
//get the readonly link releated to this pad
//get the chat entrys releated to this pad

View file

@ -20,13 +20,6 @@ hash node > /dev/null 2>&1 || {
exit 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? #Is npm installed?
hash npm > /dev/null 2>&1 || { hash npm > /dev/null 2>&1 || {
echo "Please install npm ( http://npmjs.org )" >&2 echo "Please install npm ( http://npmjs.org )" >&2
@ -54,9 +47,9 @@ npm install || {
echo "Ensure jQuery is downloaded and up to date..." echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true" DOWNLOAD_JQUERY="true"
NEEDED_VERSION="1.6.2" NEEDED_VERSION="1.7"
if [ -f "static/js/jquery.min.js" ]; then 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 if [ ${VERSION#v} = $NEEDED_VERSION ]; then
DOWNLOAD_JQUERY="false" DOWNLOAD_JQUERY="false"

View file

@ -25,6 +25,9 @@ var groupManager = require("./GroupManager");
var authorManager = require("./AuthorManager"); var authorManager = require("./AuthorManager");
var sessionManager = require("./SessionManager"); var sessionManager = require("./SessionManager");
var async = require("async"); var async = require("async");
var exportHtml = require("../utils/ExportHtml");
var importHtml = require("../utils/ImportHtml");
var cleanText = require("./Pad").cleanText;
/**********************/ /**********************/
/**GROUP FUNCTIONS*****/ /**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 <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({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 */ /**PAD FUNCTIONS */
/*****************/ /*****************/

View file

@ -21,10 +21,20 @@
require("../db/Pad"); require("../db/Pad");
var db = require("./DB").db; 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 * 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 //return pad if its already loaded
if(pad != null) if(pad != null)
@ -86,7 +96,7 @@ exports.getPad = function(id, text, callback)
} }
else else
{ {
globalPads[id] = pad; globalPads.set(id, pad);
callback(null, pad); callback(null, pad);
} }
}); });
@ -110,6 +120,6 @@ exports.isValidPadId = function(padId)
//removes a pad from the array //removes a pad from the array
exports.unloadPad = function(padId) exports.unloadPad = function(padId)
{ {
if(globalPads[padId]) if(globalPads.get(padId))
delete globalPads[padId]; globalPads.remove(padId);
} }

View file

@ -23,6 +23,7 @@ var async = require("async");
var authorManager = require("./AuthorManager"); var authorManager = require("./AuthorManager");
var padManager = require("./PadManager"); var padManager = require("./PadManager");
var sessionManager = require("./SessionManager"); 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. * 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) exports.checkAccess = function (padID, sessionID, token, password, callback)
{ {
// it's not a group pad, means we can grant access var statusObject;
if(padID.indexOf("$") == -1)
// a valid session is required (api-only mode)
if(settings.requireSession)
{ {
//get author for this token // no sessionID, access is denied
authorManager.getAuthor4Token(token, function(err, author) if(!sessionID)
{ {
// grant access, with author of token callback(null, {accessStatus: "deny"});
callback(err, {accessStatus: "grant", authorID: author}); return;
}) }
}
//don't continue // a session is not required, so we'll check if it's a public pad
return; 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]; var groupID = padID.split("$")[0];
@ -57,8 +92,6 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
var isPasswordProtected; var isPasswordProtected;
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
var statusObject;
async.series([ async.series([
//get basic informations from the database //get basic informations from the database
function(callback) function(callback)
@ -180,6 +213,8 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
{ {
//--> grant access //--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor}; 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 // there is no valid session avaiable AND pad exists
else if(!validSession && padExists) else if(!validSession && padExists)

View file

@ -50,6 +50,8 @@ var functions = {
"listSessionsOfAuthor" : ["authorID"], "listSessionsOfAuthor" : ["authorID"],
"getText" : ["padID", "rev"], "getText" : ["padID", "rev"],
"setText" : ["padID", "text"], "setText" : ["padID", "text"],
"getHTML" : ["padID", "rev"],
"setHTML" : ["padID", "html"],
"getRevisionsCount" : ["padID"], "getRevisionsCount" : ["padID"],
"deletePad" : ["padID"], "deletePad" : ["padID"],
"getReadOnlyID" : ["padID"], "getReadOnlyID" : ["padID"],
@ -69,7 +71,7 @@ var functions = {
exports.handle = function(functionName, fields, req, res) exports.handle = function(functionName, fields, req, res)
{ {
//check the api key! //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}); res.send({code: 4, message: "no or wrong API Key", data: null});
return; return;

View file

@ -81,10 +81,9 @@ exports.doExport = function(req, res, padId, type)
res.send(html); res.send(html);
callback("stop"); callback("stop");
} }
//write the html export to a file else //write the html export to a file
else
{ {
randNum = Math.floor(Math.random()*new Date().getTime()); randNum = Math.floor(Math.random()*0xFFFFFFFF);
srcFile = tempDirectory + "/eplite_export_" + randNum + ".html"; srcFile = tempDirectory + "/eplite_export_" + randNum + ".html";
fs.writeFile(srcFile, html, callback); fs.writeFile(srcFile, html, callback);
} }
@ -114,10 +113,13 @@ exports.doExport = function(req, res, padId, type)
function(callback) function(callback)
{ {
//100ms delay to accomidate for slow windows fs //100ms delay to accomidate for slow windows fs
setTimeout(function() if(os.type().indexOf("Windows") > -1)
{ {
fs.unlink(destFile, callback); setTimeout(function()
}, 100); {
fs.unlink(destFile, callback);
}, 100);
}
} }
], callback); ], callback);
} }

View file

@ -61,10 +61,19 @@ exports.doImport = function(req, res, padId)
form.parse(req, function(err, fields, files) form.parse(req, function(err, fields, files)
{ {
//save the path of the uploaded file //the upload failed, stop at this point
srcFile = files.file.path; if(err || files.file === undefined)
{
callback(err); 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 //this allows us to accept source code files like .c or .java
function(callback) function(callback)
{ {
var fileEnding = srcFile.split(".")[1]; var fileEnding = srcFile.split(".")[1].toLowerCase();
var knownFileEndings = ["txt", "doc", "docx", "pdf", "odt", "html", "htm"]; var knownFileEndings = ["txt", "doc", "docx", "pdf", "odt", "html", "htm"];
//find out if this is a known file ending //find out if this is a known file ending
@ -103,7 +112,7 @@ exports.doImport = function(req, res, padId)
//convert file to text //convert file to text
function(callback) function(callback)
{ {
var randNum = Math.floor(Math.random()*new Date().getTime()); var randNum = Math.floor(Math.random()*0xFFFFFFFF);
destFile = tempDirectory + "eplite_import_" + randNum + ".txt"; destFile = tempDirectory + "eplite_import_" + randNum + ".txt";
abiword.convertFile(srcFile, destFile, "txt", callback); 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. //node on windows has a delay on releasing of the file lock.
//We add a 100ms delay to work around this //We add a 100ms delay to work around this
setTimeout(function() if(os.type().indexOf("Windows") > -1)
{ {
callback(err); setTimeout(function()
}, 100); {
callback(err);
}, 100);
}
}); });
}, },
@ -157,6 +169,13 @@ exports.doImport = function(req, res, padId)
} }
], function(err) ], 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; if(err) throw err;
//close the connection //close the connection

View file

@ -136,7 +136,7 @@ exports.handleDisconnect = function(client)
{ {
if(pad2sessions[sessionPad][i] == client.id) if(pad2sessions[sessionPad][i] == client.id)
{ {
delete pad2sessions[sessionPad][i]; pad2sessions[sessionPad].splice(i, 1);
break; break;
} }
} }
@ -190,10 +190,10 @@ exports.handleMessage = function(client, message)
{ {
handleSuggestUserName(client, message); handleSuggestUserName(client, message);
} }
//if the message type is unkown, throw an exception //if the message type is unknown, throw an exception
else 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 //check if all ok
if(message.data.payload.newName == null) 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; return;
} }
if(message.data.payload.unnamedId == null) 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; return;
} }
@ -304,7 +304,7 @@ function handleUserInfoUpdate(client, message)
//check if all ok //check if all ok
if(message.data.userInfo.colorId == null) 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; return;
} }
@ -348,17 +348,17 @@ function handleUserChanges(client, message)
//check if all ok //check if all ok
if(message.data.baseRev == null) 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; return;
} }
if(message.data.apool == null) 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; return;
} }
if(message.data.changeset == null) 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; return;
} }
@ -600,22 +600,22 @@ function handleClientReady(client, message)
//check if all ok //check if all ok
if(!message.token) if(!message.token)
{ {
messageLogger.warn("Droped message, CLIENT_READY Message has no token!"); messageLogger.warn("Dropped message, CLIENT_READY Message has no token!");
return; return;
} }
if(!message.padId) if(!message.padId)
{ {
messageLogger.warn("Droped message, CLIENT_READY Message has no padId!"); messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!");
return; return;
} }
if(!message.protocolVersion) if(!message.protocolVersion)
{ {
messageLogger.warn("Droped message, CLIENT_READY Message has no protocolVersion!"); messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!");
return; return;
} }
if(message.protocolVersion != 2) 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; return;
} }
@ -806,8 +806,20 @@ function handleClientReady(client, message)
clientVars.userName = authorName; clientVars.userName = authorName;
} }
//Send the clientVars to the Client //This is a reconnect, so we don't have to send the client the ClientVars again
client.json.send(clientVars); 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 //Save the revision and the author id in sessioninfos
sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); sessioninfos[client.id].rev = pad.getHeadRevisionNumber();

View file

@ -128,7 +128,7 @@ exports.setSocketIO = function(_socket)
//drop message //drop message
else else
{ {
messageLogger.warn("Droped message cause of bad permissions:" + stringifyWithoutPassword(message)); messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message));
} }
} }
}); });

View file

@ -77,7 +77,7 @@ exports.handleMessage = function(client, message)
//if the message type is unkown, throw an exception //if the message type is unkown, throw an exception
else 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) if(message.padId == null)
{ {
messageLogger.warn("Droped message, changeset request has no padId!"); messageLogger.warn("Dropped message, changeset request has no padId!");
return; return;
} }
@ -106,27 +106,27 @@ function handleChangesetRequest(client, message)
//check if all ok //check if all ok
if(message.data == null) if(message.data == null)
{ {
messageLogger.warn("Droped message, changeset request has no data!"); messageLogger.warn("Dropped message, changeset request has no data!");
return; return;
} }
if(message.padId == null) if(message.padId == null)
{ {
messageLogger.warn("Droped message, changeset request has no padId!"); messageLogger.warn("Dropped message, changeset request has no padId!");
return; return;
} }
if(message.data.granularity == null) if(message.data.granularity == null)
{ {
messageLogger.warn("Droped message, changeset request has no granularity!"); messageLogger.warn("Dropped message, changeset request has no granularity!");
return; return;
} }
if(message.data.start == null) if(message.data.start == null)
{ {
messageLogger.warn("Droped message, changeset request has no start!"); messageLogger.warn("Dropped message, changeset request has no start!");
return; return;
} }
if(message.data.requestID == null) if(message.data.requestID == null)
{ {
messageLogger.warn("Droped message, changeset request has no requestID!"); messageLogger.warn("Dropped message, changeset request has no requestID!");
return; return;
} }

View file

@ -91,7 +91,10 @@ async.waterfall([
var httpLogger = log4js.getLogger("http"); var httpLogger = log4js.getLogger("http");
app.configure(function() 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()); app.use(express.cookieParser());
}); });
@ -287,14 +290,13 @@ async.waterfall([
}); });
var apiLogger = log4js.getLogger("API"); var apiLogger = log4js.getLogger("API");
//This is a api call, collect all post informations and pass it to the apiHandler //This is for making an api call, collecting all post information and passing it to the apiHandler
app.get('/api/1/:func', function(req, res) var apiCaller = function(req, res, fields) {
{
res.header("Server", serverName); res.header("Server", serverName);
res.header("Content-Type", "application/json; charset=utf-8"); 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 //wrap the send function so we can log the response
res._send = res.send; res._send = res.send;
@ -311,7 +313,22 @@ async.waterfall([
} }
//call the api handler //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 //The Etherpad client side sends information about how a disconnect happen
@ -425,19 +442,19 @@ async.waterfall([
io.set('logger', { io.set('logger', {
debug: function (str) debug: function (str)
{ {
socketIOLogger.debug(str); socketIOLogger.debug.apply(socketIOLogger, arguments);
}, },
info: function (str) info: function (str)
{ {
socketIOLogger.info(str); socketIOLogger.info.apply(socketIOLogger, arguments);
}, },
warn: function (str) warn: function (str)
{ {
socketIOLogger.warn(str); socketIOLogger.warn.apply(socketIOLogger, arguments);
}, },
error: function (str) error: function (str)
{ {
socketIOLogger.error(str); socketIOLogger.error.apply(socketIOLogger, arguments);
}, },
}); });

View file

@ -85,6 +85,8 @@ function getPadHTML(pad, revNum, callback)
}); });
} }
exports.getPadHTML = getPadHTML;
function getHTMLFromAtext(pad, atext) function getHTMLFromAtext(pad, atext)
{ {
var apool = pad.apool(); var apool = pad.apool();
@ -119,8 +121,10 @@ function getHTMLFromAtext(pad, atext)
var taker = Changeset.stringIterator(text); var taker = Changeset.stringIterator(text);
var assem = Changeset.stringAssembler(); var assem = Changeset.stringAssembler();
var openTags = [];
function emitOpenTag(i) function emitOpenTag(i)
{ {
openTags.unshift(i);
assem.append('<'); assem.append('<');
assem.append(tags[i]); assem.append(tags[i]);
assem.append('>'); assem.append('>');
@ -128,10 +132,27 @@ function getHTMLFromAtext(pad, atext)
function emitCloseTag(i) function emitCloseTag(i)
{ {
openTags.shift();
assem.append('</'); assem.append('</');
assem.append(tags[i]); assem.append(tags[i]);
assem.append('>'); 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 urls = _findURLs(text);
@ -204,18 +225,25 @@ function getHTMLFromAtext(pad, atext)
} }
} }
var tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--) for (var i = propVals.length - 1; i >= 0; i--)
{ {
if (propVals[i] === LEAVE) if (propVals[i] === LEAVE)
{ {
emitCloseTag(i); //emitCloseTag(i);
tags2close.push(i);
propVals[i] = false; propVals[i] = false;
} }
else if (propVals[i] === STAY) else if (propVals[i] === STAY)
{ {
emitCloseTag(i); //emitCloseTag(i);
tags2close.push(i);
} }
} }
orderdCloseTags(tags2close);
for (var i = 0; i < propVals.length; i++) for (var i = 0; i < propVals.length; i++)
{ {
if (propVals[i] === ENTER || propVals[i] === STAY) if (propVals[i] === ENTER || propVals[i] === STAY)
@ -231,18 +259,27 @@ function getHTMLFromAtext(pad, atext)
{ {
chars--; // exclude newline at end of line, if present chars--; // exclude newline at end of line, if present
} }
var s = taker.take(chars); 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)); assem.append(_escapeHTML(s));
} // end iteration over spans in line } // end iteration over spans in line
var tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--) for (var i = propVals.length - 1; i >= 0; i--)
{ {
if (propVals[i]) if (propVals[i])
{ {
emitCloseTag(i); tags2close.push(i);
propVals[i] = false; propVals[i] = false;
} }
} }
orderdCloseTags(tags2close);
} // end processNextChars } // end processNextChars
if (urls) if (urls)
{ {

92
node/utils/ImportHtml.js Normal file
View file

@ -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+</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;

View file

@ -42,6 +42,17 @@ exports.dbSettings = { "filename" : "../var/dirty.db" };
* The default Text of a new pad * 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"; 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 * A flag that shows if minification is enabled or not
*/ */

View file

@ -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) == "&nbsp;" && !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;

View file

@ -3,23 +3,24 @@
"description" : "A Etherpad based on node.js", "description" : "A Etherpad based on node.js",
"homepage" : "https://github.com/Pita/etherpad-lite", "homepage" : "https://github.com/Pita/etherpad-lite",
"keywords" : ["etherpad", "realtime", "collaborative", "editor"], "keywords" : ["etherpad", "realtime", "collaborative", "editor"],
"author" : "Peter 'Pita' Martischka <petermartischka@googlemail.com>", "author" : "Peter 'Pita' Martischka <petermartischka@googlemail.com> - Primary Technology Ltd",
"contributors": [ "contributors": [
{ "name": "John McLear", { "name": "John McLear",
"name": "Hans Pinckaers", "name": "Hans Pinckaers",
"name": "Robin Buse"} "name": "Robin Buse"}
], ],
"dependencies" : { "dependencies" : {
"socket.io" : "0.7.9", "socket.io" : "0.8.7",
"ueberDB" : "0.1.2", "ueberDB" : "0.1.3",
"async" : "0.1.9", "async" : "0.1.15",
"joose" : "3.18.0", "joose" : "3.50.0",
"express" : "2.4.5", "express" : "2.5.0",
"clean-css" : "0.2.4", "clean-css" : "0.2.4",
"uglify-js" : "1.0.7", "uglify-js" : "1.1.1",
"gzip" : "0.1.0", "gzip" : "0.1.0",
"formidable" : "1.0.2", "formidable" : "1.0.7",
"log4js" : "0.3.8" "log4js" : "0.3.9",
"jsdom" : "0.2.9"
}, },
"version" : "1.0.0" "version" : "1.0.0"
} }

View file

@ -8,7 +8,8 @@
"ip": "0.0.0.0", "ip": "0.0.0.0",
"port" : 9001, "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", "dbType" : "dirty",
//the database specific settings //the database specific settings
"dbSettings" : { "dbSettings" : {
@ -28,6 +29,12 @@
//the default text of a pad //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", "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, /* 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 */ but makes it impossible to debug the javascript/css */
"minify" : true, "minify" : true,

View file

@ -28,6 +28,12 @@
//the default text of a pad //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", "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, /* 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 */ but makes it impossible to debug the javascript/css */
"minify" : false, "minify" : false,

View file

@ -32,6 +32,23 @@ ul.list-bullet6 { list-style-type: square; }
ul.list-bullet7 { list-style-type: disc; } ul.list-bullet7 { list-style-type: disc; }
ul.list-bullet8 { list-style-type: circle; } 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 { body {
margin: 0; margin: 0;

View file

@ -93,7 +93,7 @@ a img
} }
input[type="file"] { input[type="file"] {
color: #fff; color: #000;
} }
#editbar ul li.separator #editbar ul li.separator
@ -469,7 +469,7 @@ table#otheruserstable { display: none; }
.modaldialog.cboxreconnecting .modaldialog-inner, .modaldialog.cboxreconnecting .modaldialog-inner,
.modaldialog.cboxconnecting .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; height: 100px;
} }
.modaldialog.cboxreconnecting { .modaldialog.cboxreconnecting {
@ -786,11 +786,15 @@ padding: 10px;
border-radius: 6px; border-radius: 6px;
} }
#embedcode, #readonlyUrl { #embedreadonly {
float:right;
}
#embedcode, #readonlyUrl, #linkcode {
margin-left:10px; margin-left:10px;
} }
#embedinput, #readonlyInput{ #embedinput, #readonlyInput, #linkinput {
width:375px; width:375px;
height:24px; height:24px;
display:inline; display:inline;
@ -1142,8 +1146,21 @@ width:33px !important;
color: #999; color: #999;
} }
label[for=readonlyinput] {
margin: 0 10px 0 2px;
}
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
#editbar ul li { #editbar ul li {
padding: 4px 1px; padding: 4px 1px;
} }
} }
#qr_center {
margin: 10px 10px auto 0;
text-align: center;
}
#qrcode{
margin-left:10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -86,12 +86,13 @@
input[type="submit"]::-moz-focus-inner { border: 0 } input[type="submit"]::-moz-focus-inner { border: 0 }
@-moz-document url-prefix() { input[type="submit"] { padding: 7px } } @-moz-document url-prefix() { input[type="submit"] { padding: 7px } }
</style> </style>
<body onload="document.mainform.padname.focus();">
<link href="static/custom/index.css" rel="stylesheet"> <link href="static/custom/index.css" rel="stylesheet">
<script src="static/custom/index.js"></script> <script src="static/custom/index.js"></script>
<div id="container"> <div id="container">
<div id="button" onclick="go2Random()">New Pad</div><br><div id="label">or create/open a Pad with the name</div> <div id="button" onclick="go2Random()">New Pad</div><br><div id="label">or create/open a Pad with the name</div>
<form action="#" onsubmit="go2Name();return false;"> <form action="#" name="mainform" onsubmit="go2Name();return false;">
<input type="text" id="padname" autofocus> <input type="text" name="padname" id="padname" autofocus>
<input type="submit" value="OK"> <input type="submit" value="OK">
</form> </form>
</div> </div>
@ -123,4 +124,5 @@
//start the costum js //start the costum js
if(typeof costumStart == "function") costumStart(); if(typeof costumStart == "function") costumStart();
</script> </script>
</body>
</html> </html>

View file

@ -74,8 +74,15 @@ function isArray(testObject)
return testObject && typeof testObject === 'object' && !(testObject.propertyIsEnumerable('length')) && typeof testObject.length === 'number'; return testObject && typeof testObject === 'object' && !(testObject.propertyIsEnumerable('length')) && typeof testObject.length === 'number';
} }
if (typeof exports !== "undefined")
{
userAgent = "node-js";
}
else
{
userAgent = navigator.userAgent.toLowerCase();
}
// Figure out what browser is being used (stolen from jquery 1.2.1) // Figure out what browser is being used (stolen from jquery 1.2.1)
var userAgent = navigator.userAgent.toLowerCase();
var browser = { var browser = {
version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1], version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1],
safari: /webkit/.test(userAgent), safari: /webkit/.test(userAgent),
@ -85,6 +92,7 @@ var browser = {
windows: /windows/.test(userAgent) // dgreensp windows: /windows/.test(userAgent) // dgreensp
}; };
function getAssoc(obj, name) function getAssoc(obj, name)
{ {
return obj["_magicdom_" + name]; return obj["_magicdom_" + name];
@ -130,3 +138,8 @@ function htmlPrettyEscape(str)
{ {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\r?\n/g, '\\n'); return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\r?\n/g, '\\n');
} }
if (typeof exports !== "undefined")
{
exports.map = map;
}

View file

@ -3534,7 +3534,8 @@ function OUTER(gscope)
function doIndentOutdent(isOut) function doIndentOutdent(isOut)
{ {
if (!(rep.selStart && rep.selEnd)) if (!(rep.selStart && rep.selEnd) ||
((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1))
{ {
return false; return false;
} }
@ -3544,25 +3545,25 @@ function OUTER(gscope)
lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] == 0) ? 1 : 0)); lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] == 0) ? 1 : 0));
var mods = []; var mods = [];
var foundLists = false;
for (var n = firstLine; n <= lastLine; n++) for (var n = firstLine; n <= lastLine; n++)
{ {
var listType = getLineListType(n); var listType = getLineListType(n);
var t = 'indent';
var level = 0;
if (listType) if (listType)
{ {
listType = /([a-z]+)([12345678])/.exec(listType); listType = /([a-z]+)([12345678])/.exec(listType);
if (listType) if (listType)
{ {
foundLists = true; t = listType[1];
var t = listType[1]; level = Number(listType[2]);
var level = Number(listType[2]);
var newLevel = Math.max(1, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1)));
if (level != newLevel)
{
mods.push([n, t + newLevel]);
}
} }
} }
var newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1)));
if (level != newLevel)
{
mods.push([n, (newLevel > 0) ? t + newLevel : '']);
}
} }
if (mods.length > 0) if (mods.length > 0)
@ -3570,7 +3571,7 @@ function OUTER(gscope)
setLineListTypes(mods); setLineListTypes(mods);
} }
return foundLists; return true;
} }
editorInfo.ace_doIndentOutdent = doIndentOutdent; editorInfo.ace_doIndentOutdent = doIndentOutdent;
@ -5231,7 +5232,8 @@ function OUTER(gscope)
var allLinesAreList = true; var allLinesAreList = true;
for (var n = firstLine; n <= lastLine; n++) for (var n = firstLine; n <= lastLine; n++)
{ {
if (!getLineListType(n)) var listType = getLineListType(n);
if (!listType || listType.slice(0, 'bullet'.length) != 'bullet')
{ {
allLinesAreList = false; allLinesAreList = false;
break; break;
@ -5241,8 +5243,16 @@ function OUTER(gscope)
var mods = []; var mods = [];
for (var n = firstLine; n <= lastLine; n++) for (var n = firstLine; n <= lastLine; n++)
{ {
var t = '';
var level = 0;
var listType = /([a-z]+)([12345678])/.exec(getLineListType(n));
if (listType)
{
t = listType[1];
level = Number(listType[2]);
}
var t = getLineListType(n); var t = getLineListType(n);
mods.push([n, allLinesAreList ? '' : (t ? t : 'bullet1')]); mods.push([n, allLinesAreList ? 'indent' + level : (t ? 'bullet' + level : 'bullet1')]);
} }
setLineListTypes(mods); setLineListTypes(mods);
} }

View file

@ -16,6 +16,8 @@
var chat = (function() var chat = (function()
{ {
var chatMentions = 0;
var title = document.title;
var self = { var self = {
show: function () show: function ()
{ {
@ -43,6 +45,8 @@ var chat = (function()
} }
}); });
}); });
chatMentions = 0;
document.title = title;
}, },
hide: function () hide: function ()
{ {
@ -54,8 +58,6 @@ var chat = (function()
}, },
scrollDown: function() scrollDown: function()
{ {
//console.log($('#chatbox').css("display"));
if($('#chatbox').css("display") != "none") if($('#chatbox').css("display") != "none")
$('#chattext').animate({scrollTop: $('#chattext')[0].scrollHeight}, "slow"); $('#chattext').animate({scrollTop: $('#chattext')[0].scrollHeight}, "slow");
}, },
@ -87,6 +89,17 @@ var chat = (function()
}); });
var text = padutils.escapeHtmlWithClickableLinks(padutils.escapeHtml(msg.text), "_blank"); var text = padutils.escapeHtmlWithClickableLinks(padutils.escapeHtml(msg.text), "_blank");
/* Performs an action if your name is mentioned */
var myName = $('#myusernameedit').val();
myName = myName.toLowerCase();
var chatText = text.toLowerCase();
var wasMentioned = false;
if (chatText.indexOf(myName) !== -1 && myName != "undefined"){
wasMentioned = true;
}
/* End of new action */
var authorName = msg.userName == null ? "unnamed" : padutils.escapeHtml(msg.userName); var authorName = msg.userName == null ? "unnamed" : padutils.escapeHtml(msg.userName);
var html = "<p class='" + authorClass + "'><b>" + authorName + ":</b><span class='time'>" + timeStr + "</span> " + text + "</p>"; var html = "<p class='" + authorClass + "'><b>" + authorName + ":</b><span class='time'>" + timeStr + "</span> " + text + "</p>";
@ -98,9 +111,22 @@ var chat = (function()
var count = Number($("#chatcounter").text()); var count = Number($("#chatcounter").text());
count++; count++;
$("#chatcounter").text(count); $("#chatcounter").text(count);
// chat throb stuff -- Just make it throb in for ~2 secs then fadeotu // chat throb stuff -- Just make it throw for twice as long
$('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text); if(wasMentioned)
$('#chatthrob').effect("pulsate", {times:1,mode:"hide"},2000); { // If the user was mentioned show for twice as long and flash the browser window
if (chatMentions == 0){
title = document.title;
}
$('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text);
$('#chatthrob').effect("pulsate", {times:1,mode:"hide"},4000);
chatMentions++;
document.title = "("+chatMentions+") " + title;
}
else
{
$('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text);
$('#chatthrob').effect("pulsate", {times:1,mode:"hide"},2000);
}
} }
self.scrollDown(); self.scrollDown();

View file

@ -77,10 +77,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
{ {
if (socket) if (socket)
{ {
/*socket.onclosed = function() {}; setChannelState("DISCONNECTED", "unload");
socket.onhiccup = function() {};
socket.disconnect(true);*/
socket.disconnect();
} }
}); });
if ($.browser.mozilla) if ($.browser.mozilla)
@ -100,18 +97,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool); editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool);
editor.setUserChangeNotificationCallback(wrapRecordingErrors("handleUserChanges", handleUserChanges)); editor.setUserChangeNotificationCallback(wrapRecordingErrors("handleUserChanges", handleUserChanges));
function abandonConnection(reason)
{
if (socket)
{
/*socket.onclosed = function() {};
socket.onhiccup = function() {};*/
socket.disconnect();
}
socket = null;
setChannelState("DISCONNECTED", reason);
}
function dmesg(str) function dmesg(str)
{ {
if (typeof window.ajlog == "string") window.ajlog += str + '\n'; if (typeof window.ajlog == "string") window.ajlog += str + '\n';
@ -124,7 +109,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
{ {
if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000))
{ {
abandonConnection("initsocketfail"); // give up setChannelState("DISCONNECTED", "initsocketfail");
} }
else else
{ {
@ -141,8 +126,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
if (state == "COMMITTING" && (t - lastCommitTime) > 20000) if (state == "COMMITTING" && (t - lastCommitTime) > 20000)
{ {
// a commit is taking too long // a commit is taking too long
appLevelDisconnectReason = "slowcommit"; setChannelState("DISCONNECTED", "slowcommit");
socket.disconnect();
} }
else if (state == "COMMITTING" && (t - lastCommitTime) > 5000) else if (state == "COMMITTING" && (t - lastCommitTime) > 5000)
{ {
@ -230,11 +214,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
handleMessageFromServer(obj); handleMessageFromServer(obj);
});*/ });*/
socket.on('disconnect', function(obj)
{
handleSocketClosed(true);
});
/*var success = false; /*var success = false;
callCatchingErrors("setUpSocket", function() { callCatchingErrors("setUpSocket", function() {
appLevelDisconnectReason = null; appLevelDisconnectReason = null;
@ -366,7 +345,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
if (newRev != (rev + 1)) if (newRev != (rev + 1))
{ {
dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (rev + 1)); dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (rev + 1));
socket.disconnect(); setChannelState("DISCONNECTED", "badmessage_newchanges");
return; return;
} }
rev = newRev; rev = newRev;
@ -378,7 +357,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
if (newRev != (rev + 1)) if (newRev != (rev + 1))
{ {
dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (rev + 1)); dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (rev + 1));
socket.disconnect(); setChannelState("DISCONNECTED", "badmessage_acceptcommit");
return; return;
} }
rev = newRev; rev = newRev;
@ -520,48 +499,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
//pad.dmesg($.map(getConnectedUsers(), function(u) { return u.userId.slice(-2); }).join(',')); //pad.dmesg($.map(getConnectedUsers(), function(u) { return u.userId.slice(-2); }).join(','));
} }
function handleSocketClosed(params)
{
socket = null;
$.each(keys(userSet), function()
{
var uid = String(this);
if (uid != userId)
{
var userInfo = userSet[uid];
delete userSet[uid];
callbacks.onUserLeave(userInfo);
dmesgUsers();
}
});
var reason = appLevelDisconnectReason || params.reason;
var shouldReconnect = params.reconnect;
if (shouldReconnect)
{
// determine if this is a tight reconnect loop due to weird connectivity problems
reconnectTimes.push(+new Date());
var TOO_MANY_RECONNECTS = 8;
var TOO_SHORT_A_TIME_MS = 10000;
if (reconnectTimes.length >= TOO_MANY_RECONNECTS && ((+new Date()) - reconnectTimes[reconnectTimes.length - TOO_MANY_RECONNECTS]) < TOO_SHORT_A_TIME_MS)
{
setChannelState("DISCONNECTED", "looping");
}
else
{
setChannelState("RECONNECTING", reason);
setUpSocket();
}
}
else
{
setChannelState("DISCONNECTED", reason);
}
}
function setChannelState(newChannelState, moreInfo) function setChannelState(newChannelState, moreInfo)
{ {
if (newChannelState != channelState) if (newChannelState != channelState)
@ -650,128 +587,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
return rev; return rev;
} }
function getDiagnosticInfo()
{
var maxCaughtErrors = 3;
var maxAceErrors = 3;
var maxDebugMessages = 50;
var longStringCutoff = 500;
function trunc(str)
{
return String(str).substring(0, longStringCutoff);
}
var info = {
errors: {
length: 0
}
};
function addError(e, catcher, time)
{
var error = {
catcher: catcher
};
if (time) error.time = time;
// a little over-cautious?
try
{
if (e.description) error.description = e.description;
}
catch (x)
{}
try
{
if (e.fileName) error.fileName = e.fileName;
}
catch (x)
{}
try
{
if (e.lineNumber) error.lineNumber = e.lineNumber;
}
catch (x)
{}
try
{
if (e.message) error.message = e.message;
}
catch (x)
{}
try
{
if (e.name) error.name = e.name;
}
catch (x)
{}
try
{
if (e.number) error.number = e.number;
}
catch (x)
{}
try
{
if (e.stack) error.stack = trunc(e.stack);
}
catch (x)
{}
info.errors[info.errors.length] = error;
info.errors.length++;
}
for (var i = 0;
((i < caughtErrors.length) && (i < maxCaughtErrors)); i++)
{
addError(caughtErrors[i], caughtErrorCatchers[i], caughtErrorTimes[i]);
}
if (editor)
{
var aceErrors = editor.getUnhandledErrors();
for (var i = 0;
((i < aceErrors.length) && (i < maxAceErrors)); i++)
{
var errorRecord = aceErrors[i];
addError(errorRecord.error, "ACE", errorRecord.time);
}
}
info.time = +new Date();
info.collabState = state;
info.channelState = channelState;
info.lastCommitTime = lastCommitTime;
info.numSocketReconnects = reconnectTimes.length;
info.userId = userId;
info.currentRev = rev;
info.participants = (function()
{
var pp = [];
for (var u in userSet)
{
pp.push(u);
}
return pp.join(',');
})();
if (debugMessages.length > maxDebugMessages)
{
debugMessages = debugMessages.slice(debugMessages.length - maxDebugMessages, debugMessages.length);
}
info.debugMessages = {
length: 0
};
for (var i = 0; i < debugMessages.length; i++)
{
info.debugMessages[i] = trunc(debugMessages[i]);
info.debugMessages.length++;
}
return info;
}
function getMissedChanges() function getMissedChanges()
{ {
var obj = {}; var obj = {};
@ -863,10 +678,10 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options)
sendClientMessage: sendClientMessage, sendClientMessage: sendClientMessage,
sendMessage: sendMessage, sendMessage: sendMessage,
getCurrentRevisionNumber: getCurrentRevisionNumber, getCurrentRevisionNumber: getCurrentRevisionNumber,
getDiagnosticInfo: getDiagnosticInfo,
getMissedChanges: getMissedChanges, getMissedChanges: getMissedChanges,
callWhenNotCommitting: callWhenNotCommitting, callWhenNotCommitting: callWhenNotCommitting,
addHistoricalAuthors: tellAceAboutHistoricalAuthors addHistoricalAuthors: tellAceAboutHistoricalAuthors,
setChannelState: setChannelState
}); });
} }

View file

@ -473,7 +473,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class
if (tname == "ul") if (tname == "ul")
{ {
var type; var type;
var rr = cls && /(?:^| )list-(bullet[12345678])\b/.exec(cls); var rr = cls && /(?:^| )list-([a-z]+[12345678])\b/.exec(cls);
type = rr && rr[1] || "bullet" + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1)); type = rr && rr[1] || "bullet" + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
oldListTypeOrNull = (_enterList(state, type) || 'none'); oldListTypeOrNull = (_enterList(state, type) || 'none');
} }

View file

@ -152,6 +152,10 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument)
{ {
if (href) if (href)
{ {
if(!~href.indexOf("http")) // if the url doesn't include http or https etc prefix it.
{
href = "http://"+href;
}
extraOpenTags = extraOpenTags + '<a href="' + href.replace(/\"/g, '&quot;') + '">'; extraOpenTags = extraOpenTags + '<a href="' + href.replace(/\"/g, '&quot;') + '">';
extraCloseTags = '</a>' + extraCloseTags; extraCloseTags = '</a>' + extraCloseTags;
} }

View file

@ -237,7 +237,7 @@ linestylefilter.getRegexpFilter = function(regExp, tag)
linestylefilter.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]/; linestylefilter.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]/;
linestylefilter.REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + linestylefilter.REGEX_WORDCHAR.source + ')'); linestylefilter.REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + linestylefilter.REGEX_WORDCHAR.source + ')');
linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + linestylefilter.REGEX_URLCHAR.source + '*(?![:.,;])' + linestylefilter.REGEX_URLCHAR.source, 'g'); linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:|www\.)/.source + linestylefilter.REGEX_URLCHAR.source + '*(?![:.,;])' + linestylefilter.REGEX_URLCHAR.source, 'g');
linestylefilter.getURLFilter = linestylefilter.getRegexpFilter( linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(
linestylefilter.REGEX_URL, 'url'); linestylefilter.REGEX_URL, 'url');

View file

@ -79,12 +79,13 @@ function randomString()
function getParams() function getParams()
{ {
var showControls = getUrlVars()["showControls"]; var params = getUrlVars()
var showChat = getUrlVars()["showChat"]; var showControls = params["showControls"];
var userName = unescape(getUrlVars()["userName"]); var showChat = params["showChat"];
var showLineNumbers = getUrlVars()["showLineNumbers"]; var userName = params["userName"];
var useMonospaceFont = getUrlVars()["useMonospaceFont"]; var showLineNumbers = params["showLineNumbers"];
var IsnoColors = getUrlVars()["noColors"]; var useMonospaceFont = params["useMonospaceFont"];
var IsnoColors = params["noColors"];
if(IsnoColors) if(IsnoColors)
{ {
@ -130,7 +131,7 @@ function getParams()
if(userName) if(userName)
{ {
// If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad.
globalUserName = userName; globalUserName = unescape(userName);
} }
} }
@ -166,15 +167,17 @@ function handshake()
var resource = loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "socket.io"; var resource = loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "socket.io";
//connect //connect
socket = io.connect(url, { socket = io.connect(url, {
resource: resource resource: resource,
'max reconnection attempts': 3
}); });
socket.once('connect', function() function sendClientReady(isReconnect)
{ {
var padId = document.location.pathname.substring(document.location.pathname.lastIndexOf("/") + 1); var padId = document.location.pathname.substring(document.location.pathname.lastIndexOf("/") + 1);
padId = unescape(padId); // unescape neccesary due to Safari and Opera interpretation of spaces padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces
document.title = document.title + " | " + padId; if(!isReconnect)
document.title = document.title + " | " + padId;
var token = readCookie("token"); var token = readCookie("token");
if (token == null) if (token == null)
@ -195,7 +198,43 @@ function handshake()
"token": token, "token": token,
"protocolVersion": 2 "protocolVersion": 2
}; };
//this is a reconnect, lets tell the server our revisionnumber
if(isReconnect == true)
{
msg.client_rev=pad.collabClient.getCurrentRevisionNumber();
msg.reconnect=true;
}
socket.json.send(msg); socket.json.send(msg);
};
var disconnectTimeout;
socket.once('connect', function () {
sendClientReady(false);
});
socket.on('reconnect', function () {
//reconnect is before the timeout, lets stop the timeout
if(disconnectTimeout)
{
clearTimeout(disconnectTimeout);
}
pad.collabClient.setChannelState("CONNECTED");
sendClientReady(true);
});
socket.on('disconnect', function () {
function disconnectEvent()
{
pad.collabClient.setChannelState("DISCONNECTED", "reconnect_timeout");
}
pad.collabClient.setChannelState("RECONNECTING");
disconnectTimeout = setTimeout(disconnectEvent, 10000);
}); });
var receivedClientVars = false; var receivedClientVars = false;
@ -351,7 +390,6 @@ var pad = {
//initialize the chat //initialize the chat
chat.init(); chat.init();
pad.diagnosticInfo.uniqueId = padutils.uniqueId();
pad.initTime = +(new Date()); pad.initTime = +(new Date());
pad.padOptions = clientVars.initialOptions; pad.padOptions = clientVars.initialOptions;
@ -648,7 +686,22 @@ var pad = {
else if (newState == "DISCONNECTED") else if (newState == "DISCONNECTED")
{ {
pad.diagnosticInfo.disconnectedMessage = message; pad.diagnosticInfo.disconnectedMessage = message;
pad.diagnosticInfo.padInitTime = pad.initTime; pad.diagnosticInfo.padId = pad.getPadId();
pad.diagnosticInfo.socket = {};
//we filter non objects from the socket object and put them in the diagnosticInfo
//this ensures we have no cyclic data - this allows us to stringify the data
for(var i in socket.socket)
{
var value = socket.socket[i];
var type = typeof value;
if(type == "string" || type == "number")
{
pad.diagnosticInfo.socket[i] = value;
}
}
pad.asyncSendDiagnosticInfo(); pad.asyncSendDiagnosticInfo();
if (typeof window.ajlog == "string") if (typeof window.ajlog == "string")
{ {
@ -720,7 +773,6 @@ var pad = {
}, },
asyncSendDiagnosticInfo: function() asyncSendDiagnosticInfo: function()
{ {
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
window.setTimeout(function() window.setTimeout(function()
{ {
$.ajax( $.ajax(
@ -728,7 +780,6 @@ var pad = {
type: 'post', type: 'post',
url: '/ep/pad/connection-diagnostic-info', url: '/ep/pad/connection-diagnostic-info',
data: { data: {
padId: pad.getPadId(),
diagnosticInfo: JSON.stringify(pad.diagnosticInfo) diagnosticInfo: JSON.stringify(pad.diagnosticInfo)
}, },
success: function() success: function()
@ -808,7 +859,7 @@ var pad = {
}, },
preloadImages: function() preloadImages: function()
{ {
var images = []; // Removed as we now use CSS and JS for colorpicker var images = ["../static/img/connectingbar.gif"];
function loadNextImage() function loadNextImage()
{ {

View file

@ -99,26 +99,16 @@ var padeditbar = (function()
self.toogleDropDown("users"); self.toogleDropDown("users");
} }
else if (cmd == 'embed') else if (cmd == 'embed')
{ {
var padurl = window.location.href.split("?")[0]; self.setEmbedLinks();
$('#embedinput').val("<iframe src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>");
self.toogleDropDown("embed");
$('#embedinput').focus().select(); $('#embedinput').focus().select();
self.toogleDropDown("embed");
} }
else if (cmd == 'import_export') else if (cmd == 'import_export')
{ {
self.toogleDropDown("importexport"); self.toogleDropDown("importexport");
} }
else if (cmd == 'readonly')
{
var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/"));
var readonlyLink = basePath + "/ro/" + clientVars.readOnlyId;
$('#readonlyImage').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=H|0&chl=" + readonlyLink);
$('#readonlyInput').val(readonlyLink);
self.toogleDropDown("readonly");
$('#readonlyInput').focus().select();
}
else if (cmd == 'save') else if (cmd == 'save')
{ {
padsavedrevs.saveNow(); padsavedrevs.saveNow();
@ -211,6 +201,24 @@ var padeditbar = (function()
{ {
syncAnimation.done(); syncAnimation.done();
} }
},
setEmbedLinks: function()
{
if ($('#readonlyinput').is(':checked'))
{
var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/"));
var readonlyLink = basePath + "/ro/" + clientVars.readOnlyId;
$('#embedinput').val("<iframe src='" + readonlyLink + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>");
$('#linkinput').val(readonlyLink);
$('#embedreadonlyqr').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=H|0&chl=" + readonlyLink);
}
else
{
var padurl = window.location.href.split("?")[0];
$('#embedinput').val("<iframe src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>");
$('#linkinput').val(padurl);
$('#embedreadonlyqr').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=H|0&chl=" + padurl);
}
} }
}; };
return self; return self;

View file

@ -711,6 +711,7 @@ var paduserlist = (function()
} }
$("#myswatch").css({'background-color': myUserInfo.colorId}); $("#myswatch").css({'background-color': myUserInfo.colorId});
$("#usericon").css({'box-shadow': 'inset 0 0 30px ' + myUserInfo.colorId});
} }
}; };
return self; return self;

View file

@ -50,12 +50,12 @@
</a> </a>
</li> </li>
<li onClick="window.pad&&pad.editbarClick('indent');return false;" > <li onClick="window.pad&&pad.editbarClick('indent');return false;" >
<a title="Indent List"> <a title="Indent">
<div class="buttonicon" style="background-position:0px -52px"></div> <div class="buttonicon" style="background-position:0px -52px"></div>
</a> </a>
</li> </li>
<li onClick="window.pad&&pad.editbarClick('outdent');return false;" > <li onClick="window.pad&&pad.editbarClick('outdent');return false;" >
<a title="Unindent List"> <a title="Unindent">
<div class="buttonicon" style="background-position:0px -134px"></div> <div class="buttonicon" style="background-position:0px -134px"></div>
</a> </a>
</li> </li>
@ -80,18 +80,13 @@
<ul id="menu_right"> <ul id="menu_right">
<li onClick="window.pad&&pad.editbarClick('readonly');return false;" >
<a id="readonlylink" title="Create a readonly link for this pad">
<div class="buttonicon" style="background-position:0px -150px"></div>
</a>
</li>
<li onClick="window.pad&&pad.editbarClick('import_export');return false;"> <li onClick="window.pad&&pad.editbarClick('import_export');return false;">
<a id="exportlink" title="Import/Export from/to different document formats"> <a id="exportlink" title="Import/Export from/to different document formats">
<div class="buttonicon" style="background-position:0px -68px"></div> <div class="buttonicon" style="background-position:0px -68px"></div>
</a> </a>
</li> </li>
<li onClick="window.pad&&pad.editbarClick('embed');return false;" > <li onClick="window.pad&&pad.editbarClick('embed');return false;" >
<a id="embedlink" title="Embed this pad"> <a id="embedlink" title="Share and Embed this pad">
<div class="buttonicon" style="background-position:0px -18px"></div> <div class="buttonicon" style="background-position:0px -18px"></div>
</a> </a>
</li> </li>
@ -103,7 +98,7 @@
</li> </li>
<li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" > <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" >
<a title="Show connected users"> <a title="Show connected users">
<div class="buttonicon" style="background-position:0px -184px;display:inline-block;"></div> <div class="buttonicon" id="usericonback" style="background-position:0px -184px;display:inline-block;"></div>
<span id="online_count">1</span> <span id="online_count">1</span>
</a> </a>
</li> </li>
@ -217,17 +212,21 @@
<!-- the embed code --> <!-- the embed code -->
<div id="embed"> <div id="embed">
<div id="embedcode"> <div id="embedreadonly">
Embed code:<input id="embedinput" type="text" value=""> <input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();"/><label for="readonlyinput">Read only</label>
</div> </div>
</div> Share:
<br/>
<!-- the embed code --> <div id="linkcode">
<div id="readonly"> <label for="linkinput">Link:</label><input id="linkinput" type="text" value="">
<div id="readonlyUrl"> </div><br/>
Use this link to share a read-only version of your pad:<input id="readonlyInput" type="text" value=""> <div id="embedcode">
<img id="readonlyImage" src="" alt="" style="margin: 10px 90px"> <label for="embedinput">Embed code:</label><input id="embedinput" type="text" value="">
</div> </div><br/>
<div id="qrcode">
<label for="embedreadonlyqr">QR code:</label><br/>
<div id="qr_center"><img id="embedreadonlyqr"></div>
</div>
</div> </div>
<div id="chatthrob"> <div id="chatthrob">

View file

@ -62,7 +62,7 @@
//get the padId out of the url //get the padId out of the url
var urlParts= document.location.pathname.split("/"); var urlParts= document.location.pathname.split("/");
padId = urlParts[urlParts.length-2]; padId = decodeURIComponent(urlParts[urlParts.length-2]);
//set the title //set the title
document.title = document.title + " | " + padId; document.title = document.title + " | " + padId;
@ -271,7 +271,11 @@
<!-- termporary place holder--> <!-- termporary place holder-->
<a id = "returnbutton">Return to pad</a> <a id = "returnbutton">Return to pad</a>
<script> <script>
$("#returnbutton").attr("href", document.referrer); if(document.referrer.length > 0 && document.referrer.substring(document.referrer.lastIndexOf("/")-1,document.referrer.lastIndexOf("/")) === "p") {
$("#returnbutton").attr("href", document.referrer);
} else {
$("#returnbutton").attr("href", document.location.href.substring(0,document.location.href.lastIndexOf("/")));
}
</script> </script>
</div> </div>