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(' + + + + +
    + + <% if (errors.length) { %> +
    + <% errors.forEach(function (item) { %> +
    <%= item.toString() %>
    + <% }) %> +
    + <% } %> + + +

    Etherpad Lite

    +
    +

    Installed plugins

    + + + + + + + + + + + + + + + + + +
    NameDescription
    + +
    + +
    +
    +

    Search for plugins to install

    +
    + + +
    + + + + + + + + + + + + + + + + + +
    NameDescription
    + +
    + + .. of . + +
    + +
    +

    + Please wait: + +

    +
    +
    +
    + + diff --git a/static/index.html b/src/templates/index.html similarity index 98% rename from static/index.html rename to src/templates/index.html index 58f68801..4a45d6a5 100644 --- a/static/index.html +++ b/src/templates/index.html @@ -6,6 +6,8 @@ + + + <% e.end_block(); %> -
    -
    +
    -
    +
    -
    +
    @@ -101,9 +150,10 @@
    Loading...
    -