diff --git a/.gitignore b/.gitignore
index 31b7d2ea..4f315224 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,5 @@
node_modules
settings.json
-static/js/jquery.js
-static/js/prefixfree.js
APIKEY.txt
bin/abiword.exe
bin/node.exe
@@ -10,4 +8,7 @@ var/dirty.db
bin/convertSettings.json
*~
*.patch
-*.DS_Store
\ No newline at end of file
+src/static/js/jquery.js
+npm-debug.log
+*.DS_Store
+.ep_initialized
diff --git a/README.plugins b/README.plugins
new file mode 100644
index 00000000..72c45644
--- /dev/null
+++ b/README.plugins
@@ -0,0 +1,16 @@
+So, a plugin is an npm package whose name starts with ep_ and that contains a file ep.json
+require("ep_etherpad-lite/static/js/plugingfw/plugins").update() will use npm to list all installed modules and read their ep.json files. These will contain registrations for hooks which are loaded
+A hook registration is a pairs of a hook name and a function reference (filename for require() plus function name)
+require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value}) will call all hook functions registered for hook_name
+That is the basis.
+Ok, so that was a slight simplification: inside ep.json, hook registrations are grouped into groups called "parts". Parts from all plugins are ordered using a topological sort according to "pre" and "post" pointers to other plugins/parts (just like dependencies, but non-installed plugins are silently ignored).
+This ordering is honored when you do callAll(hook_name) - hook functions for that hook_name are called in that order
+Ordering between plugins is undefined, only parts are ordered.
+
+A plugin usually has one part, but it van have multiple.
+This is so that it can insert some hook registration before that of another plugin, and another one after.
+This is important for e.g. registering URL-handlers for the express webserver, if you have some very generic and some very specific url-regexps
+So, that's basically it... apart from client-side hooks
+which works the same way, but uses a separate member of the part (part.client_hooks vs part.hooks), and where the hook function must obviously reside in a file require():able from the client...
+One thing more: The main etherpad tree is actually a plugin itself, called ep_etherpad-lite, and it has it's own ep.json...
+was that clear?
\ No newline at end of file
diff --git a/available_plugins/ep_fintest/.npmignore b/available_plugins/ep_fintest/.npmignore
new file mode 100644
index 00000000..74bd365b
--- /dev/null
+++ b/available_plugins/ep_fintest/.npmignore
@@ -0,0 +1,7 @@
+.git*
+docs/
+examples/
+support/
+test/
+testing.js
+.DS_Store
diff --git a/available_plugins/ep_fintest/ep.json b/available_plugins/ep_fintest/ep.json
new file mode 100644
index 00000000..4ec8e392
--- /dev/null
+++ b/available_plugins/ep_fintest/ep.json
@@ -0,0 +1,36 @@
+{
+ "parts": [
+ {
+ "name": "somepart",
+ "pre": [],
+ "post": ["ep_onemoreplugin/partone"]
+ },
+ {
+ "name": "partlast",
+ "pre": ["ep_fintest/otherpart"],
+ "post": [],
+ "hooks": {
+ "somehookname": "ep_fintest/partlast:somehook"
+ }
+ },
+ {
+ "name": "partfirst",
+ "pre": [],
+ "post": ["ep_onemoreplugin/somepart"]
+ },
+ {
+ "name": "otherpart",
+ "pre": ["ep_fintest/somepart", "ep_otherplugin/main"],
+ "post": [],
+ "hooks": {
+ "somehookname": "ep_fintest/otherpart:somehook",
+ "morehook": "ep_fintest/otherpart:morehook",
+ "expressCreateServer": "ep_fintest/otherpart:expressServer",
+ "eejsBlock_editbarMenuLeft": "ep_fintest/otherpart:eejsBlock_editbarMenuLeft"
+ },
+ "client_hooks": {
+ "somehookname": "ep_fintest/static/js/test:bar"
+ }
+ }
+ ]
+}
diff --git a/available_plugins/ep_fintest/otherpart.js b/available_plugins/ep_fintest/otherpart.js
new file mode 100644
index 00000000..718fb095
--- /dev/null
+++ b/available_plugins/ep_fintest/otherpart.js
@@ -0,0 +1,25 @@
+test = require("ep_fintest/static/js/test.js");
+console.log("FOOO:", test.foo);
+
+exports.somehook = function (hook_name, args, cb) {
+ return cb(["otherpart:somehook was here"]);
+}
+
+exports.morehook = function (hook_name, args, cb) {
+ return cb(["otherpart:morehook was here"]);
+}
+
+exports.expressServer = function (hook_name, args, cb) {
+ args.app.get('/otherpart', function(req, res) {
+ res.send("Abra cadabra");
+ });
+}
+
+exports.eejsBlock_editbarMenuLeft = function (hook_name, args, cb) {
+ args.content = args.content + '\
+
\
+ \
+
\
+ ';
+ return cb();
+}
diff --git a/available_plugins/ep_fintest/package.json b/available_plugins/ep_fintest/package.json
new file mode 100644
index 00000000..e221b5c1
--- /dev/null
+++ b/available_plugins/ep_fintest/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "ep_fintest",
+ "description": "A test plugin",
+ "version": "0.0.1",
+ "author": "RedHog (Egil Moeller) ",
+ "contributors": [],
+ "dependencies": {},
+ "engines": { "node": ">= 0.4.1 < 0.7.0" }
+}
diff --git a/available_plugins/ep_fintest/partlast.js b/available_plugins/ep_fintest/partlast.js
new file mode 100644
index 00000000..c3f1fc3e
--- /dev/null
+++ b/available_plugins/ep_fintest/partlast.js
@@ -0,0 +1,3 @@
+exports.somehook = function (hook_name, args, cb) {
+ return cb(["partlast:somehook was here"]);
+}
diff --git a/available_plugins/ep_fintest/static/js/test.js b/available_plugins/ep_fintest/static/js/test.js
new file mode 100644
index 00000000..22d58cc2
--- /dev/null
+++ b/available_plugins/ep_fintest/static/js/test.js
@@ -0,0 +1,5 @@
+exports.foo = 42;
+
+exports.bar = function (hook_name, args, cb) {
+ return cb(["FOOOO"]);
+}
\ No newline at end of file
diff --git a/available_plugins/ep_fintest/static/test.html b/available_plugins/ep_fintest/static/test.html
new file mode 100644
index 00000000..9e7fc551
--- /dev/null
+++ b/available_plugins/ep_fintest/static/test.html
@@ -0,0 +1 @@
+Test bla bla
diff --git a/bin/checkPad.js b/bin/checkPad.js
index 356b0779..a46c1814 100644
--- a/bin/checkPad.js
+++ b/bin/checkPad.js
@@ -4,19 +4,19 @@
if(process.argv.length != 3)
{
- console.error("Use: node checkPad.js $PADID");
+ console.error("Use: node bin/checkPad.js $PADID");
process.exit(1);
}
//get the padID
var padId = process.argv[2];
//initalize the database
-var log4js = require("log4js");
+var log4js = require("../src/node_modules/log4js");
log4js.setGlobalLogLevel("INFO");
-var async = require("async");
-var db = require('../node/db/DB');
-var CommonCode = require('../node/utils/common_code');
-var Changeset = CommonCode.require("/Changeset");
+var async = require("../src/node_modules/async");
+var db = require('../src/node/db/DB');
+
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var padManager;
async.series([
@@ -28,7 +28,7 @@ async.series([
//get the pad
function (callback)
{
- padManager = require('../node/db/PadManager');
+ padManager = require('../src/node/db/PadManager');
padManager.doesPadExists(padId, function(err, exists)
{
diff --git a/bin/convert.js b/bin/convert.js
index c5dc535c..ec792717 100644
--- a/bin/convert.js
+++ b/bin/convert.js
@@ -1,12 +1,12 @@
-var CommonCode = require('../node/utils/common_code');
+
var startTime = new Date().getTime();
var fs = require("fs");
var ueberDB = require("ueberDB");
var mysql = require("mysql");
var async = require("async");
-var Changeset = CommonCode.require("/Changeset");
-var randomString = CommonCode.require('/pad_utils').randomString;
-var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
var settingsFile = process.argv[2];
var sqlOutputFile = process.argv[3];
@@ -384,7 +384,7 @@ function convertPad(padId, callback)
}
//generate the latest atext
- var fullAPool = AttributePoolFactory.createAttributePool().fromJsonable(apool);
+ var fullAPool = (new AttributePool()).fromJsonable(apool);
var keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval;
var atext = changesetsMeta[keyRev].atext;
var curRev = keyRev;
diff --git a/bin/debugRun.sh b/bin/debugRun.sh
index 01197a6b..f90009d0 100755
--- a/bin/debugRun.sh
+++ b/bin/debugRun.sh
@@ -22,8 +22,7 @@ node-inspector &
echo "If you are new to node-inspector, take a look at this video: http://youtu.be/AOnK3NVnxL8"
-cd "node"
-node --debug server.js
+node --debug node_modules/ep_etherpad-lite/node/server.js $*
#kill node-inspector before ending
kill $!
diff --git a/bin/installDeps.sh b/bin/installDeps.sh
index 270ec98c..2acebd82 100755
--- a/bin/installDeps.sh
+++ b/bin/installDeps.sh
@@ -55,7 +55,13 @@ if [ ! -f $settings ]; then
fi
echo "Ensure that all dependencies are up to date..."
-npm install || {
+(
+ mkdir -p node_modules
+ cd node_modules
+ [ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite
+ cd ep_etherpad-lite
+ npm install
+) || {
rm -rf node_modules
exit 1
}
@@ -63,8 +69,8 @@ npm install || {
echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true"
NEEDED_VERSION="1.7.1"
-if [ -f "static/js/jquery.js" ]; then
- VERSION=$(cat static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
+if [ -f "src/static/js/jquery.js" ]; then
+ VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
if [ ${VERSION#v} = $NEEDED_VERSION ]; then
DOWNLOAD_JQUERY="false"
@@ -72,22 +78,7 @@ if [ -f "static/js/jquery.js" ]; then
fi
if [ $DOWNLOAD_JQUERY = "true" ]; then
- curl -lo static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1
-fi
-
-echo "Ensure prefixfree is downloaded and up to date..."
-DOWNLOAD_PREFIXFREE="true"
-NEEDED_VERSION="1.0.4"
-if [ -f "static/js/prefixfree.js" ]; then
- VERSION=$(cat static/js/prefixfree.js | grep "PrefixFree" | grep -o "[0-9].[0-9].[0-9]");
-
- if [ $VERSION = $NEEDED_VERSION ]; then
- DOWNLOAD_PREFIXFREE="false"
- fi
-fi
-
-if [ $DOWNLOAD_PREFIXFREE = "true" ]; then
- curl -lo static/js/prefixfree.js -k https://raw.github.com/LeaVerou/prefixfree/master/prefixfree.js || exit 1
+ curl -lo src/static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1
fi
#Remove all minified data to force node creating it new
@@ -98,12 +89,12 @@ echo "ensure custom css/js files are created..."
for f in "index" "pad" "timeslider"
do
- if [ ! -f "static/custom/$f.js" ]; then
- cp -v "static/custom/js.template" "static/custom/$f.js" || exit 1
+ if [ ! -f "src/static/custom/$f.js" ]; then
+ cp -v "src/static/custom/js.template" "src/static/custom/$f.js" || exit 1
fi
- if [ ! -f "static/custom/$f.css" ]; then
- cp -v "static/custom/css.template" "static/custom/$f.css" || exit 1
+ if [ ! -f "src/static/custom/$f.css" ]; then
+ cp -v "src/static/custom/css.template" "src/static/custom/$f.css" || exit 1
fi
done
diff --git a/bin/run.sh b/bin/run.sh
index c409920e..82e89a94 100755
--- a/bin/run.sh
+++ b/bin/run.sh
@@ -25,5 +25,4 @@ bin/installDeps.sh $* || exit 1
#Move to the node folder and start
echo "start..."
-cd "node"
-node server.js $*
+node node_modules/ep_etherpad-lite/node/server.js $*
diff --git a/node/server.js b/node/server.js
deleted file mode 100644
index c5377d81..00000000
--- a/node/server.js
+++ /dev/null
@@ -1,500 +0,0 @@
-/**
- * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server.
- * Static file Requests are answered directly from this module, Socket.IO messages are passed
- * to MessageHandler and minfied requests are passed to minified.
- */
-
-/*
- * 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS-IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-var ERR = require("async-stacktrace");
-var log4js = require('log4js');
-var os = require("os");
-var socketio = require('socket.io');
-var fs = require('fs');
-var settings = require('./utils/Settings');
-var db = require('./db/DB');
-var async = require('async');
-var express = require('express');
-var path = require('path');
-var minify = require('./utils/Minify');
-var CachingMiddleware = require('./utils/caching_middleware');
-var Yajsml = require('yajsml');
-var formidable = require('formidable');
-var apiHandler;
-var exportHandler;
-var importHandler;
-var exporthtml;
-var readOnlyManager;
-var padManager;
-var securityManager;
-var socketIORouter;
-
-//try to get the git version
-var version = "";
-try
-{
- var rootPath = path.normalize(__dirname + "/../")
- var ref = fs.readFileSync(rootPath + ".git/HEAD", "utf-8");
- var refPath = rootPath + ".git/" + ref.substring(5, ref.indexOf("\n"));
- version = fs.readFileSync(refPath, "utf-8");
- version = version.substring(0, 7);
- console.log("Your Etherpad Lite git version is " + version);
-}
-catch(e)
-{
- console.warn("Can't get git version for server header\n" + e.message)
-}
-
-console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues")
-
-var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)";
-
-exports.maxAge = settings.maxAge;
-
-//set loglevel
-log4js.setGlobalLogLevel(settings.loglevel);
-
-async.waterfall([
- //initalize the database
- function (callback)
- {
- db.init(callback);
- },
- //initalize the http server
- function (callback)
- {
- //create server
- var app = express.createServer();
-
- app.use(function (req, res, next) {
- res.header("Server", serverName);
- next();
- });
-
-
- //redirects browser to the pad's sanitized url if needed. otherwise, renders the html
- app.param('pad', function (req, res, next, padId) {
- //ensure the padname is valid and the url doesn't end with a /
- if(!padManager.isValidPadId(padId) || /\/$/.test(req.url))
- {
- res.send('Such a padname is forbidden', 404);
- }
- else
- {
- padManager.sanitizePadId(padId, function(sanitizedPadId) {
- //the pad id was sanitized, so we redirect to the sanitized version
- if(sanitizedPadId != padId)
- {
- var real_path = req.path.replace(/^\/p\/[^\/]+/, './' + sanitizedPadId);
- res.header('Location', real_path);
- res.send('You should be redirected to ' + real_path + '', 302);
- }
- //the pad id was fine, so just render it
- else
- {
- next();
- }
- });
- }
- });
-
- //load modules that needs a initalized db
- readOnlyManager = require("./db/ReadOnlyManager");
- exporthtml = require("./utils/ExportHtml");
- exportHandler = require('./handler/ExportHandler');
- importHandler = require('./handler/ImportHandler');
- apiHandler = require('./handler/APIHandler');
- padManager = require('./db/PadManager');
- securityManager = require('./db/SecurityManager');
- socketIORouter = require("./handler/SocketIORouter");
-
- //install logging
- var httpLogger = log4js.getLogger("http");
- app.configure(function()
- {
- // Activate http basic auth if it has been defined in settings.json
- if(settings.httpAuth != null) app.use(basic_auth);
-
- // 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.error(function(err, req, res, next){
- res.send(500);
- console.error(err.stack ? err.stack : err.toString());
- gracefulShutdown();
- });
-
- // Cache both minified and static.
- var assetCache = new CachingMiddleware;
- app.all('/(minified|static)/*', assetCache.handle);
-
- // Minify will serve static files compressed (minify enabled). It also has
- // file-specific hacks for ace/require-kernel/etc.
- app.all('/static/:filename(*)', minify.minify);
-
- // Setup middleware that will package JavaScript files served by minify for
- // CommonJS loader on the client-side.
- var jsServer = new (Yajsml.Server)({
- rootPath: 'minified/'
- , rootURI: 'http://localhost:' + settings.port + '/static/js/'
- });
- var StaticAssociator = Yajsml.associators.StaticAssociator;
- var associations =
- Yajsml.associators.associationsForSimpleMapping(minify.tar);
- var associator = new StaticAssociator(associations);
- jsServer.setAssociator(associator);
- app.use(jsServer);
-
- //checks for padAccess
- function hasPadAccess(req, res, callback)
- {
- securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj)
- {
- if(ERR(err, callback)) return;
-
- //there is access, continue
- if(accessObj.accessStatus == "grant")
- {
- callback();
- }
- //no access
- else
- {
- res.send("403 - Can't touch this", 403);
- }
- });
- }
-
- //checks for basic http auth
- function basic_auth (req, res, next) {
- if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) {
- // fetch login and password
- if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() == settings.httpAuth) {
- next();
- return;
- }
- }
-
- res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
- if (req.headers.authorization) {
- setTimeout(function () {
- res.send('Authentication required', 401);
- }, 1000);
- } else {
- res.send('Authentication required', 401);
- }
- }
-
- //serve read only pad
- app.get('/ro/:id', function(req, res)
- {
- var html;
- var padId;
- var pad;
-
- async.series([
- //translate the read only pad to a padId
- function(callback)
- {
- readOnlyManager.getPadId(req.params.id, function(err, _padId)
- {
- if(ERR(err, callback)) return;
-
- padId = _padId;
-
- //we need that to tell hasPadAcess about the pad
- req.params.pad = padId;
-
- callback();
- });
- },
- //render the html document
- function(callback)
- {
- //return if the there is no padId
- if(padId == null)
- {
- callback("notfound");
- return;
- }
-
- hasPadAccess(req, res, function()
- {
- //render the html document
- exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
- {
- if(ERR(err, callback)) return;
- html = _html;
- callback();
- });
- });
- }
- ], function(err)
- {
- //throw any unexpected error
- if(err && err != "notfound")
- ERR(err);
-
- if(err == "notfound")
- res.send('404 - Not Found', 404);
- else
- res.send(html);
- });
- });
-
- //serve pad.html under /p
- app.get('/p/:pad', function(req, res, next)
- {
- var filePath = path.normalize(__dirname + "/../static/pad.html");
- res.sendfile(filePath, { maxAge: exports.maxAge });
- });
-
- //serve timeslider.html under /p/$padname/timeslider
- app.get('/p/:pad/timeslider', function(req, res, next)
- {
- var filePath = path.normalize(__dirname + "/../static/timeslider.html");
- res.sendfile(filePath, { maxAge: exports.maxAge });
- });
-
- //serve timeslider.html under /p/$padname/timeslider
- app.get('/p/:pad/:rev?/export/:type', function(req, res, next)
- {
- var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"];
- //send a 404 if we don't support this filetype
- if(types.indexOf(req.params.type) == -1)
- {
- next();
- return;
- }
-
- //if abiword is disabled, and this is a format we only support with abiword, output a message
- if(settings.abiword == null &&
- ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1)
- {
- res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
- return;
- }
-
- res.header("Access-Control-Allow-Origin", "*");
-
- hasPadAccess(req, res, function()
- {
- exportHandler.doExport(req, res, req.params.pad, req.params.type);
- });
- });
-
- //handle import requests
- app.post('/p/:pad/import', function(req, res, next)
- {
- //if abiword is disabled, skip handling this request
- if(settings.abiword == null)
- {
- next();
- return;
- }
-
- hasPadAccess(req, res, function()
- {
- importHandler.doImport(req, res, req.params.pad);
- });
- });
-
- var apiLogger = log4js.getLogger("API");
-
- //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("Content-Type", "application/json; charset=utf-8");
-
- apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
-
- //wrap the send function so we can log the response
- res._send = res.send;
- res.send = function(response)
- {
- response = JSON.stringify(response);
- apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
-
- //is this a jsonp call, if yes, add the function call
- if(req.query.jsonp)
- response = req.query.jsonp + "(" + response + ")";
-
- res._send(response);
- }
-
- //call the api handler
- apiHandler.handle(req.params.func, fields, req, res);
- }
-
- //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
- app.post('/ep/pad/connection-diagnostic-info', function(req, res)
- {
- new formidable.IncomingForm().parse(req, function(err, fields, files)
- {
- console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
- res.end("OK");
- });
- });
-
- //The Etherpad client side sends information about client side javscript errors
- app.post('/jserror', function(req, res)
- {
- new formidable.IncomingForm().parse(req, function(err, fields, files)
- {
- console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo);
- res.end("OK");
- });
- });
-
- //serve index.html under /
- app.get('/', function(req, res)
- {
- var filePath = path.normalize(__dirname + "/../static/index.html");
- res.sendfile(filePath, { maxAge: exports.maxAge });
- });
-
- //serve robots.txt
- app.get('/robots.txt', function(req, res)
- {
- var filePath = path.normalize(__dirname + "/../static/robots.txt");
- res.sendfile(filePath, { maxAge: exports.maxAge });
- });
-
- //serve favicon.ico
- app.get('/favicon.ico', function(req, res)
- {
- var filePath = path.normalize(__dirname + "/../static/custom/favicon.ico");
- res.sendfile(filePath, { maxAge: exports.maxAge }, function(err)
- {
- //there is no custom favicon, send the default favicon
- if(err)
- {
- filePath = path.normalize(__dirname + "/../static/favicon.ico");
- res.sendfile(filePath, { maxAge: exports.maxAge });
- }
- });
- });
-
- //let the server listen
- app.listen(settings.port, settings.ip);
- console.log("Server is listening at " + settings.ip + ":" + settings.port);
-
- var onShutdown = false;
- var gracefulShutdown = function(err)
- {
- if(err && err.stack)
- {
- console.error(err.stack);
- }
- else if(err)
- {
- console.error(err);
- }
-
- //ensure there is only one graceful shutdown running
- if(onShutdown) return;
- onShutdown = true;
-
- console.log("graceful shutdown...");
-
- //stop the http server
- app.close();
-
- //do the db shutdown
- db.db.doShutdown(function()
- {
- console.log("db sucessfully closed.");
-
- process.exit(0);
- });
-
- setTimeout(function(){
- process.exit(1);
- }, 3000);
- }
-
- //connect graceful shutdown with sigint and uncaughtexception
- if(os.type().indexOf("Windows") == -1)
- {
- //sigint is so far not working on windows
- //https://github.com/joyent/node/issues/1553
- process.on('SIGINT', gracefulShutdown);
- }
-
- process.on('uncaughtException', gracefulShutdown);
-
- //init socket.io and redirect all requests to the MessageHandler
- var io = socketio.listen(app);
-
- //this is only a workaround to ensure it works with all browers behind a proxy
- //we should remove this when the new socket.io version is more stable
- io.set('transports', ['xhr-polling']);
-
- var socketIOLogger = log4js.getLogger("socket.io");
- io.set('logger', {
- debug: function (str)
- {
- socketIOLogger.debug.apply(socketIOLogger, arguments);
- },
- info: function (str)
- {
- socketIOLogger.info.apply(socketIOLogger, arguments);
- },
- warn: function (str)
- {
- socketIOLogger.warn.apply(socketIOLogger, arguments);
- },
- error: function (str)
- {
- socketIOLogger.error.apply(socketIOLogger, arguments);
- },
- });
-
- //minify socket.io javascript
- if(settings.minify)
- io.enable('browser client minification');
-
- var padMessageHandler = require("./handler/PadMessageHandler");
- var timesliderMessageHandler = require("./handler/TimesliderMessageHandler");
-
- //Initalize the Socket.IO Router
- socketIORouter.setSocketIO(io);
- socketIORouter.addComponent("pad", padMessageHandler);
- socketIORouter.addComponent("timeslider", timesliderMessageHandler);
-
- callback(null);
- }
-]);
diff --git a/settings.json.template b/settings.json.template
index 94a60fd4..f89fcd8e 100644
--- a/settings.json.template
+++ b/settings.json.template
@@ -13,7 +13,7 @@
"dbType" : "dirty",
//the database specific settings
"dbSettings" : {
- "filename" : "../var/dirty.db"
+ "filename" : "var/dirty.db"
},
/* An Example of MySQL Configuration
@@ -39,16 +39,35 @@
but makes it impossible to debug the javascript/css */
"minify" : true,
- /* How long may clients use served javascript code? Without versioning this
- is may cause problems during deployment. */
- "maxAge" : 21600000, // 6 hours
+ /* How long may clients use served javascript code (in seconds)? Without versioning this
+ may cause problems during deployment. Set to 0 to disable caching */
+ "maxAge" : 21600, // 60 * 60 * 6 = 6 hours
/* This is the path to the Abiword executable. Setting it to null, disables abiword.
Abiword is needed to enable the import/export of pads*/
"abiword" : null,
-
- /* This setting is used if you need http basic auth */
- // "httpAuth" : "user:pass",
+
+ /* This setting is used if you require authentication of all users.
+ Note: /admin always requires authentication. */
+ "requireAuthentication": false,
+
+ /* Require authorization by a module, or a user with is_admin set, see below. */
+ "requireAuthorization": false,
+
+ /* Users for basic authentication. is_admin = true gives access to /admin.
+ If you do not uncomment this, /admin will not be available! */
+ /*
+ "users": {
+ "admin": {
+ "password": "changeme1",
+ "is_admin": true
+ },
+ "user": {
+ "password": "changeme1",
+ "is_admin": false
+ }
+ },
+ */
/* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
"loglevel": "INFO"
diff --git a/settings.json.template_windows b/settings.json.template_windows
index 61f14dce..35b54d8d 100644
--- a/settings.json.template_windows
+++ b/settings.json.template_windows
@@ -12,7 +12,7 @@
"dbType" : "dirty",
//the database specific settings
"dbSettings" : {
- "filename" : "../var/dirty.db"
+ "filename" : "var/dirty.db"
},
/* An Example of MySQL Configuration
@@ -40,5 +40,9 @@
/* This is the path to the Abiword executable. Setting it to null, disables abiword.
Abiword is needed to enable the import/export of pads*/
- "abiword" : null
+ "abiword" : null,
+
+ /* cache 6 hours = 1000*60*60*6 */
+ "maxAge": 21600000
+
}
diff --git a/src/ep.json b/src/ep.json
new file mode 100644
index 00000000..6bc77735
--- /dev/null
+++ b/src/ep.json
@@ -0,0 +1,16 @@
+{
+ "parts": [
+ { "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } },
+ { "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } },
+ { "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } },
+ { "name": "padreadonly", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly:expressCreateServer" } },
+ { "name": "webaccess", "hooks": { "expressConfigure": "ep_etherpad-lite/node/hooks/express/webaccess:expressConfigure" } },
+ { "name": "apicalls", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls:expressCreateServer" } },
+ { "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } },
+ { "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } },
+ { "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } },
+ { "name": "adminplugins", "hooks": {
+ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",
+ "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } }
+ ]
+}
diff --git a/node/README.md b/src/node/README.md
similarity index 100%
rename from node/README.md
rename to src/node/README.md
diff --git a/node/db/API.js b/src/node/db/API.js
similarity index 99%
rename from node/db/API.js
rename to src/node/db/API.js
index 09cc95af..37fd3f16 100644
--- a/node/db/API.js
+++ b/src/node/db/API.js
@@ -431,7 +431,7 @@ exports.setPassword = function(padID, password, callback)
if(ERR(err, callback)) return;
//set the password
- pad.setPassword(password);
+ pad.setPassword(password == "" ? null : password);
callback();
});
diff --git a/node/db/AuthorManager.js b/src/node/db/AuthorManager.js
similarity index 97%
rename from node/db/AuthorManager.js
rename to src/node/db/AuthorManager.js
index 9baf6347..f644de12 100644
--- a/node/db/AuthorManager.js
+++ b/src/node/db/AuthorManager.js
@@ -18,11 +18,11 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
-var randomString = CommonCode.require('/pad_utils').randomString;
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
/**
* Checks if the author exists
diff --git a/node/db/DB.js b/src/node/db/DB.js
similarity index 100%
rename from node/db/DB.js
rename to src/node/db/DB.js
diff --git a/node/db/GroupManager.js b/src/node/db/GroupManager.js
similarity index 98%
rename from node/db/GroupManager.js
rename to src/node/db/GroupManager.js
index 04c79cfa..bd19507f 100644
--- a/node/db/GroupManager.js
+++ b/src/node/db/GroupManager.js
@@ -18,10 +18,10 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
-var randomString = CommonCode.require('/pad_utils').randomString;
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var padManager = require("./PadManager");
diff --git a/node/db/Pad.js b/src/node/db/Pad.js
similarity index 82%
rename from node/db/Pad.js
rename to src/node/db/Pad.js
index 40875eff..b4a39c17 100644
--- a/node/db/Pad.js
+++ b/src/node/db/Pad.js
@@ -2,11 +2,11 @@
* The pad object, defined with joose
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
-var Changeset = CommonCode.require("/Changeset");
-var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
-var randomString = CommonCode.require('/pad_utils').randomString;
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
+var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var settings = require('../utils/Settings');
@@ -15,6 +15,11 @@ var padManager = require("./PadManager");
var padMessageHandler = require("../handler/PadMessageHandler");
var readOnlyManager = require("./ReadOnlyManager");
var crypto = require("crypto");
+var randomString = require("../utils/randomstring");
+
+//serialization/deserialization attributes
+var attributeBlackList = ["id"];
+var jsonableList = ["pool"];
/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces
@@ -28,13 +33,13 @@ exports.cleanText = function (txt) {
var Pad = function Pad(id) {
this.atext = Changeset.makeAText("\n");
- this.pool = AttributePoolFactory.createAttributePool();
+ this.pool = new AttributePool();
this.head = -1;
this.chatHead = -1;
this.publicStatus = false;
this.passwordHash = null;
this.id = id;
-
+ this.savedRevisions = [];
};
exports.Pad = Pad;
@@ -75,15 +80,28 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
newRevData.meta.atext = this.atext;
}
- db.set("pad:"+this.id+":revs:"+newRev, newRevData);
- db.set("pad:"+this.id, {atext: this.atext,
- pool: this.pool.toJsonable(),
- head: this.head,
- chatHead: this.chatHead,
- publicStatus: this.publicStatus,
- passwordHash: this.passwordHash});
+ db.set("pad:"+this.id+":revs:"+newRev, newRevData);
+ this.saveToDatabase();
};
+//save all attributes to the database
+Pad.prototype.saveToDatabase = function saveToDatabase(){
+ var dbObject = {};
+
+ for(var attr in this){
+ if(typeof this[attr] === "function") continue;
+ if(attributeBlackList.indexOf(attr) !== -1) continue;
+
+ dbObject[attr] = this[attr];
+
+ if(jsonableList.indexOf(attr) !== -1){
+ dbObject[attr] = dbObject[attr].toJsonable();
+ }
+ }
+
+ db.set("pad:"+this.id, dbObject);
+}
+
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
};
@@ -200,11 +218,10 @@ Pad.prototype.setText = function setText(newText) {
};
Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) {
- this.chatHead++;
- //save the chat entry in the database
- db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
- //save the new chat head
- db.setSub("pad:"+this.id, ["chatHead"], this.chatHead);
+ this.chatHead++;
+ //save the chat entry in the database
+ db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
+ this.saveToDatabase();
};
Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) {
@@ -324,27 +341,14 @@ Pad.prototype.init = function init(text, callback) {
//if this pad exists, load it
if(value != null)
{
- _this.head = value.head;
- _this.atext = value.atext;
- _this.pool = _this.pool.fromJsonable(value.pool);
-
- //ensure we have a local chatHead variable
- if(value.chatHead != null)
- _this.chatHead = value.chatHead;
- else
- _this.chatHead = -1;
-
- //ensure we have a local publicStatus variable
- if(value.publicStatus != null)
- _this.publicStatus = value.publicStatus;
- else
- _this.publicStatus = false;
-
- //ensure we have a local passwordHash variable
- if(value.passwordHash != null)
- _this.passwordHash = value.passwordHash;
- else
- _this.passwordHash = null;
+ //copy all attr. To a transfrom via fromJsonable if necassary
+ for(var attr in value){
+ if(jsonableList.indexOf(attr) !== -1){
+ _this[attr] = _this[attr].fromJsonable(value[attr]);
+ } else {
+ _this[attr] = value[attr];
+ }
+ }
}
//this pad doesn't exist, so create it
else
@@ -452,12 +456,12 @@ Pad.prototype.remove = function remove(callback) {
//set in db
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
this.publicStatus = publicStatus;
- db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus);
+ this.saveToDatabase();
};
Pad.prototype.setPassword = function setPassword(password) {
this.passwordHash = password == null ? null : hash(password, generateSalt());
- db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash);
+ this.saveToDatabase();
};
Pad.prototype.isCorrectPassword = function isCorrectPassword(password) {
@@ -468,6 +472,31 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() {
return this.passwordHash != null;
};
+Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) {
+ //if this revision is already saved, return silently
+ for(var i in this.savedRevisions){
+ if(this.savedRevisions.revNum === revNum){
+ return;
+ }
+ }
+
+ //build the saved revision object
+ var savedRevision = {};
+ savedRevision.revNum = revNum;
+ savedRevision.savedById = savedById;
+ savedRevision.label = label || "Revision " + revNum;
+ savedRevision.timestamp = new Date().getTime();
+ savedRevision.id = randomString(10);
+
+ //save this new saved revision
+ this.savedRevisions.push(savedRevision);
+ this.saveToDatabase();
+};
+
+Pad.prototype.getSavedRevisions = function getSavedRevisions() {
+ return this.savedRevisions;
+};
+
/* Crypto helper methods */
function hash(password, salt)
diff --git a/node/db/PadManager.js b/src/node/db/PadManager.js
similarity index 100%
rename from node/db/PadManager.js
rename to src/node/db/PadManager.js
diff --git a/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js
similarity index 94%
rename from node/db/ReadOnlyManager.js
rename to src/node/db/ReadOnlyManager.js
index e5dab99b..34340630 100644
--- a/node/db/ReadOnlyManager.js
+++ b/src/node/db/ReadOnlyManager.js
@@ -18,11 +18,11 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
-var randomString = CommonCode.require('/pad_utils').randomString;
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
/**
* returns a read only id for a pad
diff --git a/node/db/SecurityManager.js b/src/node/db/SecurityManager.js
similarity index 98%
rename from node/db/SecurityManager.js
rename to src/node/db/SecurityManager.js
index 33ab37d4..a092453a 100644
--- a/node/db/SecurityManager.js
+++ b/src/node/db/SecurityManager.js
@@ -18,7 +18,7 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
@@ -26,7 +26,7 @@ var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
var settings = require("../utils/Settings")
-var randomString = CommonCode.require('/pad_utils').randomString;
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
/**
* This function controlls the access to a pad, it checks if the user can access a pad.
diff --git a/node/db/SessionManager.js b/src/node/db/SessionManager.js
similarity index 98%
rename from node/db/SessionManager.js
rename to src/node/db/SessionManager.js
index c5af33c6..ec4948a6 100644
--- a/node/db/SessionManager.js
+++ b/src/node/db/SessionManager.js
@@ -18,10 +18,10 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
-var randomString = CommonCode.require('/pad_utils').randomString;
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var groupMangager = require("./GroupManager");
diff --git a/node/easysync_tests.js b/src/node/easysync_tests.js
similarity index 97%
rename from node/easysync_tests.js
rename to src/node/easysync_tests.js
index 8e7398be..374e949f 100644
--- a/node/easysync_tests.js
+++ b/src/node/easysync_tests.js
@@ -20,9 +20,9 @@
* limitations under the License.
*/
-var CommonCode = require('./utils/common_code');
-var Changeset = CommonCode.require("/Changeset");
-var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
+
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
+var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
function random() {
this.nextInt = function (maxValue) {
@@ -227,7 +227,7 @@ function runTests() {
return attribs; // it's already an attrib pool
} else {
// assume it's an array of attrib strings to be split and added
- var p = AttributePoolFactory.createAttributePool();
+ var p = new AttributePool();
attribs.forEach(function (kv) {
p.putAttrib(kv.split(','));
});
@@ -325,7 +325,7 @@ function runTests() {
runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]);
var testPoolWithChars = (function () {
- var p = AttributePoolFactory.createAttributePool();
+ var p = new AttributePool();
p.putAttrib(['char', 'newline']);
for (var i = 1; i < 36; i++) {
p.putAttrib(['char', Changeset.numToString(i)]);
@@ -560,7 +560,7 @@ function runTests() {
var rand = new random();
print("> testCompose#" + randomSeed);
- var p = AttributePoolFactory.createAttributePool();
+ var p = new AttributePool();
var startText = randomMultiline(10, 20, rand) + '\n';
@@ -594,7 +594,7 @@ function runTests() {
(function simpleComposeAttributesTest() {
print("> simpleComposeAttributesTest");
- var p = AttributePoolFactory.createAttributePool();
+ var p = new AttributePool();
p.putAttrib(['bold', '']);
p.putAttrib(['bold', 'true']);
var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x");
@@ -604,7 +604,7 @@ function runTests() {
})();
(function followAttributesTest() {
- var p = AttributePoolFactory.createAttributePool();
+ var p = new AttributePool();
p.putAttrib(['x', '']);
p.putAttrib(['x', 'abc']);
p.putAttrib(['x', 'def']);
@@ -633,7 +633,7 @@ function runTests() {
var rand = new random();
print("> testFollow#" + randomSeed);
- var p = AttributePoolFactory.createAttributePool();
+ var p = new AttributePool();
var startText = randomMultiline(10, 20, rand) + '\n';
@@ -682,8 +682,8 @@ function runTests() {
(function testMoveOpsToNewPool() {
print("> testMoveOpsToNewPool");
- var pool1 = AttributePoolFactory.createAttributePool();
- var pool2 = AttributePoolFactory.createAttributePool();
+ var pool1 = new AttributePool();
+ var pool2 = new AttributePool();
pool1.putAttrib(['baz', 'qux']);
pool1.putAttrib(['foo', 'bar']);
@@ -738,7 +738,7 @@ function runTests() {
(function testOpAttributeValue() {
print("> testOpAttributeValue");
- var p = AttributePoolFactory.createAttributePool();
+ var p = new AttributePool();
p.putAttrib(['name', 'david']);
p.putAttrib(['color', 'green']);
diff --git a/src/node/eejs/examples/bar.ejs b/src/node/eejs/examples/bar.ejs
new file mode 100644
index 00000000..6a2cc4ba
--- /dev/null
+++ b/src/node/eejs/examples/bar.ejs
@@ -0,0 +1,9 @@
+a
+<% e.begin_block("bar"); %>
+ A
+ <% e.begin_block("foo"); %>
+ XX
+ <% e.end_block(); %>
+ B
+<% e.end_block(); %>
+b
diff --git a/src/node/eejs/examples/foo.ejs b/src/node/eejs/examples/foo.ejs
new file mode 100644
index 00000000..daee5f8e
--- /dev/null
+++ b/src/node/eejs/examples/foo.ejs
@@ -0,0 +1,7 @@
+<% e.inherit("./bar.ejs"); %>
+
+<% e.begin_define_block("foo"); %>
+ YY
+ <% e.super(); %>
+ ZZ
+<% e.end_define_block(); %>
diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js
new file mode 100644
index 00000000..2d02a45a
--- /dev/null
+++ b/src/node/eejs/index.js
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2011 RedHog (Egil Möller)
+ *
+ * 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.
+ */
+
+/* Basic usage:
+ *
+ * require("./index").require("./examples/foo.ejs")
+ */
+
+var ejs = require("ejs");
+var fs = require("fs");
+var path = require("path");
+var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
+var resolve = require("resolve");
+
+exports.info = {
+ buf_stack: [],
+ block_stack: [],
+ blocks: {},
+ file_stack: [],
+};
+
+exports._init = function (b, recursive) {
+ exports.info.buf_stack.push(exports.info.buf);
+ exports.info.buf = b;
+}
+
+exports._exit = function (b, recursive) {
+ exports.info.file_stack[exports.info.file_stack.length-1].inherit.forEach(function (item) {
+ exports._require(item.name, item.args);
+ });
+ exports.info.buf = exports.info.buf_stack.pop();
+}
+
+exports.begin_capture = function() {
+ exports.info.buf_stack.push(exports.info.buf.concat());
+ exports.info.buf.splice(0, exports.info.buf.length);
+}
+
+exports.end_capture = function () {
+ var res = exports.info.buf.join("");
+ exports.info.buf.splice.apply(
+ exports.info.buf,
+ [0, exports.info.buf.length].concat(exports.info.buf_stack.pop()));
+ return res;
+}
+
+exports.begin_define_block = function (name) {
+ if (typeof exports.info.blocks[name] == "undefined")
+ exports.info.blocks[name] = {};
+ exports.info.block_stack.push(name);
+ exports.begin_capture();
+}
+
+exports.super = function () {
+ exports.info.buf.push('');
+}
+
+exports.end_define_block = function () {
+ content = exports.end_capture();
+ var name = exports.info.block_stack.pop();
+ if (typeof exports.info.blocks[name].content == "undefined")
+ exports.info.blocks[name].content = content;
+ else if (typeof exports.info.blocks[name].content.indexOf(''))
+ exports.info.blocks[name].content = exports.info.blocks[name].content.replace('', content);
+
+ return exports.info.blocks[name].content;
+}
+
+exports.end_block = function () {
+ var name = exports.info.block_stack[exports.info.block_stack.length-1];
+ var args = {content: exports.end_define_block()};
+ hooks.callAll("eejsBlock_" + name, args);
+ exports.info.buf.push(args.content);
+}
+
+exports.begin_block = exports.begin_define_block;
+
+exports.inherit = function (name, args) {
+ exports.info.file_stack[exports.info.file_stack.length-1].inherit.push({name:name, args:args});
+}
+
+exports.require = function (name, args, mod) {
+ if (args == undefined) args = {};
+
+ var basedir = __dirname;
+ var paths = [];
+
+ if (exports.info.file_stack.length) {
+ basedir = path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path);
+ }
+ if (mod) {
+ basedir = path.dirname(mod.filename);
+ paths = mod.paths;
+ }
+
+ var ejspath = resolve.sync(
+ name,
+ {
+ paths : paths,
+ basedir : basedir,
+ extensions : [ '.html', '.ejs' ],
+ }
+ )
+
+ args.e = exports;
+ args.require = require;
+ var template = '<% e._init(buf); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>';
+
+ exports.info.file_stack.push({path: ejspath, inherit: []});
+ var res = ejs.render(template, args);
+ exports.info.file_stack.pop();
+
+ return res;
+}
+
+exports._require = function (name, args) {
+ exports.info.buf.push(exports.require(name, args));
+}
diff --git a/node/handler/APIHandler.js b/src/node/handler/APIHandler.js
similarity index 95%
rename from node/handler/APIHandler.js
rename to src/node/handler/APIHandler.js
index a7f66151..98b1ed16 100644
--- a/node/handler/APIHandler.js
+++ b/src/node/handler/APIHandler.js
@@ -18,23 +18,23 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var fs = require("fs");
var api = require("../db/API");
var padManager = require("../db/PadManager");
-var randomString = CommonCode.require('/pad_utils').randomString;
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
//ensure we have an apikey
var apikey = null;
try
{
- apikey = fs.readFileSync("../APIKEY.txt","utf8");
+ apikey = fs.readFileSync("./APIKEY.txt","utf8");
}
catch(e)
{
apikey = randomString(32);
- fs.writeFileSync("../APIKEY.txt",apikey,"utf8");
+ fs.writeFileSync("./APIKEY.txt",apikey,"utf8");
}
//a list of all functions
diff --git a/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js
similarity index 100%
rename from node/handler/ExportHandler.js
rename to src/node/handler/ExportHandler.js
diff --git a/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js
similarity index 93%
rename from node/handler/ImportHandler.js
rename to src/node/handler/ImportHandler.js
index ed5eb05e..788706ce 100644
--- a/node/handler/ImportHandler.js
+++ b/src/node/handler/ImportHandler.js
@@ -196,6 +196,6 @@ exports.doImport = function(req, res, padId)
ERR(err);
//close the connection
- res.send("", 200);
+ res.send("", 200);
});
}
diff --git a/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
similarity index 94%
rename from node/handler/PadMessageHandler.js
rename to src/node/handler/PadMessageHandler.js
index 135b4b63..3f6cfa56 100644
--- a/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -18,18 +18,21 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var async = require("async");
var padManager = require("../db/PadManager");
-var Changeset = CommonCode.require("/Changeset");
-var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
+var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
+var AttributeManager = require("ep_etherpad-lite/static/js/AttributeManager");
var authorManager = require("../db/AuthorManager");
var readOnlyManager = require("../db/ReadOnlyManager");
var settings = require('../utils/Settings');
var securityManager = require("../db/SecurityManager");
+var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins.js");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
+var _ = require('underscore');
/**
* A associative array that translates a session to a pad
@@ -127,7 +130,11 @@ exports.handleDisconnect = function(client)
//Go trough all user that are still on the pad, and send them the USER_LEAVE message
for(i in pad2sessions[sessionPad])
{
- socketio.sockets.sockets[pad2sessions[sessionPad][i]].json.send(messageToTheOtherUsers);
+ var socket = socketio.sockets.sockets[pad2sessions[sessionPad][i]];
+ if(socket !== undefined){
+ socket.json.send(messageToTheOtherUsers);
+ }
+
}
});
}
@@ -197,6 +204,23 @@ exports.handleMessage = function(client, message)
}
}
+/**
+ * Handles a save revision message
+ * @param client the client that send this message
+ * @param message the message from the client
+ */
+function handleSaveRevisionMessage(client, message){
+ var padId = session2pad[client.id];
+ var userId = sessioninfos[client.id].author;
+
+ padManager.getPad(padId, function(err, pad)
+ {
+ if(ERR(err)) return;
+
+ pad.addSavedRevision(pad.head, userId);
+ });
+}
+
/**
* Handles a Chat Message
* @param client the client that send this message
@@ -366,7 +390,7 @@ function handleUserChanges(client, message)
//get all Vars we need
var baseRev = message.data.baseRev;
- var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool);
+ var wireApool = (new AttributePool()).fromJsonable(message.data.apool);
var changeset = message.data.changeset;
var r, apool, pad;
@@ -563,8 +587,12 @@ function _correctMarkersInPad(atext, apool) {
var offset = 0;
while (iter.hasNext()) {
var op = iter.next();
- var listValue = Changeset.opAttributeValue(op, 'list', apool);
- if (listValue) {
+
+ var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute){
+ return Changeset.opAttributeValue(op, attribute, apool);
+ }) !== undefined;
+
+ if (hasMarker) {
for(var i=0;i 0 && text.charAt(offset-1) != '\n') {
badMarkers.push(offset);
@@ -736,9 +764,10 @@ function handleClientReady(client, message)
{
for(var i in pad2sessions[message.padId])
{
- if(sessioninfos[pad2sessions[message.padId][i]].author == author)
+ if(sessioninfos[pad2sessions[message.padId][i]] && sessioninfos[pad2sessions[message.padId][i]].author == author)
{
- socketio.sockets.sockets[pad2sessions[message.padId][i]].json.send({disconnect:"userdup"});
+ var socket = socketio.sockets.sockets[pad2sessions[message.padId][i]];
+ if(socket) socket.json.send({disconnect:"userdup"});
}
}
}
@@ -799,9 +828,12 @@ function handleClientReady(client, message)
"hideSidebar": false
},
"abiwordAvailable": settings.abiwordAvailable(),
- "hooks": {}
+ "plugins": {
+ "plugins": plugins.plugins,
+ "parts": plugins.parts,
+ }
}
-
+
//Add a username to the clientVars if one avaiable
if(authorName != null)
{
diff --git a/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js
similarity index 100%
rename from node/handler/SocketIORouter.js
rename to src/node/handler/SocketIORouter.js
diff --git a/node/handler/TimesliderMessageHandler.js b/src/node/handler/TimesliderMessageHandler.js
similarity index 97%
rename from node/handler/TimesliderMessageHandler.js
rename to src/node/handler/TimesliderMessageHandler.js
index 18806843..5556efa1 100644
--- a/node/handler/TimesliderMessageHandler.js
+++ b/src/node/handler/TimesliderMessageHandler.js
@@ -18,12 +18,12 @@
* limitations under the License.
*/
-var CommonCode = require('../utils/common_code');
+
var ERR = require("async-stacktrace");
var async = require("async");
var padManager = require("../db/PadManager");
-var Changeset = CommonCode.require("/Changeset");
-var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
+var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
var settings = require('../utils/Settings');
var authorManager = require("../db/AuthorManager");
var log4js = require('log4js');
@@ -155,8 +155,6 @@ function createTimesliderClientVars (padId, callback)
var clientVars = {
viewId: padId,
colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"],
- sliderEnabled : true,
- supportsSlider: true,
savedRevisions: [],
padIdForUrl: padId,
fullWidth: false,
@@ -166,6 +164,7 @@ function createTimesliderClientVars (padId, callback)
hooks: [],
initialStyledContents: {}
};
+
var pad;
var initialChangesets = [];
@@ -180,6 +179,12 @@ function createTimesliderClientVars (padId, callback)
callback();
});
},
+ //get all saved revisions and add them
+ function(callback)
+ {
+ clientVars.savedRevisions = pad.getSavedRevisions();
+ callback();
+ },
//get all authors and add them to
function(callback)
{
@@ -265,7 +270,7 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback)
var forwardsChangesets = [];
var backwardsChangesets = [];
var timeDeltas = [];
- var apool = AttributePoolFactory.createAttributePool();
+ var apool = new AttributePool();
var pad;
var composedChangesets = {};
var revisionDate = [];
diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js
new file mode 100644
index 00000000..7b21206c
--- /dev/null
+++ b/src/node/hooks/express/adminplugins.js
@@ -0,0 +1,53 @@
+var path = require('path');
+var eejs = require('ep_etherpad-lite/node/eejs');
+var installer = require('ep_etherpad-lite/static/js/pluginfw/installer');
+var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ args.app.get('/admin/plugins', function(req, res) {
+ var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
+ var render_args = {
+ plugins: plugins.plugins,
+ search_results: {},
+ errors: [],
+ };
+
+ res.send(eejs.require(
+ "ep_etherpad-lite/templates/admin/plugins.html",
+ render_args), {});
+ });
+}
+
+exports.socketio = function (hook_name, args, cb) {
+ var io = args.io.of("/pluginfw/installer");
+ io.on('connection', function (socket) {
+ if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return;
+
+ socket.on("load", function (query) {
+ socket.emit("installed-results", {results: plugins.plugins});
+ });
+
+ socket.on("search", function (query) {
+ socket.emit("progress", {progress:0, message:'Fetching results...'});
+ installer.search(query, true, function (progress) {
+ if (progress.results)
+ socket.emit("search-result", progress);
+ socket.emit("progress", progress);
+ });
+ });
+
+ socket.on("install", function (plugin_name) {
+ socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."});
+ installer.install(plugin_name, function (progress) {
+ socket.emit("progress", progress);
+ });
+ });
+
+ socket.on("uninstall", function (plugin_name) {
+ socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."});
+ installer.uninstall(plugin_name, function (progress) {
+ socket.emit("progress", progress);
+ });
+ });
+ });
+}
diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js
new file mode 100644
index 00000000..48d50722
--- /dev/null
+++ b/src/node/hooks/express/apicalls.js
@@ -0,0 +1,60 @@
+var log4js = require('log4js');
+var apiLogger = log4js.getLogger("API");
+var formidable = require('formidable');
+var apiHandler = require('../../handler/APIHandler');
+
+//This is for making an api call, collecting all post information and passing it to the apiHandler
+var apiCaller = function(req, res, fields) {
+ res.header("Content-Type", "application/json; charset=utf-8");
+
+ apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
+
+ //wrap the send function so we can log the response
+ //note: res._send seems to be already in use, so better use a "unique" name
+ res._____send = res.send;
+ res.send = function (response) {
+ response = JSON.stringify(response);
+ apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
+
+ //is this a jsonp call, if yes, add the function call
+ if(req.query.jsonp)
+ response = req.query.jsonp + "(" + response + ")";
+
+ res._____send(response);
+ }
+
+ //call the api handler
+ apiHandler.handle(req.params.func, fields, req, res);
+}
+
+exports.apiCaller = apiCaller;
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ //This is a api GET call, collect all post informations and pass it to the apiHandler
+ args.app.get('/api/1/:func', function (req, res) {
+ apiCaller(req, res, req.query)
+ });
+
+ //This is a api POST call, collect all post informations and pass it to the apiHandler
+ args.app.post('/api/1/:func', function(req, res) {
+ new formidable.IncomingForm().parse(req, function (err, fields, files) {
+ apiCaller(req, res, fields)
+ });
+ });
+
+ //The Etherpad client side sends information about how a disconnect happen
+ args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) {
+ new formidable.IncomingForm().parse(req, function(err, fields, files) {
+ console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
+ res.end("OK");
+ });
+ });
+
+ //The Etherpad client side sends information about client side javscript errors
+ args.app.post('/jserror', function(req, res) {
+ new formidable.IncomingForm().parse(req, function(err, fields, files) {
+ console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo);
+ res.end("OK");
+ });
+ });
+}
diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js
new file mode 100644
index 00000000..cb8c5898
--- /dev/null
+++ b/src/node/hooks/express/errorhandling.js
@@ -0,0 +1,52 @@
+var os = require("os");
+var db = require('../../db/DB');
+
+
+exports.onShutdown = false;
+exports.gracefulShutdown = function(err) {
+ if(err && err.stack) {
+ console.error(err.stack);
+ } else if(err) {
+ console.error(err);
+ }
+
+ //ensure there is only one graceful shutdown running
+ if(exports.onShutdown) return;
+ exports.onShutdown = true;
+
+ console.log("graceful shutdown...");
+
+ //stop the http server
+ exports.app.close();
+
+ //do the db shutdown
+ db.db.doShutdown(function() {
+ console.log("db sucessfully closed.");
+
+ process.exit(0);
+ });
+
+ setTimeout(function(){
+ process.exit(1);
+ }, 3000);
+}
+
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ exports.app = args.app;
+
+ args.app.error(function(err, req, res, next){
+ res.send(500);
+ console.error(err.stack ? err.stack : err.toString());
+ exports.gracefulShutdown();
+ });
+
+ //connect graceful shutdown with sigint and uncaughtexception
+ if(os.type().indexOf("Windows") == -1) {
+ //sigint is so far not working on windows
+ //https://github.com/joyent/node/issues/1553
+ process.on('SIGINT', exports.gracefulShutdown);
+ }
+
+ process.on('uncaughtException', exports.gracefulShutdown);
+}
diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js
new file mode 100644
index 00000000..9e78f34d
--- /dev/null
+++ b/src/node/hooks/express/importexport.js
@@ -0,0 +1,41 @@
+var hasPadAccess = require("../../padaccess");
+var settings = require('../../utils/Settings');
+var exportHandler = require('../../handler/ExportHandler');
+var importHandler = require('../../handler/ImportHandler');
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) {
+ var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"];
+ //send a 404 if we don't support this filetype
+ if (types.indexOf(req.params.type) == -1) {
+ next();
+ return;
+ }
+
+ //if abiword is disabled, and this is a format we only support with abiword, output a message
+ if (settings.abiword == null &&
+ ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
+ res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
+ return;
+ }
+
+ res.header("Access-Control-Allow-Origin", "*");
+
+ hasPadAccess(req, res, function() {
+ exportHandler.doExport(req, res, req.params.pad, req.params.type);
+ });
+ });
+
+ //handle import requests
+ args.app.post('/p/:pad/import', function(req, res, next) {
+ //if abiword is disabled, skip handling this request
+ if(settings.abiword == null) {
+ next();
+ return;
+ }
+
+ hasPadAccess(req, res, function() {
+ importHandler.doImport(req, res, req.params.pad);
+ });
+ });
+}
diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js
new file mode 100644
index 00000000..60ece0ad
--- /dev/null
+++ b/src/node/hooks/express/padreadonly.js
@@ -0,0 +1,65 @@
+var async = require('async');
+var ERR = require("async-stacktrace");
+var readOnlyManager = require("../../db/ReadOnlyManager");
+var hasPadAccess = require("../../padaccess");
+var exporthtml = require("../../utils/ExportHtml");
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ //serve read only pad
+ args.app.get('/ro/:id', function(req, res)
+ {
+ var html;
+ var padId;
+ var pad;
+
+ async.series([
+ //translate the read only pad to a padId
+ function(callback)
+ {
+ readOnlyManager.getPadId(req.params.id, function(err, _padId)
+ {
+ if(ERR(err, callback)) return;
+
+ padId = _padId;
+
+ //we need that to tell hasPadAcess about the pad
+ req.params.pad = padId;
+
+ callback();
+ });
+ },
+ //render the html document
+ function(callback)
+ {
+ //return if the there is no padId
+ if(padId == null)
+ {
+ callback("notfound");
+ return;
+ }
+
+ hasPadAccess(req, res, function()
+ {
+ //render the html document
+ exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
+ {
+ if(ERR(err, callback)) return;
+ html = _html;
+ callback();
+ });
+ });
+ }
+ ], function(err)
+ {
+ //throw any unexpected error
+ if(err && err != "notfound")
+ ERR(err);
+
+ if(err == "notfound")
+ res.send('404 - Not Found', 404);
+ else
+ res.send(html);
+ });
+ });
+
+}
\ No newline at end of file
diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js
new file mode 100644
index 00000000..4f5dd7a5
--- /dev/null
+++ b/src/node/hooks/express/padurlsanitize.js
@@ -0,0 +1,29 @@
+var padManager = require('../../db/PadManager');
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ //redirects browser to the pad's sanitized url if needed. otherwise, renders the html
+ args.app.param('pad', function (req, res, next, padId) {
+ //ensure the padname is valid and the url doesn't end with a /
+ if(!padManager.isValidPadId(padId) || /\/$/.test(req.url))
+ {
+ res.send('Such a padname is forbidden', 404);
+ }
+ else
+ {
+ padManager.sanitizePadId(padId, function(sanitizedPadId) {
+ //the pad id was sanitized, so we redirect to the sanitized version
+ if(sanitizedPadId != padId)
+ {
+ var real_path = req.path.replace(/^\/p\/[^\/]+/, '/p/' + sanitizedPadId);
+ res.header('Location', real_path);
+ res.send('You should be redirected to ' + real_path + '', 302);
+ }
+ //the pad id was fine, so just render it
+ else
+ {
+ next();
+ }
+ });
+ }
+ });
+}
diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js
new file mode 100644
index 00000000..6774b653
--- /dev/null
+++ b/src/node/hooks/express/socketio.js
@@ -0,0 +1,65 @@
+var log4js = require('log4js');
+var socketio = require('socket.io');
+var settings = require('../../utils/Settings');
+var socketIORouter = require("../../handler/SocketIORouter");
+var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
+
+var padMessageHandler = require("../../handler/PadMessageHandler");
+var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler");
+
+var connect = require('connect');
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ //init socket.io and redirect all requests to the MessageHandler
+ var io = socketio.listen(args.app);
+
+ /* Require an express session cookie to be present, and load the
+ * session. See http://www.danielbaulig.de/socket-ioexpress for more
+ * info */
+ io.set('authorization', function (data, accept) {
+ if (!data.headers.cookie) return accept('No session cookie transmitted.', false);
+ data.cookie = connect.utils.parseCookie(data.headers.cookie);
+ data.sessionID = data.cookie.express_sid;
+ args.app.sessionStore.get(data.sessionID, function (err, session) {
+ if (err || !session) return accept('Bad session / session has expired', false);
+ data.session = new connect.middleware.session.Session(data, session);
+ accept(null, true);
+ });
+ });
+
+
+ //this is only a workaround to ensure it works with all browers behind a proxy
+ //we should remove this when the new socket.io version is more stable
+ io.set('transports', ['xhr-polling']);
+
+ var socketIOLogger = log4js.getLogger("socket.io");
+ io.set('logger', {
+ debug: function (str)
+ {
+ socketIOLogger.debug.apply(socketIOLogger, arguments);
+ },
+ info: function (str)
+ {
+ socketIOLogger.info.apply(socketIOLogger, arguments);
+ },
+ warn: function (str)
+ {
+ socketIOLogger.warn.apply(socketIOLogger, arguments);
+ },
+ error: function (str)
+ {
+ socketIOLogger.error.apply(socketIOLogger, arguments);
+ },
+ });
+
+ //minify socket.io javascript
+ if(settings.minify)
+ io.enable('browser client minification');
+
+ //Initalize the Socket.IO Router
+ socketIORouter.setSocketIO(io);
+ socketIORouter.addComponent("pad", padMessageHandler);
+ socketIORouter.addComponent("timeslider", timesliderMessageHandler);
+
+ hooks.callAll("socketio", {"app": args.app, "io": io});
+}
diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js
new file mode 100644
index 00000000..474f475e
--- /dev/null
+++ b/src/node/hooks/express/specialpages.js
@@ -0,0 +1,46 @@
+var path = require('path');
+var eejs = require('ep_etherpad-lite/node/eejs');
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+
+ //serve index.html under /
+ args.app.get('/', function(req, res)
+ {
+ res.send(eejs.require("ep_etherpad-lite/templates/index.html"));
+ });
+
+ //serve robots.txt
+ args.app.get('/robots.txt', function(req, res)
+ {
+ var filePath = path.normalize(__dirname + "/../../../static/robots.txt");
+ res.sendfile(filePath);
+ });
+
+ //serve favicon.ico
+ args.app.get('/favicon.ico', function(req, res)
+ {
+ var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico");
+ res.sendfile(filePath, function(err)
+ {
+ //there is no custom favicon, send the default favicon
+ if(err)
+ {
+ filePath = path.normalize(__dirname + "/../../../static/favicon.ico");
+ res.sendfile(filePath);
+ }
+ });
+ });
+
+ //serve pad.html under /p
+ args.app.get('/p/:pad', function(req, res, next)
+ {
+ res.send(eejs.require("ep_etherpad-lite/templates/pad.html"));
+ });
+
+ //serve timeslider.html under /p/$padname/timeslider
+ args.app.get('/p/:pad/timeslider', function(req, res, next)
+ {
+ res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html"));
+ });
+
+}
\ No newline at end of file
diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js
new file mode 100644
index 00000000..f284e478
--- /dev/null
+++ b/src/node/hooks/express/static.js
@@ -0,0 +1,57 @@
+var path = require('path');
+var minify = require('../../utils/Minify');
+var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
+var CachingMiddleware = require('../../utils/caching_middleware');
+var settings = require("../../utils/Settings");
+var Yajsml = require('yajsml');
+var fs = require("fs");
+var ERR = require("async-stacktrace");
+var _ = require("underscore");
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ // Cache both minified and static.
+ var assetCache = new CachingMiddleware;
+ args.app.all('/(javascripts|static)/*', assetCache.handle);
+
+ // Minify will serve static files compressed (minify enabled). It also has
+ // file-specific hacks for ace/require-kernel/etc.
+ args.app.all('/static/:filename(*)', minify.minify);
+
+ // Setup middleware that will package JavaScript files served by minify for
+ // CommonJS loader on the client-side.
+ var jsServer = new (Yajsml.Server)({
+ rootPath: 'javascripts/src/'
+ , rootURI: 'http://localhost:' + settings.port + '/static/js/'
+ , libraryPath: 'javascripts/lib/'
+ , libraryURI: 'http://localhost:' + settings.port + '/static/plugins/'
+ });
+
+ var StaticAssociator = Yajsml.associators.StaticAssociator;
+ var associations =
+ Yajsml.associators.associationsForSimpleMapping(minify.tar);
+ var associator = new StaticAssociator(associations);
+ jsServer.setAssociator(associator);
+ args.app.use(jsServer);
+
+ // serve plugin definitions
+ // not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js");
+ args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) {
+
+ var clientParts = _(plugins.parts)
+ .filter(function(part){ return _(part).has('client_hooks') });
+
+ var clientPlugins = {};
+
+ _(clientParts).chain()
+ .map(function(part){ return part.plugin })
+ .uniq()
+ .each(function(name){
+ clientPlugins[name] = _(plugins.plugins[name]).clone();
+ delete clientPlugins[name]['package'];
+ });
+
+ res.header("Content-Type","application/json; charset=utf-8");
+ res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts}));
+ res.end();
+ });
+}
diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js
new file mode 100644
index 00000000..028d8ab1
--- /dev/null
+++ b/src/node/hooks/express/webaccess.js
@@ -0,0 +1,109 @@
+var express = require('express');
+var log4js = require('log4js');
+var httpLogger = log4js.getLogger("http");
+var settings = require('../../utils/Settings');
+var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
+
+
+//checks for basic http auth
+exports.basicAuth = function (req, res, next) {
+ var hookResultMangle = function (cb) {
+ return function (err, data) {
+ return cb(!err && data.length && data[0]);
+ }
+ }
+
+ var authorize = function (cb) {
+ // Do not require auth for static paths...this could be a bit brittle
+ if (req.path.match(/^\/(static|javascripts|pluginfw)/)) return cb(true);
+
+ if (req.path.indexOf('/admin') != 0) {
+ if (!settings.requireAuthentication) return cb(true);
+ if (!settings.requireAuthorization && req.session && req.session.user) return cb(true);
+ }
+
+ if (req.session && req.session.user && req.session.user.is_admin) return cb(true);
+
+ hooks.aCallFirst("authorize", {req: req, res:res, next:next, resource: req.path}, hookResultMangle(cb));
+ }
+
+ var authenticate = function (cb) {
+ // If auth headers are present use them to authenticate...
+ if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) {
+ var userpass = new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString().split(":")
+ var username = userpass[0];
+ var password = userpass[1];
+
+ if (settings.users[username] != undefined && settings.users[username].password == password) {
+ settings.users[username].username = username;
+ req.session.user = settings.users[username];
+ return cb(true);
+ }
+ return hooks.aCallFirst("authenticate", {req: req, res:res, next:next, username: username, password: password}, hookResultMangle(cb));
+ }
+ hooks.aCallFirst("authenticate", {req: req, res:res, next:next}, hookResultMangle(cb));
+ }
+
+
+ /* Authentication OR authorization failed. */
+ var failure = function () {
+ return hooks.aCallFirst("authFailure", {req: req, res:res, next:next}, hookResultMangle(function (ok) {
+ if (ok) return;
+ /* No plugin handler for invalid auth. Return Auth required
+ * Headers, delayed for 1 second, if authentication failed
+ * before. */
+ res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
+ if (req.headers.authorization) {
+ setTimeout(function () {
+ res.send('Authentication required', 401);
+ }, 1000);
+ } else {
+ res.send('Authentication required', 401);
+ }
+ }));
+ }
+
+
+ /* This is the actual authentication/authorization hoop. It is done in four steps:
+
+ 1) Try to just access the thing
+ 2) If not allowed using whatever creds are in the current session already, try to authenticate
+ 3) If authentication using already supplied credentials succeeds, try to access the thing again
+ 4) If all els fails, give the user a 401 to request new credentials
+
+ Note that the process could stop already in step 3 with a redirect to login page.
+
+ */
+
+ authorize(function (ok) {
+ if (ok) return next();
+ authenticate(function (ok) {
+ if (!ok) return failure();
+ authorize(function (ok) {
+ if (ok) return next();
+ failure();
+ });
+ });
+ });
+}
+
+exports.expressConfigure = function (hook_name, args, cb) {
+ // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158.
+ // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway.
+ if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR"))
+ args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
+ args.app.use(express.cookieParser());
+
+ /* Do not let express create the session, so that we can retain a
+ * reference to it for socket.io to use. Also, set the key (cookie
+ * name) to a javascript identifier compatible string. Makes code
+ * handling it cleaner :) */
+
+ args.app.sessionStore = new express.session.MemoryStore();
+ args.app.use(express.session({store: args.app.sessionStore,
+ key: 'express_sid',
+ secret: apikey = randomString(32)}));
+
+ args.app.use(exports.basicAuth);
+}
diff --git a/src/node/padaccess.js b/src/node/padaccess.js
new file mode 100644
index 00000000..a3d1df33
--- /dev/null
+++ b/src/node/padaccess.js
@@ -0,0 +1,21 @@
+var ERR = require("async-stacktrace");
+var securityManager = require('./db/SecurityManager');
+
+//checks for padAccess
+module.exports = function (req, res, callback) {
+
+ // FIXME: Why is this ever undefined??
+ if (req.cookies === undefined) req.cookies = {};
+
+ securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj) {
+ if(ERR(err, callback)) return;
+
+ //there is access, continue
+ if(accessObj.accessStatus == "grant") {
+ callback();
+ //no access
+ } else {
+ res.send("403 - Can't touch this", 403);
+ }
+ });
+}
diff --git a/src/node/server.js b/src/node/server.js
new file mode 100755
index 00000000..4eb38ea7
--- /dev/null
+++ b/src/node/server.js
@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+/**
+ * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server.
+ * Static file Requests are answered directly from this module, Socket.IO messages are passed
+ * to MessageHandler and minfied requests are passed to minified.
+ */
+
+/*
+ * 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var log4js = require('log4js');
+var fs = require('fs');
+var settings = require('./utils/Settings');
+var db = require('./db/DB');
+var async = require('async');
+var express = require('express');
+var path = require('path');
+var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
+var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
+var npm = require("npm/lib/npm.js");
+var _ = require("underscore");
+
+//try to get the git version
+var version = "";
+try
+{
+ var rootPath = path.resolve(npm.dir, '..');
+ var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8");
+ var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n"));
+ version = fs.readFileSync(refPath, "utf-8");
+ version = version.substring(0, 7);
+ console.log("Your Etherpad Lite git version is " + version);
+}
+catch(e)
+{
+ console.warn("Can't get git version for server header\n" + e.message)
+}
+
+console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues")
+
+var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)";
+
+//set loglevel
+log4js.setGlobalLogLevel(settings.loglevel);
+
+async.waterfall([
+ //initalize the database
+ function (callback)
+ {
+ db.init(callback);
+ },
+
+ plugins.update,
+
+ function (callback) {
+ console.info("Installed plugins: " + plugins.formatPlugins());
+ console.debug("Installed parts:\n" + plugins.formatParts());
+ console.debug("Installed hooks:\n" + plugins.formatHooks());
+ callback();
+ },
+
+ //initalize the http server
+ function (callback)
+ {
+ //create server
+ var app = express.createServer();
+
+ app.use(function (req, res, next) {
+ res.header("Server", serverName);
+ next();
+ });
+
+ app.configure(function() { hooks.callAll("expressConfigure", {"app": app}); });
+
+ hooks.callAll("expressCreateServer", {"app": app});
+
+ //let the server listen
+ app.listen(settings.port, settings.ip);
+ console.log("You can access your Etherpad-Lite instance at http://" + settings.ip + ":" + settings.port + "/");
+ if(!_.isEmpty(settings.users)){
+ console.log("The plugin admin page is at http://" + settings.ip + ":" + settings.port + "/admin/plugins");
+ }
+ else{
+ console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json");
+ }
+ callback(null);
+ }
+]);
diff --git a/node/utils/Abiword.js b/src/node/utils/Abiword.js
similarity index 100%
rename from node/utils/Abiword.js
rename to src/node/utils/Abiword.js
diff --git a/node/utils/Cli.js b/src/node/utils/Cli.js
similarity index 100%
rename from node/utils/Cli.js
rename to src/node/utils/Cli.js
diff --git a/node/utils/ExportDokuWiki.js b/src/node/utils/ExportDokuWiki.js
similarity index 98%
rename from node/utils/ExportDokuWiki.js
rename to src/node/utils/ExportDokuWiki.js
index abe6d347..bcb21108 100644
--- a/node/utils/ExportDokuWiki.js
+++ b/src/node/utils/ExportDokuWiki.js
@@ -15,8 +15,8 @@
*/
var async = require("async");
-var CommonCode = require('./common_code');
-var Changeset = CommonCode.require("/Changeset");
+
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var padManager = require("../db/PadManager");
function getPadDokuWiki(pad, revNum, callback)
diff --git a/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js
similarity index 99%
rename from node/utils/ExportHtml.js
rename to src/node/utils/ExportHtml.js
index afeafd3a..91ebe59f 100644
--- a/node/utils/ExportHtml.js
+++ b/src/node/utils/ExportHtml.js
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-var CommonCode = require('./common_code');
+
var async = require("async");
-var Changeset = CommonCode.require("/Changeset");
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var padManager = require("../db/PadManager");
var ERR = require("async-stacktrace");
-var Security = CommonCode.require('/security');
+var Security = require('ep_etherpad-lite/static/js/security');
function getPadPlainText(pad, revNum)
{
diff --git a/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js
similarity index 93%
rename from node/utils/ImportHtml.js
rename to src/node/utils/ImportHtml.js
index ce866369..4b50b032 100644
--- a/node/utils/ImportHtml.js
+++ b/src/node/utils/ImportHtml.js
@@ -17,10 +17,10 @@
var jsdom = require('jsdom-nocontextifiy').jsdom;
var log4js = require('log4js');
-var CommonCode = require('../utils/common_code');
-var Changeset = CommonCode.require("/Changeset");
-var contentcollector = CommonCode.require("/contentcollector");
-var map = CommonCode.require("/ace2_common").map;
+
+var Changeset = require("ep_etherpad-lite/static/js/Changeset");
+var contentcollector = require("ep_etherpad-lite/static/js/contentcollector");
+var map = require("ep_etherpad-lite/static/js/ace2_common").map;
function setPadHTML(pad, html, callback)
{
diff --git a/node/utils/Minify.js b/src/node/utils/Minify.js
similarity index 87%
rename from node/utils/Minify.js
rename to src/node/utils/Minify.js
index 39c6ceb3..c5996565 100644
--- a/node/utils/Minify.js
+++ b/src/node/utils/Minify.js
@@ -27,19 +27,22 @@ var cleanCSS = require('clean-css');
var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify;
var path = require('path');
+var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
var RequireKernel = require('require-kernel');
-var server = require('../server');
var ROOT_DIR = path.normalize(__dirname + "/../../static/");
var TAR_PATH = path.join(__dirname, 'tar.json');
var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8'));
// Rewrite tar to include modules with no extensions and proper rooted paths.
+var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js';
exports.tar = {};
for (var key in tar) {
- exports.tar['/' + key] =
- tar[key].map(function (p) {return '/' + p}).concat(
- tar[key].map(function (p) {return '/' + p.replace(/\.js$/, '')})
+ exports.tar[LIBRARY_PREFIX + '/' + key] =
+ tar[key].map(function (p) {return LIBRARY_PREFIX + '/' + p}).concat(
+ tar[key].map(function (p) {
+ return LIBRARY_PREFIX + '/' + p.replace(/\.js$/, '')
+ })
);
}
@@ -63,6 +66,22 @@ exports.minify = function(req, res, next)
return;
}
+ /* Handle static files for plugins:
+ paths like "plugins/ep_myplugin/static/js/test.js"
+ are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js,
+ commonly ETHERPAD_ROOT/node_modules/ep_myplugin/static/js/test.js
+ */
+ var match = filename.match(/^plugins\/([^\/]+)\/static\/(.*)/);
+ if (match) {
+ var pluginName = match[1];
+ var resourcePath = match[2];
+ var plugin = plugins.plugins[pluginName];
+ if (plugin) {
+ var pluginPath = plugin.package.realPath;
+ filename = path.relative(ROOT_DIR, pluginPath + '/static/' + resourcePath);
+ }
+ }
+
// What content type should this be?
// TODO: This should use a MIME module.
var contentType;
@@ -89,10 +108,10 @@ exports.minify = function(req, res, next)
date = new Date(date);
res.setHeader('last-modified', date.toUTCString());
res.setHeader('date', (new Date()).toUTCString());
- if (server.maxAge) {
- var expiresDate = new Date((new Date()).getTime()+server.maxAge*1000);
+ if (settings.maxAge !== undefined) {
+ var expiresDate = new Date((new Date()).getTime()+settings.maxAge*1000);
res.setHeader('expires', expiresDate.toUTCString());
- res.setHeader('cache-control', 'max-age=' + server.maxAge);
+ res.setHeader('cache-control', 'max-age=' + settings.maxAge);
}
}
@@ -112,7 +131,10 @@ exports.minify = function(req, res, next)
res.end();
} else if (req.method == 'GET') {
getFileCompressed(filename, contentType, function (error, content) {
- if(ERR(error)) return;
+ if(ERR(error, function(){
+ res.writeHead(500, {});
+ res.end();
+ })) return;
res.header("Content-Type", contentType);
res.writeHead(200, {});
res.write(content);
diff --git a/node/utils/Settings.js b/src/node/utils/Settings.js
similarity index 77%
rename from node/utils/Settings.js
rename to src/node/utils/Settings.js
index 0d30fb68..e60446df 100644
--- a/node/utils/Settings.js
+++ b/src/node/utils/Settings.js
@@ -23,6 +23,10 @@ var fs = require("fs");
var os = require("os");
var path = require('path');
var argv = require('./Cli').argv;
+var npm = require("npm/lib/npm.js");
+
+/* Root path of the installation */
+exports.root = path.normalize(path.join(npm.dir, ".."));
/**
* The IP ep-lite should listen to
@@ -40,7 +44,7 @@ exports.dbType = "dirty";
/**
* This setting is passed with dbType to ueberDB to set up the database
*/
-exports.dbSettings = { "filename" : "../var/dirty.db" };
+exports.dbSettings = { "filename" : path.join(exports.root, "dirty.db") };
/**
* The default Text of a new pad
*/
@@ -76,10 +80,12 @@ exports.abiword = null;
*/
exports.loglevel = "INFO";
-/**
- * Http basic auth, with "user:password" format
- */
-exports.httpAuth = null;
+/* This setting is used if you need authentication and/or
+ * authorization. Note: /admin always requires authentication, and
+ * either authorization by a module, or a user with is_admin set */
+exports.requireAuthentication = false;
+exports.requireAuthorization = false;
+exports.users = {};
//checks if abiword is avaiable
exports.abiwordAvailable = function()
@@ -96,11 +102,19 @@ exports.abiwordAvailable = function()
// Discover where the settings file lives
var settingsFilename = argv.settings || "settings.json";
-var settingsPath = settingsFilename.charAt(0) == '/' ? '' : path.normalize(__dirname + "/../../");
-
-//read the settings sync
-var settingsStr = fs.readFileSync(settingsPath + settingsFilename).toString();
+if (settingsFilename.charAt(0) != '/') {
+ settingsFilename = path.normalize(path.join(root, settingsFilename));
+}
+var settingsStr
+try{
+ //read the settings sync
+ settingsStr = fs.readFileSync(settingsFilename).toString();
+} catch(e){
+ console.warn('No settings file found. Using defaults.');
+ settingsStr = '{}';
+}
+
//remove all comments
settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,"");
@@ -138,3 +152,7 @@ for(var i in settings)
console.warn("This setting doesn't exist or it was removed");
}
}
+
+if(exports.dbType === "dirty"){
+ console.warn("DirtyDB is used. This is fine for testing but not recommended for production.")
+}
diff --git a/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js
similarity index 94%
rename from node/utils/caching_middleware.js
rename to src/node/utils/caching_middleware.js
index a26e22d1..ba2b462d 100644
--- a/node/utils/caching_middleware.js
+++ b/src/node/utils/caching_middleware.js
@@ -18,12 +18,12 @@ var async = require('async');
var Buffer = require('buffer').Buffer;
var fs = require('fs');
var path = require('path');
-var server = require('../server');
var zlib = require('zlib');
var util = require('util');
+var settings = require('./Settings');
-var ROOT_DIR = path.normalize(__dirname + "/../");
-var CACHE_DIR = ROOT_DIR + '../var/';
+var CACHE_DIR = path.normalize(path.join(settings.root, 'var/'));
+CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
var responseCache = {};
@@ -37,7 +37,7 @@ function CachingMiddleware() {
}
CachingMiddleware.prototype = new function () {
function handle(req, res, next) {
- if (!(req.method == "GET" || req.method == "HEAD")) {
+ if (!(req.method == "GET" || req.method == "HEAD") || !CACHE_DIR) {
return next(undefined, req, res);
}
@@ -73,6 +73,9 @@ CachingMiddleware.prototype = new function () {
var _headers = {};
old_res.setHeader = res.setHeader;
res.setHeader = function (key, value) {
+ // Don't set cookies, see issue #707
+ if (key.toLowerCase() === 'set-cookie') return;
+
_headers[key.toLowerCase()] = value;
old_res.setHeader.call(res, key, value);
};
diff --git a/node/utils/customError.js b/src/node/utils/customError.js
similarity index 100%
rename from node/utils/customError.js
rename to src/node/utils/customError.js
diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js
new file mode 100644
index 00000000..4c1bba24
--- /dev/null
+++ b/src/node/utils/randomstring.js
@@ -0,0 +1,16 @@
+/**
+ * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids
+ */
+var randomString = function randomString(len)
+{
+ var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ var randomstring = '';
+ for (var i = 0; i < len; i++)
+ {
+ var rnum = Math.floor(Math.random() * chars.length);
+ randomstring += chars.substring(rnum, rnum + 1);
+ }
+ return randomstring;
+};
+
+module.exports = randomString;
diff --git a/node/utils/tar.json b/src/node/utils/tar.json
similarity index 74%
rename from node/utils/tar.json
rename to src/node/utils/tar.json
index e922ddde..15ce68e2 100644
--- a/node/utils/tar.json
+++ b/src/node/utils/tar.json
@@ -1,13 +1,7 @@
{
"pad.js": [
- "jquery.js"
- , "security.js"
- , "pad.js"
- , "ace2_common.js"
+ "pad.js"
, "pad_utils.js"
- , "plugins.js"
- , "undo-xpopup.js"
- , "json2.js"
, "pad_cookie.js"
, "pad_editor.js"
, "pad_editbar.js"
@@ -22,17 +16,11 @@
, "chat.js"
, "excanvas.js"
, "farbtastic.js"
- , "prefixfree.js"
]
, "timeslider.js": [
- "jquery.js"
- , "security.js"
- , "plugins.js"
- , "undo-xpopup.js"
- , "json2.js"
+ "timeslider.js"
, "colorutils.js"
, "draggable.js"
- , "ace2_common.js"
, "pad_utils.js"
, "pad_cookie.js"
, "pad_editor.js"
@@ -41,7 +29,7 @@
, "pad_modals.js"
, "pad_savedrevs.js"
, "pad_impexp.js"
- , "AttributePoolFactory.js"
+ , "AttributePool.js"
, "Changeset.js"
, "domline.js"
, "linestylefilter.js"
@@ -49,13 +37,12 @@
, "broadcast.js"
, "broadcast_slider.js"
, "broadcast_revisions.js"
- , "timeslider.js"
]
, "ace2_inner.js": [
- "ace2_common.js"
- , "AttributePoolFactory.js"
+ "ace2_inner.js"
+ , "AttributePool.js"
, "Changeset.js"
- , "security.js"
+ , "ChangesetUtils.js"
, "skiplist.js"
, "virtual_lines.js"
, "cssmanager.js"
@@ -65,6 +52,18 @@
, "changesettracker.js"
, "linestylefilter.js"
, "domline.js"
- , "ace2_inner.js"
+ , "AttributeManager.js"
+ ]
+, "ace2_common.js": [
+ "ace2_common.js"
+ , "jquery.js"
+ , "rjquery.js"
+ , "underscore.js"
+ , "security.js"
+ , "json2.js"
+ , "pluginfw/plugins.js"
+ , "pluginfw/hooks.js"
+ , "pluginfw/async.js"
+ , "pluginfw/parent_require.js"
]
}
diff --git a/package.json b/src/package.json
similarity index 62%
rename from package.json
rename to src/package.json
index 01eb8e96..c46abbbf 100644
--- a/package.json
+++ b/src/package.json
@@ -1,5 +1,5 @@
{
- "name" : "etherpad-lite",
+ "name" : "ep_etherpad-lite",
"description" : "A Etherpad based on node.js",
"homepage" : "https://github.com/Pita/etherpad-lite",
"keywords" : ["etherpad", "realtime", "collaborative", "editor"],
@@ -10,20 +10,29 @@
"name": "Robin Buse" }
],
"dependencies" : {
- "yajsml" : "1.1.2",
+ "yajsml" : "1.1.3",
"request" : "2.9.100",
- "require-kernel" : "1.0.3",
- "socket.io" : "0.8.7",
+ "require-kernel" : "1.0.5",
+ "resolve" : "0.2.1",
+ "socket.io" : "0.9.6",
"ueberDB" : "0.1.7",
"async" : "0.1.18",
"express" : "2.5.8",
+ "connect" : "1.8.7",
"clean-css" : "0.3.2",
"uglify-js" : "1.2.5",
"formidable" : "1.0.9",
"log4js" : "0.4.1",
"jsdom-nocontextifiy" : "0.2.10",
- "async-stacktrace" : "0.0.2"
+ "async-stacktrace" : "0.0.2",
+ "npm" : "1.1",
+ "ejs" : "0.6.1",
+ "graceful-fs" : "1.1.5",
+ "slide" : "1.1.3",
+ "semver" : "1.0.13",
+ "underscore" : "1.3.1"
},
+ "bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": {
"jshint" : "*"
},
diff --git a/src/static/css/admin.css b/src/static/css/admin.css
new file mode 100644
index 00000000..5eb008fa
--- /dev/null
+++ b/src/static/css/admin.css
@@ -0,0 +1,122 @@
+body {
+ margin: 0;
+ color: #333;
+ font: 14px helvetica, sans-serif;
+ background: #ddd;
+ background: -webkit-radial-gradient(circle,#aaa,#eee 60%) center fixed;
+ background: -moz-radial-gradient(circle,#aaa,#eee 60%) center fixed;
+ background: -ms-radial-gradient(circle,#aaa,#eee 60%) center fixed;
+ background: -o-radial-gradient(circle,#aaa,#eee 60%) center fixed;
+ border-top: 8px solid rgba(51,51,51,.8);
+}
+#wrapper {
+ margin-top: 160px;
+ padding: 15px;
+ background: #fff;
+ opacity: .9;
+ box-shadow: 0px 1px 8px rgba(0,0,0,0.3);
+ max-width: 700px;
+ margin: auto;
+ border-radius: 0 0 7px 7px;
+}
+h1 {
+ font-size: 29px;
+}
+h2 {
+ font-size: 24px;
+}
+.separator {
+ margin: 10px 0;
+ height: 1px;
+ background: #aaa;
+ background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+ background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+ background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+ background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+}
+form {
+ margin-bottom: 0;
+}
+#inner {
+ width: 300px;
+ margin: 0 auto;
+}
+input {
+ font-weight: bold;
+ font-size: 15px;
+}
+input[type="button"] {
+ padding: 4px 6px;
+ margin: 0;
+}
+input[type="button"].do-install, input[type="button"].do-uninstall {
+ float: right;
+ width: 100px;
+}
+input[type="button"]#do-search {
+ display: block;
+}
+input[type="text"] {
+ border-radius: 3px;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ padding: 10px;
+ *padding: 0; /* IE7 hack */
+ width: 100%;
+ outline: none;
+ border: 1px solid #ddd;
+ margin: 0 0 5px 0;
+ max-width: 500px;
+}
+table {
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ border-spacing: 0;
+ width: 100%;
+ margin: 20px 0;
+}
+table thead tr {
+ background: #eee;
+}
+td, th {
+ padding: 5px;
+}
+.template {
+ display: none;
+}
+.dialog {
+ display: none;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 700px;
+ height: 500px;
+ margin-left: -350px;
+ margin-top: -250px;
+ border: 3px solid #999;
+ background: #eee;
+}
+.dialog .title {
+ margin: 0;
+ padding: 2px;
+ border-bottom: 3px solid #999;
+ font-size: 24px;
+ line-height: 24px;
+ height: 24px;
+ overflow: hidden;
+}
+.dialog .title .close {
+ float: right;
+ padding: 1px 10px;
+}
+.dialog .history {
+ background: #222;
+ color: #eee;
+ position: absolute;
+ top: 41px;
+ bottom: 10px;
+ left: 10px;
+ right: 10px;
+ padding: 2px;
+ overflow: auto;
+}
\ No newline at end of file
diff --git a/static/css/iframe_editor.css b/src/static/css/iframe_editor.css
similarity index 98%
rename from static/css/iframe_editor.css
rename to src/static/css/iframe_editor.css
index d2d2f977..4fcd955f 100644
--- a/static/css/iframe_editor.css
+++ b/src/static/css/iframe_editor.css
@@ -5,6 +5,13 @@
html { cursor: text; } /* in Safari, produces text cursor for whole doc (inc. below body) */
span { cursor: auto; }
+::selection {
+ background: #acf;
+}
+::-moz-selection {
+ background: #acf;
+}
+
a { cursor: pointer !important; }
ul, ol, li {
diff --git a/src/static/css/pad.css b/src/static/css/pad.css
new file mode 100644
index 00000000..b1187b09
--- /dev/null
+++ b/src/static/css/pad.css
@@ -0,0 +1,995 @@
+*,
+html,
+body,
+p {
+ margin: 0;
+ padding: 0;
+}
+.clear {
+ clear: both
+}
+html {
+ font-size: 62.5%;
+ width: 100%;
+}
+body,
+textarea {
+ font-family: Helvetica, Arial, sans-serif
+}
+iframe {
+ position: absolute
+}
+#users {
+ background: #f7f7f7;
+ background: -webkit-linear-gradient( #F7F7F7,#EEE);
+ background: -moz-linear-gradient( #F7F7F7,#EEE);
+ background: -ms-linear-gradient( #F7F7F7,#EEE);
+ background: -o-linear-gradient( #F7F7F7,#EEE);
+ background: linear-gradient( #F7F7F7,#EEE);
+ width: 160px;
+ color: #fff;
+ padding: 5px;
+ border-radius: 0 0 6px 6px;
+ border: 1px solid #ccc;
+}
+#otherusers {
+ max-height: 400px;
+ overflow: auto;
+}
+a img {
+ border: 0
+}
+/* menu */
+.toolbar {
+ background: #f7f7f7;
+ background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: linear-gradient(#f7f7f7, #f1f1f1 80%);
+ border-bottom: 1px solid #ccc;
+ overflow: hidden;
+ padding-top: 4px;
+ width: 100%;
+ white-space: nowrap;
+ height: 32px;
+}
+.toolbar ul {
+ position: relative;
+ list-style: none;
+ padding-right: 3px;
+ padding-left: 1px;
+ z-index: 2;
+ overflow: hidden;
+ float: left
+}
+.toolbar ul.menu_right {
+ float: right
+}
+.toolbar ul li {
+ float: left;
+ margin-left: 2px;
+}
+.toolbar ul li.separator {
+ border: inherit;
+ background: inherit;
+ visibility: hidden;
+ width: 0px;
+ padding: 5px;
+}
+.toolbar ul li a:hover {
+ text-decoration: none;
+}
+.toolbar ul li a:hover {
+ background: #fff;
+ background: -webkit-linear-gradient(#f4f4f4, #e4e4e4);
+ background: -moz-linear-gradient(#f4f4f4, #e4e4e4);
+ background: -o-linear-gradient(#f4f4f4, #e4e4e4);
+ background: -ms-linear-gradient(#f4f4f4, #e4e4e4);
+ background: linear-gradient(#f4f4f4, #e4e4e4);
+}
+.toolbar ul li a:active {
+ background: #eee;
+ background: -webkit-linear-gradient(#ddd, #fff);
+ background: -moz-linear-gradient(#ddd, #fff);
+ background: -o-linear-gradient(#ddd, #fff);
+ background: -ms-linear-gradient(#ddd, #fff);
+ background: linear-gradient(#ddd, #fff);
+ -webkit-box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
+ -moz-box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
+ box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
+}
+.toolbar ul li a {
+ background: #fff;
+ background: -webkit-linear-gradient(#fff, #f0f0f0);
+ background: -moz-linear-gradient(#fff, #f0f0f0);
+ background: -o-linear-gradient(#fff, #f0f0f0);
+ background: -ms-linear-gradient(#fff, #f0f0f0);
+ background: linear-gradient(#fff, #f0f0f0);
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ color: #ccc;
+ cursor: pointer;
+ display: inline-block;
+ min-height: 18px;
+ overflow: hidden;
+ padding: 4px 5px;
+ text-align: center;
+ text-decoration: none;
+ min-width: 18px;
+}
+.toolbar ul li a .buttonicon {
+ position: relative;
+ top: 1px;
+}
+.toolbar ul li a.grouped-left {
+ border-radius: 3px 0 0 3px;
+}
+.toolbar ul li a.grouped-middle {
+ border-radius: 0;
+ margin-left: -2px;
+ border-left: 0;
+}
+.toolbar ul li a.grouped-right {
+ border-radius: 0 3px 3px 0;
+ margin-left: -2px;
+ border-left: 0;
+}
+.toolbar ul li a.selected {
+ background: #eee !important;
+ background: -webkit-linear-gradient(#EEE, #F0F0F0) !important;
+ background: -moz-linear-gradient(#EEE, #F0F0F0) !important;
+ background: -o-linear-gradient(#EEE, #F0F0F0) !important;
+ background: -ms-linear-gradient(#EEE, #F0F0F0) !important;
+ background: linear-gradient(#EEE, #F0F0F0) !important;
+}
+.toolbar ul li select {
+ background: #fff;
+ padding: 4px;
+ line-height: 22px; /* fix for safari (win/mac) */
+ height: 28px; /* fix for chrome (mac) */
+ border-radius: 3px;
+ border: 1px solid #ccc;
+ outline: none;
+}
+#usericon a {
+ min-width: 30px;
+ text-align: left;
+}
+#usericon a #online_count {
+ color: #777;
+ font-size: 10px;
+ position: relative;
+ top: 2px;
+}
+#editorcontainer {
+ position: absolute;
+ width: 100%;
+ top: 37px; /* + 1px border */
+ left: 0px;
+ bottom: 0px;
+ z-index: 1;
+}
+#editorcontainer iframe {
+ height: 100%;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+}
+#editorloadingbox {
+ padding-top: 100px;
+ padding-bottom: 100px;
+ font-size: 2.5em;
+ color: #aaa;
+ text-align: center;
+ position: absolute;
+ width: 100%;
+ height: 30px;
+ z-index: 100;
+}
+#editorcontainerbox {
+ position: absolute;
+ bottom: 0;
+ top: 0;
+ width: 100%;
+}
+#padpage {
+ position: absolute;
+ top: 0px;
+ bottom: 0px;
+ width: 100%;
+}
+#padmain {
+ margin-top: 0px;
+ position: absolute;
+ top: 63px !important;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ zoom: 1;
+}
+#padeditor {
+ bottom: 0px;
+ left: 0;
+ position: absolute;
+ right: 0px;
+ top: 0;
+ zoom: 1;
+}
+#myswatchbox {
+ position: absolute;
+ left: 5px;
+ top: 5px;
+ width: 24px;
+ height: 24px;
+ border: 1px solid #000;
+ background: transparent;
+ cursor: pointer;
+}
+#myswatch {
+ width: 100%;
+ height: 100%;
+ background: transparent; /*...initially*/
+}
+#mycolorpicker {
+ width: 232px;
+ height: 265px;
+ position: absolute;
+ left: -250px;
+ top: 0px;
+ z-index: 101;
+ display: none;
+ border-radius: 0 0 6px 6px;
+ background: #f7f7f7;
+ border: 1px solid #ccc;
+ border-top: 0;
+ padding-left: 10px;
+ padding-top: 10px;
+}
+#mycolorpickersave {
+ left: 10px;
+ font-weight: bold;
+}
+#mycolorpickercancel {
+ left: 85px
+}
+#mycolorpickersave,
+#mycolorpickercancel {
+ background: #fff;
+ background: -webkit-linear-gradient(#fff, #ccc);
+ background: -moz-linear-gradient(#fff, #ccc);
+ background: -o-linear-gradient(#fff, #ccc);
+ background: -ms-linear-gradient(#fff, #ccc);
+ background: linear-gradient(#fff, #ccc);
+ border: 1px solid #ccc;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ color: #000;
+ overflow: hidden;
+ padding: 4px;
+ top: 240px;
+ text-align: center;
+ position: absolute;
+ width: 60px;
+}
+#mycolorpickerpreview {
+ position: absolute;
+ left: 207px;
+ top: 240px;
+ width: 16px;
+ height: 16px;
+ padding: 4px;
+ overflow: hidden;
+ color: #fff;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+#myusernameform {
+ margin-left: 35px
+}
+#myusernameedit {
+ font-size: 1.3em;
+ color: #fff;
+ padding: 3px;
+ height: 18px;
+ margin: 0;
+ border: 0;
+ width: 117px;
+ background: transparent;
+}
+#myusernameform input.editable {
+ border: 1px solid #444
+}
+#myuser .myusernameedithoverable:hover {
+ background: white;
+ color: black;
+}
+#mystatusform {
+ margin-left: 35px;
+ margin-top: 5px;
+}
+#mystatusedit {
+ font-size: 1.2em;
+ color: #777;
+ font-style: italic;
+ display: none;
+ padding: 2px;
+ height: 14px;
+ margin: 0;
+ border: 1px solid #bbb;
+ width: 199px;
+ background: transparent;
+}
+#myusernameform .editactive,
+#myusernameform .editempty {
+ background: white;
+ border-left: 1px solid #c3c3c3;
+ border-top: 1px solid #c3c3c3;
+ border-right: 1px solid #e6e6e6;
+ border-bottom: 1px solid #e6e6e6;
+ color: #000;
+}
+#myusernameform .editempty {
+ color: #333
+}
+#myswatchbox, #myusernameedit, #otheruserstable .swatch {
+ border: 1px solid #ccc !important;
+ color: #333;
+}
+table#otheruserstable {
+ display: none
+}
+#nootherusers {
+ padding: 10px;
+ font-size: 1.2em;
+ color: #eee;
+ font-weight: bold;
+}
+#nootherusers a {
+ color: #3C88FF
+}
+#otheruserstable td {
+ height: 26px;
+ vertical-align: middle;
+ padding: 0 2px;
+ color: #333;
+}
+#otheruserstable .swatch {
+ border: 1px solid #000;
+ width: 13px;
+ height: 13px;
+ overflow: hidden;
+ margin: 0 4px;
+}
+.usertdswatch {
+ width: 1%
+}
+.usertdname {
+ font-size: 1.3em;
+ color: #444;
+}
+.usertdstatus {
+ font-size: 1.1em;
+ font-style: italic;
+ color: #999;
+}
+.usertdactivity {
+ font-size: 1.1em;
+ color: #777;
+}
+.usertdname input {
+ border: 1px solid #bbb;
+ width: 80px;
+ padding: 2px;
+}
+.usertdname input.editactive,
+.usertdname input.editempty {
+ background: white;
+ border-left: 1px solid #c3c3c3;
+ border-top: 1px solid #c3c3c3;
+ border-right: 1px solid #e6e6e6;
+ border-bottom: 1px solid #e6e6e6;
+}
+.usertdname input.editempty {
+ color: #888;
+ font-style: italic;
+}
+.modaldialog.cboxreconnecting .modaldialog-inner,
+.modaldialog.cboxconnecting .modaldialog-inner {
+ background: url(../../static/img/connectingbar.gif) no-repeat center 60px;
+ height: 100px;
+}
+.modaldialog.cboxreconnecting,
+.modaldialog.cboxconnecting,
+.modaldialog.cboxdisconnected {
+ background: #8FCDE0
+}
+.cboxdisconnected #connectionboxinner div {
+ display: none
+}
+.cboxdisconnected_userdup #connectionboxinner #disconnected_userdup {
+ display: block
+}
+.cboxdisconnected_deleted #connectionboxinner #disconnected_deleted {
+ display: block
+}
+.cboxdisconnected_initsocketfail #connectionboxinner #disconnected_initsocketfail {
+ display: block
+}
+.cboxdisconnected_looping #connectionboxinner #disconnected_looping {
+ display: block
+}
+.cboxdisconnected_slowcommit #connectionboxinner #disconnected_slowcommit {
+ display: block
+}
+.cboxdisconnected_unauth #connectionboxinner #disconnected_unauth {
+ display: block
+}
+.cboxdisconnected_unknown #connectionboxinner #disconnected_unknown {
+ display: block
+}
+.cboxdisconnected_initsocketfail #connectionboxinner #reconnect_advise,
+.cboxdisconnected_looping #connectionboxinner #reconnect_advise,
+.cboxdisconnected_slowcommit #connectionboxinner #reconnect_advise,
+.cboxdisconnected_unknown #connectionboxinner #reconnect_advise {
+ display: block
+}
+.cboxdisconnected div#reconnect_form {
+ display: block
+}
+.cboxdisconnected .disconnected h2 {
+ display: none
+}
+.cboxdisconnected .disconnected .h2_disconnect {
+ display: block
+}
+.cboxdisconnected_userdup .disconnected h2.h2_disconnect {
+ display: none
+}
+.cboxdisconnected_userdup .disconnected h2.h2_userdup {
+ display: block
+}
+.cboxdisconnected_unauth .disconnected h2.h2_disconnect {
+ display: none
+}
+.cboxdisconnected_unauth .disconnected h2.h2_unauth {
+ display: block
+}
+#connectionstatus {
+ position: absolute;
+ width: 37px;
+ height: 41px;
+ overflow: hidden;
+ right: 0;
+ z-index: 11;
+}
+#connectionboxinner .connecting {
+ margin-top: 20px;
+ font-size: 2.0em;
+ color: #555;
+ text-align: center;
+ display: none;
+}
+.cboxconnecting #connectionboxinner .connecting {
+ display: block
+}
+#connectionboxinner .disconnected h2 {
+ font-size: 1.8em;
+ color: #333;
+ text-align: left;
+ margin-top: 10px;
+ margin-left: 10px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+}
+#connectionboxinner .disconnected p {
+ margin: 10px 10px;
+ font-size: 1.2em;
+ line-height: 1.1;
+ color: #333;
+}
+#connectionboxinner .disconnected {
+ display: none
+}
+.cboxdisconnected #connectionboxinner .disconnected {
+ display: block
+}
+#connectionboxinner .reconnecting {
+ margin-top: 20px;
+ font-size: 1.6em;
+ color: #555;
+ text-align: center;
+ display: none;
+}
+.cboxreconnecting #connectionboxinner .reconnecting {
+ display: block
+}
+#reconnect_form button {
+ font-size: 12pt;
+ padding: 5px;
+}
+#mainmodals {
+ z-index: 600; /* higher than the modals themselves: */
+}
+.modalfield {
+ font-size: 1.2em;
+ padding: 1px;
+ border: 1px solid #bbb;
+}
+#mainmodals .editempty {
+ color: #aaa
+}
+.modaldialog {
+ position: absolute;
+ top: 100px;
+ left: 50%;
+ margin-left: -243px;
+ width: 485px;
+ display: none;
+ z-index: 501;
+ zoom: 1;
+ overflow: hidden;
+ background: white;
+ border: 1px solid #999;
+}
+.modaldialog .modaldialog-inner {
+ padding: 10pt
+}
+.modaldialog .modaldialog-hide {
+ float: right;
+ background-repeat: no-repeat;
+ background-image: url(static/img/sharebox4.gif);
+ display: block;
+ width: 22px;
+ height: 22px;
+ background-position: -454px -6px;
+ margin-right: -5px;
+ margin-top: -5px;
+}
+.modaldialog label,
+.modaldialog h1 {
+ color: #222222;
+ font-size: 125%;
+ font-weight: bold;
+}
+.modaldialog th {
+ vertical-align: top;
+ text-align: left;
+}
+
+#modaloverlay {
+ z-index: 500;
+ display: none;
+ background-repeat: repeat-both;
+ width: 100%;
+ position: absolute;
+ height: 100%;
+ left: 0;
+ top: 0;
+}
+* html #modaloverlay {
+ /* for IE 6+ */
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+ filter: alpha(opacity=100);
+ opacity: 1; /* in case this is looked at */
+ background-image: none;
+ background-repeat: no-repeat; /* scale the image */
+}
+
+#chatbox {
+ position: absolute;
+ bottom: 0px;
+ right: 20px;
+ width: 180px;
+ height: 200px;
+ z-index: 400;
+ background-color: #f7f7f7;
+ border-left: 1px solid #999;
+ border-right: 1px solid #999;
+ border-top: 1px solid #999;
+ padding: 3px;
+ padding-bottom: 10px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ display: none;
+}
+#chattext {
+ background-color: white;
+ border: 1px solid white;
+ -ms-overflow-y: scroll;
+ overflow-y: scroll;
+ font-size: 12px;
+ position: absolute;
+ right: 0px;
+ left: 0px;
+ top: 25px;
+ bottom: 25px;
+ z-index: 1002;
+}
+#chattext p {
+ padding: 3px;
+ -ms-overflow-x: hidden;
+ overflow-x: hidden;
+}
+#chatinputbox {
+ padding: 3px 2px;
+ position: absolute;
+ bottom: 0px;
+ right: 0px;
+ left: 3px;
+}
+#chatlabel {
+ font-size: 13px;
+ font-weight: bold;
+ color: #555;
+ text-decoration: none;
+ margin-right: 3px;
+ vertical-align: middle;
+}
+#chatinput {
+ border: 1px solid #BBBBBB;
+ width: 100%;
+ float: right;
+}
+#chaticon {
+ z-index: 400;
+ position: fixed;
+ bottom: 0px;
+ right: 20px;
+ padding: 5px;
+ border-left: 1px solid #999;
+ border-right: 1px solid #999;
+ border-top: 1px solid #999;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ background-color: #fff;
+ cursor: pointer;
+}
+#chaticon a {
+ text-decoration: none
+}
+#chatcounter {
+ color: #777;
+ font-size: 10px;
+ vertical-align: middle;
+}
+#titlebar {
+ line-height: 16px;
+ font-weight: bold;
+ color: #555;
+ position: relative;
+ bottom: 2px;
+}
+#titlelabel {
+ font-size: 13px;
+ margin: 4px 0 0 4px;
+ position: absolute;
+}
+#titlecross {
+ font-size: 25px;
+ float: right;
+ text-align: right;
+ text-decoration: none;
+ cursor: pointer;
+ color: #555;
+}
+.time {
+ float: right;
+ color: #333;
+ font-style: italic;
+ font-size: 10px;
+ margin-left: 3px;
+ margin-right: 3px;
+ margin-top: 2px;
+}
+.exporttype {
+ margin-top: 4px;
+ background-repeat: no-repeat;
+ padding-left: 25px;
+ background-image: url("../../static/img/etherpad_lite_icons.png");
+ color: #333;
+ text-decoration: none;
+}
+#exporthtml {
+ background-position: 0px -299px
+}
+#exportplain {
+ background-position: 0px -395px
+}
+#exportword {
+ background-position: 0px -275px
+}
+#exportpdf {
+ background-position: 0px -371px
+}
+#exportopen {
+ background-position: 0px -347px
+}
+#exportdokuwiki {
+ background-position: 0px -459px
+}
+#importstatusball {
+ display: none
+}
+#importarrow {
+ display: none
+}
+#importmessagesuccess {
+ display: none
+}
+#importsubmitinput {
+ height: 25px;
+ width: 85px;
+ margin-top: 12px;
+}
+#importstatusball {
+ height: 50px
+}
+#chatthrob {
+ display: none;
+ position: absolute;
+ bottom: 40px;
+ font-size: 14px;
+ width: 150px;
+ height: 40px;
+ right: 20px;
+ z-index: 200;
+ background-color: #000;
+ color: white;
+ background-color: rgb(0,0,0);
+ background-color: rgba(0,0,0,0.7);
+ padding: 10px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
+ filter: alpha(opacity=80);
+ opacity: .8;
+}
+.buttonicon {
+ width: 16px;
+ height: 16px;
+ background-image: url('../../static/img/etherpad_lite_icons.png');
+ background-repeat: no-repeat;
+ display: inline-block;
+ vertical-align: middle;
+}
+.buttonicon-bold {
+ background-position: 0px -116px
+}
+.buttonicon-italic {
+ background-position: 0px 0px
+}
+.buttonicon-underline {
+ background-position: 0px -236px
+}
+.buttonicon-strikethrough {
+ background-position: 0px -200px
+}
+.buttonicon-insertorderedlist {
+ background-position: 0px -477px
+}
+.buttonicon-insertunorderedlist {
+ background-position: 0px -34px
+}
+.buttonicon-indent {
+ background-position: 0px -52px
+}
+.buttonicon-outdent {
+ background-position: 0px -134px
+}
+.buttonicon-undo {
+ background-position: 0px -255px
+}
+.buttonicon-redo {
+ background-position: 0px -166px
+}
+.buttonicon-clearauthorship {
+ background-position: 0px -86px
+}
+.buttonicon-settings {
+ background-position: 0px -436px
+}
+.buttonicon-import_export {
+ background-position: 0px -68px
+}
+.buttonicon-embed {
+ background-position: 0px -18px
+}
+.buttonicon-history {
+ background-position: 0px -218px
+}
+.buttonicon-chat {
+ background-position: 0px -102px;
+}
+.buttonicon-showusers {
+ background-position: 0px -183px;
+}
+.buttonicon-savedRevision {
+ background-position: 0px -493px
+}
+#focusprotector {
+ z-index: 100;
+ position: absolute;
+ bottom: 0px;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ background-color: white;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=1)";
+ filter: alpha(opacity=1);
+ opacity: 0.01;
+ display: none;
+}
+#online_count {
+ color: #888;
+}
+.rtl {
+ direction: RTL
+}
+#chattext p {
+ word-wrap: break-word
+}
+/* fix for misaligned checkboxes */
+input[type=checkbox] {
+ vertical-align: -1px
+}
+.right {
+ float: right
+}
+.popup {
+ font-size: 14px;
+ width: 450px;
+ padding: 10px;
+ border-radius: 0 0 6px 6px;
+ border: 1px solid #ccc;
+ background: #f7f7f7;
+ background: -webkit-linear-gradient(#F7F7F7, #EEE);
+ background: -moz-linear-gradient(#F7F7F7, #EEE);
+ background: -ms-linear-gradient(#F7F7F7, #EEE);
+ background: -o-linear-gradient(#F7F7F7, #EEE);
+ background: linear-gradient(#F7F7F7, #EEE);
+ -webkit-box-shadow: 0 0 8px #888;
+ -moz-box-shadow: 0 0 8px #888;
+ box-shadow: 0 2px 4px #ddd;
+ color: #222;
+}
+.popup input[type=text] {
+ width: 100%;
+ padding: 5px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ display: block;
+ margin-top: 10px;
+}
+.popup input[type=text], #users input[type=text] {
+ outline: none;
+}
+.popup a {
+ text-decoration: none
+}
+.popup h1 {
+ color: #555;
+ font-size: 18px
+}
+.popup h2 {
+ color: #777;
+ font-size: 15px
+}
+.popup p {
+ margin: 5px 0
+}
+.column {
+ float: left;
+ width: 50%;
+}
+#settings,
+#importexport,
+#embed,
+#users {
+ position: absolute;
+ top: 36px;
+ right: 20px;
+ display: none;
+ z-index: 500;
+}
+.stickyChat {
+ background-color: #f1f1f1 !important;
+ right: 0px !important;
+ top: 37px;
+ -webkit-border-radius: 0px !important;
+ -moz-border-radius: 0px !important;
+ border-radius: 0px !important;
+ height: auto !important;
+ border: none !important;
+ border-left: 1px solid #ccc !important;
+ width: 185px !important;
+}
+@media screen and (max-width: 960px) {
+ .modaldialog {
+ position: relative;
+ margin: 0 auto;
+ width: 80%;
+ top: 40px;
+ left: 0;
+ }
+}
+@media screen and (max-width: 600px) {
+ .toolbar ul li.separator {
+ display: none;
+ }
+ .toolbar ul li a {
+ padding: 4px 1px
+ }
+}
+@media only screen and (min-device-width: 320px) and (max-device-width: 720px) {
+ #users {
+ top: 36px;
+ bottom: 40px;
+ border-radius: none;
+ }
+ #mycolorpicker {
+ left: -73px;
+ /* #mycolorpicker: width -#users: width */;
+ }
+ #editorcontainer {
+ margin-bottom: 33px
+ }
+ .toolbar ul.menu_right {
+ background: #f7f7f7;
+ background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: linear-gradient(#f7f7f7, #f1f1f1 80%);
+ width: 100%;
+ overflow: hidden;
+ height: 32px;
+ position: fixed;
+ bottom: 0;
+ border-top: 1px solid #ccc;
+ }
+ .toolbar ul.menu_right > li:last-child {
+ float: right;
+ }
+ .toolbar ul.menu_right > li a {
+ border-radius: 0;
+ border: none;
+ background: none;
+ margin: 0;
+ padding: 8px;
+ }
+ .toolbar ul li a.selected {
+ background: none !important
+ }
+ #chaticon, #timesliderlink {
+ display: none !important
+ }
+ .popup {
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ }
+ #settings,
+ #importexport,
+ #embed {
+ left: 0;
+ top: 0;
+ bottom: 33px;
+ right: 0;
+ }
+ .toolbar ul li .separator {
+ display: none
+ }
+}
diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css
new file mode 100644
index 00000000..4c8913d3
--- /dev/null
+++ b/src/static/css/timeslider.css
@@ -0,0 +1,288 @@
+#editorcontainerbox {
+ overflow: auto;
+ top: 40px;
+ position: static;
+}
+#padcontent {
+ font-size: 12px;
+ padding: 10px;
+}
+#timeslider-wrapper {
+ left: 0;
+ position: relative;
+ right: 0;
+ top: 0;
+}
+#timeslider-left {
+ background-image: url(../../static/img/timeslider_left.png);
+ height: 63px;
+ left: 0;
+ position: absolute;
+ width: 134px;
+}
+#timeslider-right {
+ background-image: url(../../static/img/timeslider_right.png);
+ height: 63px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 155px;
+}
+#timeslider {
+ background-image: url(../../static/img/timeslider_background.png);
+ height: 63px;
+ margin: 0 9px;
+}
+#timeslider #timeslider-slider {
+ height: 61px;
+ left: 0;
+ position: absolute;
+ top: 1px;
+ width: 100%;
+}
+#ui-slider-handle {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ background-image: url(../../static/img/crushed_current_location.png);
+ cursor: pointer;
+ height: 61px;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 13px;
+}
+#ui-slider-bar {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ cursor: pointer;
+ height: 35px;
+ margin-left: 5px;
+ margin-right: 148px;
+ position: relative;
+ top: 20px;
+}
+#playpause_button,
+#playpause_button_icon {
+ height: 47px;
+ position: absolute;
+ width: 47px;
+}
+#playpause_button {
+ background-image: url(../../static/img/crushed_button_undepressed.png);
+ right: 77px;
+ top: 9px;
+}
+#playpause_button_icon {
+ background-image: url(../../static/img/play.png);
+ left: 0;
+ top: 0;
+}
+.pause#playpause_button_icon {
+ background-image: url(../../static/img/pause.png)
+}
+#leftstar,
+#rightstar,
+#leftstep,
+#rightstep {
+ background: url(../../static/img/stepper_buttons.png) 0 0 no-repeat;
+ height: 21px;
+ overflow: hidden;
+ position: absolute;
+}
+#leftstar {
+ background-position: 0 -44px;
+ right: 34px;
+ top: 8px;
+ width: 30px;
+}
+#rightstar {
+ background-position: -29px -44px;
+ right: 5px;
+ top: 8px;
+ width: 29px;
+}
+#leftstep {
+ background-position: 0 -22px;
+ right: 34px;
+ top: 20px;
+ width: 30px;
+}
+#rightstep {
+ background-position: -29px -22px;
+ right: 5px;
+ top: 20px;
+ width: 29px;
+}
+#timeslider .star {
+ background-image: url(../../static/img/star.png);
+ cursor: pointer;
+ height: 16px;
+ position: absolute;
+ top: 40px;
+ width: 15px;
+}
+#timeslider #timer {
+ color: #fff;
+ font-family: Arial, sans-serif;
+ font-size: 11px;
+ left: 7px;
+ position: absolute;
+ text-align: center;
+ top: 9px;
+ width: 122px;
+}
+.topbarcenter,
+#docbar {
+ display: none
+}
+#padmain {
+ top: 0px !important
+}
+#editbarright {
+ float: right
+}
+#returnbutton {
+ color: #222;
+ font-size: 16px;
+ line-height: 29px;
+ margin-top: 0;
+ padding-right: 6px;
+}
+#importexport .popup {
+ width: 185px
+}
+#importexport {
+ top: 118px;
+ width: 185px;
+}
+.timeslider-bar {
+ background: #f7f7f7;
+ background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
+ background: linear-gradient(#f7f7f7, #f1f1f1 80%);
+ overflow: hidden;
+ padding-top: 3px;
+ width: 100%;
+}
+.timeslider-bar #editbar {
+ border-bottom: none;
+ float: right;
+ width: 170px;
+ width: initial;
+}
+.timeslider-bar h1 {
+ margin: 5px
+}
+.timeslider-bar p {
+ margin: 5px
+}
+#timeslider-top {
+ width: 100%;
+ position: fixed;
+ z-index: 1;
+}
+#authorsList .author {
+ padding-left: 0.4em;
+ padding-right: 0.4em;
+}
+#authorsList .author-anonymous {
+ padding-left: 0.6em;
+ padding-right: 0.6em;
+}
+#padeditor {
+ position: static
+}
+/* lists */
+.list-bullet2,
+.list-indent2,
+.list-number2 {
+ margin-left: 3em
+}
+.list-bullet3,
+.list-indent3,
+.list-number3 {
+ margin-left: 4.5em
+}
+.list-bullet4,
+.list-indent4,
+.list-number4 {
+ margin-left: 6em
+}
+.list-bullet5,
+.list-indent5,
+.list-number5 {
+ margin-left: 7.5em
+}
+.list-bullet6,
+.list-indent6,
+.list-number6 {
+ margin-left: 9em
+}
+.list-bullet7,
+.list-indent7,
+.list-number7 {
+ margin-left: 10.5em
+}
+.list-bullet8,
+.list-indent8,
+.list-number8 {
+ margin-left: 12em
+}
+/* unordered lists */
+UL {
+ list-style-type: disc;
+ margin-left: 1.5em;
+}
+UL UL {
+ margin-left: 0 !important
+}
+.list-bullet2,
+.list-bullet5,
+.list-bullet8 {
+ list-style-type: circle
+}
+.list-bullet3,
+.list-bullet6 {
+ list-style-type: square
+}
+.list-indent1,
+.list-indent2,
+.list-indent3,
+.list-indent5,
+.list-indent5,
+.list-indent6,
+.list-indent7,
+.list-indent8 {
+ list-style-type: none
+}
+/* ordered lists */
+OL {
+ list-style-type: decimal;
+ margin-left: 1.5em;
+}
+.list-number2,
+.list-number5,
+.list-number8 {
+ list-style-type: lower-latin
+}
+.list-number3,
+.list-number6 {
+ list-style-type: lower-roman
+}
+/* IE 6/7 fixes */
+* HTML #ui-slider-handle {
+ background-image: url(../../static/img/current_location.gif)
+}
+* HTML #timeslider .star {
+ background-image: url(../../static/img/star.gif)
+}
+* HTML #playpause_button_icon {
+ background-image: url(../../static/img/play.gif)
+}
+* HTML .pause#playpause_button_icon {
+ background-image: url(../../static/img/pause.gif)
+}
\ No newline at end of file
diff --git a/src/static/custom/.gitignore b/src/static/custom/.gitignore
new file mode 100644
index 00000000..aae16bb2
--- /dev/null
+++ b/src/static/custom/.gitignore
@@ -0,0 +1,3 @@
+*
+!.gitignore
+!*.template
diff --git a/static/custom/css.template b/src/static/custom/css.template
similarity index 100%
rename from static/custom/css.template
rename to src/static/custom/css.template
diff --git a/static/custom/js.template b/src/static/custom/js.template
similarity index 100%
rename from static/custom/js.template
rename to src/static/custom/js.template
diff --git a/static/favicon.ico b/src/static/favicon.ico
similarity index 100%
rename from static/favicon.ico
rename to src/static/favicon.ico
diff --git a/static/img/backgrad.gif b/src/static/img/backgrad.gif
similarity index 100%
rename from static/img/backgrad.gif
rename to src/static/img/backgrad.gif
diff --git a/static/img/connectingbar.gif b/src/static/img/connectingbar.gif
similarity index 100%
rename from static/img/connectingbar.gif
rename to src/static/img/connectingbar.gif
diff --git a/static/img/crushed_button_depressed.png b/src/static/img/crushed_button_depressed.png
similarity index 100%
rename from static/img/crushed_button_depressed.png
rename to src/static/img/crushed_button_depressed.png
diff --git a/static/img/crushed_button_undepressed.png b/src/static/img/crushed_button_undepressed.png
similarity index 100%
rename from static/img/crushed_button_undepressed.png
rename to src/static/img/crushed_button_undepressed.png
diff --git a/static/img/crushed_current_location.png b/src/static/img/crushed_current_location.png
similarity index 100%
rename from static/img/crushed_current_location.png
rename to src/static/img/crushed_current_location.png
diff --git a/src/static/img/etherpad_lite_icons.png b/src/static/img/etherpad_lite_icons.png
new file mode 100644
index 00000000..27867d42
Binary files /dev/null and b/src/static/img/etherpad_lite_icons.png differ
diff --git a/static/img/fileicons.gif b/src/static/img/fileicons.gif
similarity index 100%
rename from static/img/fileicons.gif
rename to src/static/img/fileicons.gif
diff --git a/static/img/leftarrow.png b/src/static/img/leftarrow.png
similarity index 100%
rename from static/img/leftarrow.png
rename to src/static/img/leftarrow.png
diff --git a/static/img/loading.gif b/src/static/img/loading.gif
similarity index 100%
rename from static/img/loading.gif
rename to src/static/img/loading.gif
diff --git a/static/img/pause.png b/src/static/img/pause.png
similarity index 100%
rename from static/img/pause.png
rename to src/static/img/pause.png
diff --git a/static/img/play.png b/src/static/img/play.png
similarity index 100%
rename from static/img/play.png
rename to src/static/img/play.png
diff --git a/static/img/roundcorner_left.gif b/src/static/img/roundcorner_left.gif
similarity index 100%
rename from static/img/roundcorner_left.gif
rename to src/static/img/roundcorner_left.gif
diff --git a/static/img/roundcorner_right.gif b/src/static/img/roundcorner_right.gif
similarity index 100%
rename from static/img/roundcorner_right.gif
rename to src/static/img/roundcorner_right.gif
diff --git a/src/static/img/star.png b/src/static/img/star.png
new file mode 100644
index 00000000..e0c7099e
Binary files /dev/null and b/src/static/img/star.png differ
diff --git a/static/img/stepper_buttons.png b/src/static/img/stepper_buttons.png
similarity index 100%
rename from static/img/stepper_buttons.png
rename to src/static/img/stepper_buttons.png
diff --git a/static/img/timeslider_background.png b/src/static/img/timeslider_background.png
similarity index 100%
rename from static/img/timeslider_background.png
rename to src/static/img/timeslider_background.png
diff --git a/static/img/timeslider_left.png b/src/static/img/timeslider_left.png
similarity index 100%
rename from static/img/timeslider_left.png
rename to src/static/img/timeslider_left.png
diff --git a/static/img/timeslider_right.png b/src/static/img/timeslider_right.png
similarity index 100%
rename from static/img/timeslider_right.png
rename to src/static/img/timeslider_right.png
diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js
new file mode 100644
index 00000000..2d523f6a
--- /dev/null
+++ b/src/static/js/AttributeManager.js
@@ -0,0 +1,164 @@
+var Changeset = require('./Changeset');
+var ChangesetUtils = require('./ChangesetUtils');
+var _ = require('./underscore');
+
+var lineMarkerAttribute = 'lmkr';
+
+// If one of these attributes are set to the first character of a
+// line it is considered as a line attribute marker i.e. attributes
+// set on this marker are applied to the whole line.
+// The list attribute is only maintained for compatibility reasons
+var lineAttributes = [lineMarkerAttribute,'list'];
+
+/*
+ The Attribute manager builds changesets based on a document
+ representation for setting and removing range or line-based attributes.
+
+ @param rep the document representation to be used
+ @param applyChangesetCallback this callback will be called
+ once a changeset has been built.
+
+
+ A document representation contains
+ - an array `alines` containing 1 attributes string for each line
+ - an Attribute pool `apool`
+ - a SkipList `lines` containing the text lines of the document.
+*/
+
+var AttributeManager = function(rep, applyChangesetCallback)
+{
+ this.rep = rep;
+ this.applyChangesetCallback = applyChangesetCallback;
+ this.author = '';
+
+ // If the first char in a line has one of the following attributes
+ // it will be considered as a line marker
+};
+
+AttributeManager.lineAttributes = lineAttributes;
+
+AttributeManager.prototype = _(AttributeManager.prototype).extend({
+
+ applyChangeset: function(changeset){
+ if(!this.applyChangesetCallback) return changeset;
+
+ var cs = changeset.toString();
+ if (!Changeset.isIdentity(cs))
+ {
+ this.applyChangesetCallback(cs);
+ }
+
+ return changeset;
+ },
+
+ /*
+ Sets attributes on a range
+ @param start [row, col] tuple pointing to the start of the range
+ @param end [row, col] tuple pointing to the end of the range
+ @param attribute: an array of attributes
+ */
+ setAttributesOnRange: function(start, end, attribs)
+ {
+ var builder = Changeset.builder(this.rep.lines.totalWidth());
+ ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, start);
+ ChangesetUtils.buildKeepRange(this.rep, builder, start, end, attribs, this.rep.apool);
+ return this.applyChangeset(builder);
+ },
+
+ /*
+ Returns if the line already has a line marker
+ @param lineNum: the number of the line
+ */
+ lineHasMarker: function(lineNum){
+ var that = this;
+
+ return _.find(lineAttributes, function(attribute){
+ return that.getAttributeOnLine(lineNum, attribute) != '';
+ }) !== undefined;
+ },
+
+ /*
+ Gets a specified attribute on a line
+ @param lineNum: the number of the line to set the attribute for
+ @param attributeKey: the name of the attribute to get, e.g. list
+ */
+ getAttributeOnLine: function(lineNum, attributeName){
+ // get `attributeName` attribute of first char of line
+ var aline = this.rep.alines[lineNum];
+ if (aline)
+ {
+ var opIter = Changeset.opIterator(aline);
+ if (opIter.hasNext())
+ {
+ return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || '';
+ }
+ }
+ return '';
+ },
+
+ /*
+ Sets a specified attribute on a line
+ @param lineNum: the number of the line to set the attribute for
+ @param attributeKey: the name of the attribute to set, e.g. list
+ @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
+
+ */
+ setAttributeOnLine: function(lineNum, attributeName, attributeValue){
+ var loc = [0,0];
+ var builder = Changeset.builder(this.rep.lines.totalWidth());
+ var hasMarker = this.lineHasMarker(lineNum);
+
+ ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
+
+ if(hasMarker){
+ ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
+ [attributeName, attributeValue]
+ ], this.rep.apool);
+ }else{
+ // add a line marker
+ builder.insert('*', [
+ ['author', this.author],
+ ['insertorder', 'first'],
+ [lineMarkerAttribute, '1'],
+ [attributeName, attributeValue]
+ ], this.rep.apool);
+ }
+
+ return this.applyChangeset(builder);
+ },
+
+ /*
+ Removes a specified attribute on a line
+ @param lineNum: the number of the affected line
+ @param attributeKey: the name of the attribute to remove, e.g. list
+
+ */
+ removeAttributeOnLine: function(lineNum, attributeName, attributeValue){
+
+ var loc = [0,0];
+ var builder = Changeset.builder(this.rep.lines.totalWidth());
+ var hasMarker = this.lineHasMarker(lineNum);
+
+ if(hasMarker){
+ ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
+ ChangesetUtils.buildRemoveRange(this.rep, builder, loc, (loc = [lineNum, 1]));
+ }
+
+ return this.applyChangeset(builder);
+ },
+
+ /*
+ Sets a specified attribute on a line
+ @param lineNum: the number of the line to set the attribute for
+ @param attributeKey: the name of the attribute to set, e.g. list
+ @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
+ */
+ toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) {
+ return this.getAttributeOnLine(attributeName) ?
+ this.removeAttributeOnLine(lineNum, attributeName) :
+ this.setAttributeOnLine(lineNum, attributeName, attributeValue);
+
+ }
+});
+
+module.exports = AttributeManager;
\ No newline at end of file
diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js
new file mode 100644
index 00000000..f5990c07
--- /dev/null
+++ b/src/static/js/AttributePool.js
@@ -0,0 +1,96 @@
+/**
+ * This code represents the Attribute Pool Object of the original Etherpad.
+ * 90% of the code is still like in the original Etherpad
+ * Look at https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
+ * You can find a explanation what a attribute pool is here:
+ * https://github.com/Pita/etherpad-lite/blob/master/doc/easysync/easysync-notes.txt
+ */
+
+/*
+ * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ An AttributePool maintains a mapping from [key,value] Pairs called
+ Attributes to Numbers (unsigened integers) and vice versa. These numbers are
+ used to reference Attributes in Changesets.
+*/
+
+var AttributePool = function () {
+ this.numToAttrib = {}; // e.g. {0: ['foo','bar']}
+ this.attribToNum = {}; // e.g. {'foo,bar': 0}
+ this.nextNum = 0;
+};
+
+AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) {
+ var str = String(attrib);
+ if (str in this.attribToNum) {
+ return this.attribToNum[str];
+ }
+ if (dontAddIfAbsent) {
+ return -1;
+ }
+ var num = this.nextNum++;
+ this.attribToNum[str] = num;
+ this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
+ return num;
+};
+
+AttributePool.prototype.getAttrib = function (num) {
+ var pair = this.numToAttrib[num];
+ if (!pair) {
+ return pair;
+ }
+ return [pair[0], pair[1]]; // return a mutable copy
+};
+
+AttributePool.prototype.getAttribKey = function (num) {
+ var pair = this.numToAttrib[num];
+ if (!pair) return '';
+ return pair[0];
+};
+
+AttributePool.prototype.getAttribValue = function (num) {
+ var pair = this.numToAttrib[num];
+ if (!pair) return '';
+ return pair[1];
+};
+
+AttributePool.prototype.eachAttrib = function (func) {
+ for (var n in this.numToAttrib) {
+ var pair = this.numToAttrib[n];
+ func(pair[0], pair[1]);
+ }
+};
+
+AttributePool.prototype.toJsonable = function () {
+ return {
+ numToAttrib: this.numToAttrib,
+ nextNum: this.nextNum
+ };
+};
+
+AttributePool.prototype.fromJsonable = function (obj) {
+ this.numToAttrib = obj.numToAttrib;
+ this.nextNum = obj.nextNum;
+ this.attribToNum = {};
+ for (var n in this.numToAttrib) {
+ this.attribToNum[String(this.numToAttrib[n])] = Number(n);
+ }
+ return this;
+};
+
+
+module.exports = AttributePool;
\ No newline at end of file
diff --git a/static/js/Changeset.js b/src/static/js/Changeset.js
similarity index 99%
rename from static/js/Changeset.js
rename to src/static/js/Changeset.js
index 81c0c81b..738ee1ba 100644
--- a/static/js/Changeset.js
+++ b/src/static/js/Changeset.js
@@ -25,7 +25,7 @@
* limitations under the License.
*/
-var AttributePoolFactory = require("/AttributePoolFactory");
+var AttributePool = require("./AttributePool");
var _opt = null;
@@ -1731,7 +1731,7 @@ exports.appendATextToAssembler = function (atext, assem) {
* @param pool {AtributePool}
*/
exports.prepareForWire = function (cs, pool) {
- var newPool = AttributePoolFactory.createAttributePool();;
+ var newPool = new AttributePool();
var newCs = exports.moveOpsToNewPool(cs, pool, newPool);
return {
translated: newCs,
diff --git a/src/static/js/ChangesetUtils.js b/src/static/js/ChangesetUtils.js
new file mode 100644
index 00000000..e0b67881
--- /dev/null
+++ b/src/static/js/ChangesetUtils.js
@@ -0,0 +1,60 @@
+/**
+ * This module contains several helper Functions to build Changesets
+ * based on a SkipList
+ */
+
+/**
+ * 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.
+ */
+exports.buildRemoveRange = function(rep, builder, start, end)
+{
+ var startLineOffset = rep.lines.offsetOfIndex(start[0]);
+ var endLineOffset = rep.lines.offsetOfIndex(end[0]);
+
+ if (end[0] > start[0])
+ {
+ builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]);
+ builder.remove(end[1]);
+ }
+ else
+ {
+ builder.remove(end[1] - start[1]);
+ }
+}
+
+exports.buildKeepRange = function(rep, builder, start, end, attribs, pool)
+{
+ var startLineOffset = rep.lines.offsetOfIndex(start[0]);
+ var endLineOffset = rep.lines.offsetOfIndex(end[0]);
+
+ if (end[0] > start[0])
+ {
+ builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool);
+ builder.keep(end[1], 0, attribs, pool);
+ }
+ else
+ {
+ builder.keep(end[1] - start[1], 0, attribs, pool);
+ }
+}
+
+exports.buildKeepToStartOfRange = function(rep, builder, start)
+{
+ var startLineOffset = rep.lines.offsetOfIndex(start[0]);
+
+ builder.keep(startLineOffset, start[0]);
+ builder.keep(start[1]);
+}
+
diff --git a/static/js/ace.js b/src/static/js/ace.js
similarity index 80%
rename from static/js/ace.js
rename to src/static/js/ace.js
index 22d4eaa6..26d6c0eb 100644
--- a/static/js/ace.js
+++ b/src/static/js/ace.js
@@ -28,7 +28,8 @@ Ace2Editor.registry = {
nextId: 1
};
-var plugins = require('/plugins').plugins;
+var hooks = require('./pluginfw/hooks');
+var _ = require('./underscore');
function Ace2Editor()
{
@@ -70,7 +71,7 @@ function Ace2Editor()
function doActionsPendingInit()
{
- $.each(actionsPendingInit, function(i,fn){
+ _.each(actionsPendingInit, function(fn,i){
fn()
});
actionsPendingInit = [];
@@ -87,7 +88,7 @@ function Ace2Editor()
'setUserChangeNotificationCallback', 'setAuthorInfo',
'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange'];
- $.each(aceFunctionsPendingInit, function(i,fnName){
+ _.each(aceFunctionsPendingInit, function(fnName,i){
var prefix = 'ace_';
var name = prefix + fnName;
editor[fnName] = pendingInit(function(){
@@ -156,28 +157,38 @@ function Ace2Editor()
}
function pushRequireScriptTo(buffer) {
var KERNEL_SOURCE = '../static/js/require-kernel.js';
- var KERNEL_BOOT = 'require.setRootURI("../minified/");\nrequire.setGlobalKeyPath("require");'
+ var KERNEL_BOOT = '\
+require.setRootURI("../javascripts/src");\n\
+require.setLibraryURI("../javascripts/lib");\n\
+require.setGlobalKeyPath("require");\n\
+';
if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) {
buffer.push('\
');
- pushScriptsTo(iframeHTML);
+
+ iframeHTML.push('
+
+
+
+
+