diff --git a/.gitignore b/.gitignore index 31b7d2ea9..4f3152245 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 000000000..72c456447 --- /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 000000000..74bd365b4 --- /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 000000000..4ec8e3924 --- /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 000000000..718fb095c --- /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 000000000..e221b5c18 --- /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 000000000..c3f1fc3eb --- /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 000000000..22d58cc2f --- /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 000000000..9e7fc5511 --- /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 356b07799..a46c18140 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 c5dc535cd..ec792717e 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 01197a6b8..f90009d05 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 270ec98cd..2acebd82e 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 c409920e7..82e89a946 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 c5377d81b..000000000 --- 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 94a60fd40..f89fcd8ed 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 61f14dcea..35b54d8da 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 000000000..6bc777350 --- /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 09cc95afc..37fd3f161 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 9baf63475..f644de121 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 04c79cfae..bd19507ff 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 40875effb..b4a39c17e 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 e5dab99b4..343406300 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 33ab37d44..a092453ad 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 c5af33c68..ec4948a6f 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 8e7398bea..374e949fd 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 000000000..6a2cc4bab --- /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 000000000..daee5f8e8 --- /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 000000000..2d02a45a6 --- /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 a7f66151c..98b1ed165 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 100% rename from node/handler/ImportHandler.js rename to src/node/handler/ImportHandler.js 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 48f0aa988..4a570e219 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); + } + } }); } @@ -185,6 +192,11 @@ exports.handleMessage = function(client, message) { handleChatMessage(client, message); } + else if(message.type == "COLLABROOM" && + message.data.type == "SAVE_REVISION") + { + handleSaveRevisionMessage(client, message); + } else if(message.type == "COLLABROOM" && message.data.type == "CLIENT_MESSAGE" && message.data.payload.type == "suggestUserName") @@ -198,6 +210,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 @@ -367,7 +396,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; @@ -564,8 +593,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); @@ -737,9 +770,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"}); } } } @@ -800,9 +834,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 188068430..5556efa1e 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 000000000..7b21206c9 --- /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 000000000..48d507224 --- /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 000000000..cb8c58987 --- /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 000000000..9e78f34d7 --- /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 000000000..60ece0add --- /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 000000000..4f5dd7a5d --- /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 000000000..6774b653a --- /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 000000000..474f475ec --- /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 000000000..f284e4783 --- /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 000000000..028d8ab1b --- /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 000000000..a3d1df332 --- /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 000000000..4eb38ea7a --- /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 abe6d3471..bcb211081 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 afeafd3a9..91ebe59f7 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 ce8663697..4b50b0326 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 39c6ceb3f..c59965653 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 0d30fb688..e60446df4 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 95% rename from node/utils/caching_middleware.js rename to src/node/utils/caching_middleware.js index a26e22d18..70d5a08c4 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); } 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 000000000..4c1bba244 --- /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 e922dddeb..461e51979 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,12 +37,13 @@ , "broadcast.js" , "broadcast_slider.js" , "broadcast_revisions.js" - , "timeslider.js" ] , "ace2_inner.js": [ - "ace2_common.js" - , "AttributePoolFactory.js" + "ace2_inner.js" + , "underscore.js" + , "AttributePool.js" , "Changeset.js" + , "ChangesetUtils.js" , "security.js" , "skiplist.js" , "virtual_lines.js" @@ -65,6 +54,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 01eb8e96a..c46abbbf6 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 000000000..5eb008fa0 --- /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 100% rename from static/css/iframe_editor.css rename to src/static/css/iframe_editor.css diff --git a/src/static/css/pad.css b/src/static/css/pad.css new file mode 100644 index 000000000..40089bbfb --- /dev/null +++ b/src/static/css/pad.css @@ -0,0 +1,1514 @@ +*, +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 { + position: absolute; + z-index: 500; + background-color: #000; + background-color: rgba(0,0,0,0.7); + width: 160px; + right: 20px; + top: 40px; + color: #fff; + padding: 5px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} +#otherusers { + max-height: 400px; + overflow: auto; +} +a img { + border: 0 +} +/* menu */ +.toolbar ul { + position: relative; + list-style: none; + padding-right: 3px; + padding-left: 1px; + z-index: 2; + overflow: hidden; +} +.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: 3px; + width: 100%; + white-space: nowrap; + height: 32px; +} +.toolbar ul li { + 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; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + cursor: pointer; + float: left; + height: 18px; + margin-left: 2px; + overflow: hidden; + padding: 4px 5px; + width: 18px; +} +.toolbar ul li a { + text-decoration: none; + color: #ccc; + position: absolute; +} +.toolbar ul li a span { + position: relative; + top: -2px; +} +.toolbar ul li: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: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.separator { + border: inherit; + background: inherit; + visibility: hidden; + width: 0px; +} +.toolbar ul li a { + display: block +} +.toolbar ul li a img { + padding: 1px +} +.toolbar ul { + float: left +} +.toolbar ul.menu_right { + float: right +} +#users { + display: none +} +#editorcontainer { + position: absolute; + width: 100%; + top: 36px; + 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%; +} +.maximized #padpage { + left: 8px; + right: 8px; + width: auto; + margin-left: 0; +} +body.fullwidth #padpage { + width: auto; + margin-left: 6px; + margin-right: 6px; +} +body.squish1width #padpage { + width: 800px +} +body.squish2width #padpage { + width: 700px +} +a#backtoprosite, +#accountnav { + display: block; + position: absolute; + height: 15px; + line-height: 15px; + width: auto; + top: 5px; + font-size: 1.2em; + display: none; +} +a#backtoprosite, +#accountnav a { + color: #cde7ff; + text-decoration: underline; +} +a#backtoprosite { + padding-left: 20px; + left: 6px; + background: url(static/img/protop.gif) no-repeat -5px -6px; +} +#accountnav { + right: 30px; + color: #fff; +} +#specialkeyarea { + top: 5px; + left: 250px; + color: yellow; + font-weight: bold; + font-size: 1.5em; + position: absolute; +} +#alertbar { + margin-top: 6px; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + opacity: 0; + display: none; + position: absolute; + left: 0; + right: 0; + z-index: 53; +} +#servermsg { + position: relative; + zoom: 1; + border: 1px solid #992; + background: #ffc; + padding: 0.8em; + font-size: 1.2em; +} +#servermsg h3 { + font-weight: bold; + margin-right: 10px; + margin-bottom: 1em; + float: left; + width: auto; +} +#servermsg #servermsgdate { + font-style: italic; + font-weight: normal; + color: #666; +} +a#hidetopmsg { + position: absolute; + right: 5px; + bottom: 5px; +} +#shuttingdown { + position: relative; + zoom: 1; + border: 1px solid #992; + background: #ffc; + padding: 0.6em; + font-size: 1.2em; + margin-top: 6px; +} +#docbar { + margin-top: 6px; + height: 25px; + position: relative; + zoom: 1; + background: #fbfbfb url(static/img/padtopback2.gif) repeat-x 0 -31px; +} +.docbarbutton { + padding-top: 2px; + padding-bottom: 2px; + padding-left: 4px; + padding-right: 4px; + border-left: 1px solid #CCC; + white-space: nowrap; +} +.docbarbutton img { + border: 0px; + width: 13px; + margin-right: 2px; + vertical-align: middle; + margin-top: 3px; + margin-bottom: 2px; +} +.menu, +.menu ul { + font-size: 10pt; + list-style-type: none; +} +.menu ul { + padding-left: 20px +} +.menu a { + font-size: 10px; + line-height: 18px; + text-decoration: none; + color: #444; + font-weight: bold; +} +.docbarbutton.highlight { + background-color: #fef2bd; + border: 1px solid #CCC; + border-right: 0px; +} +#docbarleft { + position: absolute; + left: 0; + top: 0; + height: 100%; + overflow: hidden; + background: url(static/img/padtop5.gif) no-repeat left -31px; + width: 7px; +} +#docbarpadtitle { + position: absolute; + height: auto; + left: 9px; + width: 280px; + font-size: 1.6em; + color: #444; + font-weight: normal; + line-height: 22px; + margin-left: 2px; + height: 22px; + top: 2px; + overflow: hidden; /*not supported in FF*/ + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; +} +.docbar-public #docbarpadtitle { + padding-left: 22px; + background: url(static/img/public.gif) no-repeat left center; +} +#docbarrenamelink { + position: absolute; + top: 6px; + font-size: 1.1em; + display: none; +} +#docbarrenamelink a { + color: #999 +} +#docbarrenamelink a:hover { + color: #48d +} +#padtitlebuttons { + position: absolute; + width: 74px; + zoom: 1; + height: 17px; + top: 4px; + left: 170px; + display: none; + background: url(static/img/ok_or_cancel.gif) 0px 0px; +} +#padtitlesave { + position: absolute; + display: block; + height: 0; + padding-top: 17px; + overflow: hidden; + width: 23px; + left: 0; + top: 0; +} +#padtitlecancel { + position: absolute; + display: block; + height: 0; + padding-top: 17px; + overflow: hidden; + width: 35px; + right: 0; + top: 0; +} +#padtitleedit { + position: absolute; + top: 2px; + left: 5px; + height: 15px; + padding: 2px; + font-size: 1.4em; + background: white; + border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; + border-bottom: 1px solid #e6e6e6; + width: 150px; + display: none; +} +#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; +} +.hidesidebar #padeditor { + right: 0 +} +#vdraggie { + /* background: url(static/img/vdraggie.gif) no-repeat top center;;*/ + width: 16px; + height: 16px; + background-image: url('../../static/img/etherpad_lite_icons.png'); + background-repeat: no-repeat; + background-position: 0px -300px; + cursor: W-resize; + bottom: 0; + position: absolute; + right: 268px; + top: 0; + width: 56px; + z-index: 10; +} +.toolbarsavetable { + position: absolute; + top: 6px; + right: 8px; + height: 24px; +} +.toolbarsavetable td, +.toolbartable td { + white-space: nowrap +} +#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; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + background: rgba(0, 0, 0, 0.7); + padding-left: 10px; + padding-top: 10px; +} +/* +#mycolorpicker ul li + { + +float: left; + +} + +#mycolorpicker .picked { + border: 1px solid #000 !important; + } + + +#mycolorpicker .picked .pickerswatch { + border: 1px solid #fff; + } + + */ +#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 +} +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: #fff; +} +#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; +} +/* We give docbar a higher z-index than its descendant impexp-wrapper in +order to allow the Import/Export panel to be on top of stuff lower +down on the page in IE. Strange but it works! */ +#docbar { + z-index: 52 +} +#impexp-wrapper { + width: 650px; + right: 10px; +} +#impexp-panel { + height: 160px +} +.docbarimpexp-closing #impexp-wrapper { + z-index: 50 +} +#savedrevs-wrapper { + width: 100%; + left: 0; +} +#savedrevs-panel { + height: 79px +} +.docbarsavedrevs-closing #savedrevs-wrapper { + z-index: 50 +} +#savedrevs-wrapper .dbpanel-rightedge { + background-position: 0 -10px +} +#options-wrapper { + width: 340px; + right: 200px; +} +#options-panel { + height: 114px +} +.docbaroptions-closing #options-wrapper { + z-index: 50 +} +#security-wrapper { + width: 320px; + right: 300px; +} +#security-panel { + height: 130px +} +.docbarsecurity-closing #security-wrapper { + z-index: 50 +} +#revision-notifier { + position: absolute; + right: 8px; + top: 25px; + width: auto; + height: auto; + font-size: 1.2em; + background: #ffc; + border: 1px solid #aaa; + color: #444; + padding: 3px 5px; + display: none; + z-index: 55; +} +#revision-notifier .label { + color: #777; + font-weight: bold; +} +#mainmodals { + z-index: 600; /* higher than the modals themselves: */ +} +.modalfield { + font-size: 1.2em; + padding: 1px; + border: 1px solid #bbb; +} +#mainmodals .editempty { + color: #aaa +} +.expand-collapse { + height: 22px; + background-image: url(static/img/sharedistri.gif); + background-repeat: no-repeat; + background-position: 0 3px; + padding-left: 17px; + text-decoration: none; +} +.expand-collapse.expanded { + background-position: 0 -31px +} +.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; +} +.sharebox-url { + width: 440px; + height: 18px; + text-align: left; + font-size: 1.3em; + line-height: 18px; + padding: 2px; +} +#viewbarcontents { + display: none +} +#viewzoomtitle { + position: absolute; + left: 10px; + top: 4px; + height: 20px; + line-height: 20px; + width: auto; +} + +#bottomarea { + height: 28px; + overflow: hidden; + position: absolute; + height: 28px; + bottom: 0px; + left: 0px; + right: 0px; + font-size: 1.2em; + color: #444; +} +#widthprefcheck { + position: absolute; + background-image: url(static/img/layoutbuttons.gif); + background-repeat: no-repeat; + cursor: pointer; + width: 86px; + height: 20px; + top: 4px; + right: 2px; +} +.widthprefunchecked { + background-position: -1px -1px +} +.widthprefchecked { + background-position: -1px -23px +} +#sidebarcheck { + position: absolute; + background-image: url(static/img/layoutbuttons.gif); + background-repeat: no-repeat; + cursor: pointer; + width: 86px; + height: 20px; + top: 4px; + right: 90px; +} +.sidebarunchecked { + background-position: -1px -45px +} +.sidebarchecked { + background-position: -1px -67px +} + +#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 */ +} +a#topbarmaximize { + float: right; + width: 16px; + height: 16px; + margin-right: -143px; + margin-top: 4px; + background: url(static/img/maximize_normal.png); +} +.maximized a#topbarmaximize { + background: url(static/img/maximize_maximized.png) +} +.toolbarinner h1 { + line-height: 29px; + font-size: 16px; + padding-left: 6pt; + margin-top: 0; + white-space: nowrap; +} +.toolbarinner h1 a { + font-size: 12px +} +.bigbutton { + display: block; + background-color: #a3bde0; + color: #555555; + border-style: solid; + border-width: 2px; + border-left-color: #d6e2f1; + border-right-color: #86aee1; + border-top-color: #d6e2f1; + border-bottom-color: #86aee1; + margin: 10pt; + text-align: center; + text-decoration: none; + padding: 50pt; + font-size: 20pt; + -webkit-border-radius: 3pt; + -moz-border-radius: 3pt; + border-radius: 3pt; +} +.modaldialog .bigbutton { + padding-left: 0; + padding-right: 0; + width: 100%; +} +ul#colorpickerswatches { + padding-left: 3px; + padding-top: 5px; +} +ul#colorpickerswatches li { + border: 1px solid #ccc; + width: 14px; + height: 14px; + overflow: hidden; + margin: 3px 6px; + padding: 0px; +} +ul#colorpickerswatches li:hover { + border: 1px solid #000; + cursor: pointer; +} +#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: #555; + font-size: 9px; + 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: 2px; + background-repeat: no-repeat; + padding-left: 25px; + background-image: url("../../static/img/etherpad_lite_icons.png"); + color: #fff; + text-decoration: none; +} +#importexportline { + border-left: 1px solid #fff; + height: 190px; + position: absolute; + width: 0px; + left: 260px; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; + filter: alpha(opacity=80); + opacity: .8; +} +.impexpbutton { + background-image: -webkit-linear-gradient(center top , #EEEEEE, white 20%, white 20%); + background-image: -moz-linear-gradient(center top , #EEEEEE, white 20%, white 20%); + background-image: -o-linear-gradient(center top , #EEEEEE, white 20%, white 20%); + background-image: -ms-linear-gradient(center top , #EEEEEE, white 20%, white 20%); + background-image: linear-gradient(center top , #EEEEEE, white 20%, white 20%); + padding: 3px; +} +#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 +} +#exportwordle { + background-position: 0px -323px +} +#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; + margin-left: 1px; + margin-top: 1px; +} +.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; + display: inline-block; + vertical-align: middle; + margin: 0 !important; +} +.buttonicon-showusers { + background-position: 0px -183px; + display: inline-block; +} +.buttonicon-savedRevision { + background-position: 0px -493px +} +#usericon { + width: 33px !important +} +#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; + font-size: 11px; + line-height: 18px; + position: fixed; +} +.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; + z-index: 500; + padding: 10px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + background: #222; + background: rgba(0,0,0,.7); + background: -webkit-linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,.7) 35px, rgba(0,0,0,.6)); + background: -moz-linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,.7) 35px, rgba(0,0,0,.6)); + background: -o-linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,.7) 35px, rgba(0,0,0,.6)); + background: -ms-linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,.7) 35px, rgba(0,0,0,.6)); + background: linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,.7) 35px, rgba(0,0,0,.6)); + -webkit-box-shadow: 0 0 8px #888; + -moz-box-shadow: 0 0 8px #888; + box-shadow: 0 0 8px #888; + color: #fff; +} +.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 a { + text-decoration: none +} +.popup h1 { + font-size: 18px +} +.popup h2 { + font-size: 15px +} +.popup p { + margin: 5px 0 +} +.column { + float: left; + width: 50%; +} +#settings, +#importexport, +#embed { + position: absolute; + top: 55px; + right: 20px; + display: none; +} +.note { + color: #ddd; + font-size: 11px; + font-weight: bold; +} +.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; +} +.stickyChat { + background-color: #f1f1f1 !important; + right: 0px !important; + top: 36px; + -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 { + padding: 4px 1px + } +} +@media only screen and (min-device-width: 320px) and (max-device-width: 720px) { + .toolbar ul li { + padding: 4px 3px + } + #users { + right: 0; + top: 36px; + bottom: 33px; + -webkit-border-radius: none; + -moz-border-radius: none; + border-radius: none; + } + #mycolorpicker { + left: -72px; + /* #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 { + height: 24px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + margin-top: 0; + border: 0; + float: right; + } + #chaticon { + bottom: 3px; + right: 55px; + border-right: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + 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: 0; + } + #chatbox { + bottom: 32px; + right: 0; + border-top-right-radius: 0; + border-right: none; + } + .toolbar ul li a span { + top: -3px + } + #usericonback { + margin-top: 4px + } + .toolbar ul.menu_right li:not(:last-child) { + display: block + } + .toolbar ul.menu_right > li { + background: none; + border: none; + margin-top: 4px; + padding: 4px 8px; + } + .selected { + background: none !important + } + #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; + } + .separator { + display: none + } + #online_count { + line-height: 24px + } +} \ No newline at end of file diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css new file mode 100644 index 000000000..4c8913d38 --- /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 000000000..aae16bb24 --- /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 000000000..27867d428 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 000000000..e0c7099e5 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 000000000..2d523f6aa --- /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 000000000..f5990c07d --- /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 81c0c81b2..738ee1bab 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 000000000..e0b67881f --- /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 79% rename from static/js/ace.js rename to src/static/js/ace.js index 22d4eaa6e..8cb9f1884 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(''); - // For compatability's sake transform in and out. - for (var i = 0, ii = iframeHTML.length; i < ii; i++) { - iframeHTML[i] = JSON.stringify(iframeHTML[i]); - } - plugins.callHook("aceInitInnerdocbodyHead", { + hooks.callAll("aceInitInnerdocbodyHead", { iframeHTML: iframeHTML }); - for (var i = 0, ii = iframeHTML.length; i < ii; i++) { - iframeHTML[i] = JSON.parse(iframeHTML[i]); - } // calls to these functions ($$INCLUDE_...) are replaced when this file is processed // and compressed, putting the compressed code from the named file directly into the @@ -248,21 +253,34 @@ function Ace2Editor() $$INCLUDE_CSS("../static/css/iframe_editor.css"); $$INCLUDE_CSS("../static/css/pad.css"); $$INCLUDE_CSS("../static/custom/pad.css"); + + var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path }); + includedCSS = includedCSS.concat(additionalCSS); + pushStyleTagsFor(iframeHTML, includedCSS); var includedJS = []; - var $$INCLUDE_JS = function(filename) {includedJS.push(filename)}; pushRequireScriptTo(iframeHTML); + pushScriptsTo(iframeHTML); + // Inject my plugins into my child. iframeHTML.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 99% rename from static/index.html rename to src/templates/index.html index 58f688017..ecfa0d9e0 100644 --- a/static/index.html +++ b/src/templates/index.html @@ -130,6 +130,7 @@ + - - + - - \ No newline at end of file + <% e.end_block(); %> + diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html new file mode 100644 index 000000000..8331062f5 --- /dev/null +++ b/src/templates/timeslider.html @@ -0,0 +1,164 @@ + + + + + + + Etherpad Lite Timeslider + + + + + + + +
    +
    +
    +
    +

    Server Notice:

    hide +

    +
    +
    + + + + +
    + +
    +
    + +
    + +
    +
    + + Return to pad +
    + +
    +

    + + +

    +

    Authors: + + No Authors +

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    + + +
    + + +
    + + + + + + + + + + + diff --git a/static/css/pad.css b/static/css/pad.css deleted file mode 100644 index 969d00276..000000000 --- a/static/css/pad.css +++ /dev/null @@ -1,1270 +0,0 @@ -*,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 -{ - position: absolute; - z-index:500; - background-color: #000; - background-color: rgba(0,0,0,0.7); - width: 160px; - right: 20px; - top: 40px; - color: #fff; - padding: 5px; - border-radius: 6px; -} - -a img -{ - border: 0; -} - -/* menu */ -#editbar ul -{ - position: relative; - list-style: none; - padding-right: 3px; - padding-left: 1px; - z-index: 2; - overflow: hidden; - -} - -#editbar -{ - background: #f7f7f7; - background: linear-gradient(#f7f7f7, #f1f1f1 80%); - border-bottom: 1px solid #ccc; - height: 32px; - overflow: hidden; - padding-top: 3px; - width: 100%; -} - -#editbar ul li -{ - background: #fff; - background: linear-gradient(#fff, #f0f0f0); - border: 1px solid #ccc; - border-radius: 4px; - cursor: pointer; - float: left; - height: 18px; - margin-left: 2px; - overflow: hidden; - padding: 4px 5px; - width: 18px; -} - -#editbar ul li a -{ - text-decoration: none; - color: #ccc; - position: absolute; -} - -#editbar ul li a span -{ - position: relative; - top:-2px -} - -#editbar ul li:hover { - background: #fff; - background: linear-gradient(#f4f4f4, #e4e4e4); -} - -#editbar ul li:active { - background: #eee; - background: linear-gradient(#ddd, #fff); - box-shadow: 0 0 8px rgba(0,0,0,.1) inset; -} - -#editbar ul li.separator -{ - border: inherit; - background: inherit; - visibility:hidden; - width: 0px; -} -#editbar ul li a -{ - display: block; -} -#editbar ul li a img -{ - padding: 1px; -} - - -#editbar ul -{ - float: left; -} -#editbar ul#menu_right -{ - float: right; -} - -#users -{ - display: none; -} - -#editorcontainer -{ - position: absolute; - - width: 100%; - - top: 36px; - 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%; -} - -.maximized #padpage { - left: 8px; - right: 8px; - width: auto; - margin-left: 0; -} - -body.fullwidth #padpage { width: auto; margin-left: 6px; margin-right: 6px; } -body.squish1width #padpage { width: 800px; } -body.squish2width #padpage { width: 700px; } - -a#backtoprosite, #accountnav { - display: block; position: absolute; height: 15px; line-height: 15px; - width: auto; top: 5px; font-size: 1.2em; display:none; -} -a#backtoprosite, #accountnav a { color: #cde7ff; text-decoration: underline; } - -a#backtoprosite { padding-left: 20px; left: 6px; - background: url(static/img/protop.gif) no-repeat -5px -6px; } -#accountnav { right: 30px; color: #fff; } - -.propad a#topbaretherpad { background: url(static/img/protop.gif) no-repeat -397px -3px; } - -#specialkeyarea { top: 5px; left: 250px; color: yellow; font-weight: bold; - font-size: 1.5em; position: absolute; } - -#alertbar { - margin-top: 6px; - opacity: 0; - display: none; - position:absolute; - left:0; - right:0; - z-index:53; -} - -#servermsg { position: relative; zoom: 1; border: 1px solid #992; - background: #ffc; padding: 0.8em; font-size: 1.2em; } -#servermsg h3 { font-weight: bold; margin-right: 10px; - margin-bottom: 1em; float: left; width: auto; } -#servermsg #servermsgdate { font-style: italic; font-weight: normal; color: #666; } -a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } - -#shuttingdown { position: relative; zoom: 1; border: 1px solid #992; - background: #ffc; padding: 0.6em; font-size: 1.2em; margin-top: 6px; } - -#docbar { margin-top: 6px; height: 25px; position: relative; zoom: 1; - background: #fbfbfb url(static/img/padtopback2.gif) repeat-x 0 -31px; } - -.docbarbutton -{ - padding-top: 2px; - padding-bottom: 2px; - padding-left: 4px; - padding-right: 4px; - border-left: 1px solid #CCC; - white-space: nowrap; -} - -.docbarbutton img -{ - border: 0px; - width: 13px; - margin-right: 2px; - vertical-align: middle; - margin-top: 3px; - margin-bottom: 2px; -} - -.menu, -.menu ul { - font-size: 10pt; - list-style-type: none; -} - -.menu ul { - padding-left: 20px; -} - -.menu a { - font-size: 10px; - line-height: 18px; - text-decoration: none; - color: #444; - font-weight: bold; -} - -.docbarbutton.highlight -{ - background-color: #fef2bd; - border: 1px solid #CCC; - border-right: 0px; -} - -#docbarleft { position: absolute; left: 0; top: 0; height: 100%; - overflow: hidden; - background: url(static/img/padtop5.gif) no-repeat left -31px; width: 7px; } - - - -#docbarpadtitle { position: absolute; height: auto; left: 9px; - width: 280px; font-size: 1.6em; color: #444; font-weight: normal; - line-height: 22px; margin-left: 2px; height: 22px; top: 2px; - overflow: hidden; text-overflow: ellipsis /*not supported in FF*/; - white-space:nowrap; } -.docbar-public #docbarpadtitle { padding-left: 22px; - background: url(static/img/public.gif) no-repeat left center; } - -#docbarrenamelink { position: absolute; top: 6px; - font-size: 1.1em; display: none; } -#docbarrenamelink a { color: #999; } -#docbarrenamelink a:hover { color: #48d; } -#padtitlebuttons { position: absolute; width: 74px; zoom: 1; - height: 17px; top: 4px; left: 170px; display: none; - background: url(static/img/ok_or_cancel.gif) 0px 0px; } -#padtitlesave { position: absolute; display: block; - height: 0; padding-top: 17px; overflow: hidden; - width: 23px; left: 0; top: 0; } -#padtitlecancel { position: absolute; display: block; - height: 0; padding-top: 17px; overflow: hidden; - width: 35px; right: 0; top: 0; } -#padtitleedit { position: absolute; top: 2px; left: 5px; - height: 15px; padding: 2px; font-size: 1.4em; - background: white; border-left: 1px solid #c3c3c3; - border-top: 1px solid #c3c3c3; - border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; - width: 150px; display: none; -} - -#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; -} -.hidesidebar #padeditor { right: 0; } - -#vdraggie { -/* background: url(static/img/vdraggie.gif) no-repeat top center;*/ - width:16px; - height:16px; - background-image:url('../../static/img/etherpad_lite_icons.png'); - background-repeat: no-repeat; - background-position: 0px -300px; - - cursor: W-resize; - bottom:0; - position:absolute; - right:268px; - top:0; - width:56px; - z-index: 10; -} - -#editbarsavetable -{ - position:absolute; - top: 6px; - right: 8px; - height: 24px; -} - -#editbarsavetable td, #editbartable td -{ - white-space: nowrap; -} - -#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: 5px; - background: rgba(0, 0, 0, 0.7); - padding-left:10px; - padding-top:10px; -} -/* -#mycolorpicker ul li -{ - float: left; -} -#mycolorpicker .picked { border: 1px solid #000 !important; } - -#mycolorpicker .picked .pickerswatch { border: 1px solid #fff; } -*/ -#mycolorpickersave { - left: 10px; - font-weight: bold; -} - -#mycolorpickercancel { - left: 85px; -} - -#mycolorpickersave, #mycolorpickercancel { - background: #fff; - background: linear-gradient(#fff, #ccc); - border: 1px solid #ccc; - 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; - 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; } - -table#otheruserstable { display: none; } -#nootherusers { padding: 10px; font-size: 1.2em; color: #eee; font-weight: bold;} -#nootherusers a { color: #3C88FF; } - -#otheruserstable td { - border-top: 1px solid #555; - height: 26px; - vertical-align: middle; - padding: 0 2px; - color: #fff; -} - -#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; -} - -/* We give docbar a higher z-index than its descendant impexp-wrapper in - order to allow the Import/Export panel to be on top of stuff lower - down on the page in IE. Strange but it works! */ -#docbar { z-index: 52; } - -#impexp-wrapper { width: 650px; right: 10px; } -#impexp-panel { height: 160px; } -.docbarimpexp-closing #impexp-wrapper { z-index: 50; } - -#savedrevs-wrapper { width: 100%; left: 0; } -#savedrevs-panel { height: 79px; } -.docbarsavedrevs-closing #savedrevs-wrapper { z-index: 50; } -#savedrevs-wrapper .dbpanel-rightedge { background-position: 0 -10px; } - -#options-wrapper { width: 340px; right: 200px; } -#options-panel { height: 114px; } -.docbaroptions-closing #options-wrapper { z-index: 50; } - -#security-wrapper { width: 320px; right: 300px; } -#security-panel { height: 130px; } -.docbarsecurity-closing #security-wrapper { z-index: 50; } - -#revision-notifier { position: absolute; right: 8px; top: 25px; - width: auto; height: auto; font-size: 1.2em; background: #ffc; - border: 1px solid #aaa; color: #444; padding: 3px 5px; - display: none; z-index: 55; } -#revision-notifier .label { color: #777; font-weight: bold; } - -#mainmodals { z-index: 600; /* higher than the modals themselves - so that modals are on top in IE */ } -.modalfield { font-size: 1.2em; padding: 1px; border: 1px solid #bbb;} -#mainmodals .editempty { color: #aaa; } - -.expand-collapse { - height: 22px; - background-image: url(static/img/sharedistri.gif); - background-repeat: no-repeat; - background-position: 0 3px; - padding-left: 17px; - text-decoration: none; -} -.expand-collapse.expanded { - background-position: 0 -31px; -} - - -.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; -} - -.nonprouser #sharebox-stripe { display: none; } - -.sharebox-url { - width: 440px; height: 18px; - text-align: left; - font-size: 1.3em; - line-height: 18px; - padding: 2px; -} - -#sharebox-send { - float: right; - background-repeat: no-repeat; - background-image: url(static/img/sharebox4.gif); - display: block; - width: 87px; height: 22px; - background-position: -383px -289px; -} - - -#viewbarcontents { display: none; } -#viewzoomtitle { - position: absolute; left: 10px; top: 4px; height: 20px; line-height: 20px; - width: auto; -} -#viewzoommenu { - width: 65px; -} -#bottomarea { - height: 28px; - overflow: hidden; - position: absolute; - height: 28px; - bottom: 0px; - left: 0px; - right: 0px; - font-size: 1.2em; - color: #444; -} -#widthprefcheck { position: absolute; - background-image: url(static/img/layoutbuttons.gif); - background-repeat: no-repeat; cursor: pointer; - width: 86px; height: 20px; top: 4px; right: 2px; } -.widthprefunchecked { background-position: -1px -1px; } -.widthprefchecked { background-position: -1px -23px; } -#sidebarcheck { position: absolute; - background-image: url(static/img/layoutbuttons.gif); - background-repeat: no-repeat; cursor: pointer; - width: 86px; height: 20px; top: 4px; right: 90px; } -.sidebarunchecked { background-position: -1px -45px; } -.sidebarchecked { background-position: -1px -67px; } -#feedbackbutton { display: block; position: absolute; width: 68px; - height: 0; padding-top: 17px; overflow: hidden; - background: url(static/img/bottomareagfx.gif); - top: 5px; right: 220px; -} - -#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+ */ - opacity: 1; /* in case this is looked at */ - background-image: none; - background-repeat: no-repeat; - /* scale the image */ -} - -a#topbarmaximize { - float: right; - width: 16px; - height: 16px; - margin-right:-143px; - margin-top:4px; - background: url(static/img/maximize_normal.png); -} - -.maximized a#topbarmaximize { - background: url(static/img/maximize_maximized.png); -} - -#editbarinner h1 { - line-height: 29px; - font-size: 16px; - padding-left: 6pt; - margin-top: 0; -} - -#editbarinner h1 a { - font-size: 12px; -} - -.bigbutton { - display: block; - background-color: #a3bde0; - color: #555555; - border-style: solid; - border-width: 2px; - border-left-color: #d6e2f1; - border-right-color: #86aee1; - border-top-color: #d6e2f1; - border-bottom-color: #86aee1; - margin: 10pt; - text-align: center; - text-decoration: none; - padding: 50pt; - font-size: 20pt; - border-radius: 3pt; -} - -.modaldialog .bigbutton { - padding-left: 0; - padding-right: 0; - width: 100%; -} - -} - - -ul#colorpickerswatches -{ - padding-left: 3px; - padding-top: 5px; -} - -ul#colorpickerswatches li -{ - border: 1px solid #ccc; - width: 14px; - height: 14px; - overflow: hidden; - margin: 3px 6px; - padding: 0px; -} - -ul#colorpickerswatches li:hover -{ - border: 1px solid #000; - cursor: pointer; -} - - - -#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; - overflow-y:scroll; - font-size: 12px; - position:absolute; - right:0px; - left:0px; - top:25px; - bottom:25px; - z-index:1002; -} - -#chattext p -{ - padding: 3px; - 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:#555; - font-size:9px; - 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: 2px; - background-repeat:no-repeat; - padding-left:25px; - background-image: url("../../static/img/etherpad_lite_icons.png"); - color:#fff; - text-decoration:none; -} - -#importexportline{ - border-left: 1px solid #fff; - height: 190px; - position:absolute; - width:0px; - left:260px; - opacity:.8; -} - -.impexpbutton{ - background-image: linear-gradient(center top , #EEEEEE, white 20%, white 20%); - padding:3px; -} - -#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; -} - -#exportwordle{ - background-position: 0px -323px; -} - -#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; -border-radius: 6px; -opacity:.8; -} - -.buttonicon{ -width:16px; -height:16px; -background-image:url('../../static/img/etherpad_lite_icons.png'); -background-repeat: no-repeat; -margin-left: 1px; -margin-top: 1px; -} -.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; - display: inline-block; - vertical-align: middle; - margin: 0 !important; -} -.buttonicon-showusers { - background-position: 0px -183px; - display: inline-block; -} - -#usericon -{ -width:33px !important; -} - -#focusprotector -{ - z-index: 100; - position: absolute; - bottom: 0px; - top: 0px; - left: 0px; - right: 0px; - background-color: white; - opacity:0.01; - display:none; -} - -#online_count{ - color: #888; - font-size: 11px; - line-height: 18px; - position: fixed; -} - -#qr_center { - margin: 10px 10px auto 0; - text-align: center; -} - -#embedreadonlyqr { - box-shadow: 0 0 10px #000; - border-radius: 3px; - transition: all .2s ease-in-out; -} - -#embedreadonlyqr:hover { - cursor: none; - transform: scale(1.5); -} - -.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; - z-index: 500; - padding: 10px; - border-radius: 6px; - background: #222; - background: rgba(0,0,0,.7); - background: linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,.7) 35px, rgba(0,0,0,.6)); - box-shadow: 0 0 8px #888; - color: #fff; -} - -.popup input[type=text] { - width: 100%; - padding: 5px; - box-sizing: border-box; - display:block; - margin-top: 10px; -} - -.popup a { - text-decoration: none; -} - -.popup h1 { - font-size: 18px; -} -.popup h2 { - font-size: 15px; -} -.popup p { - margin: 5px 0; -} - -.column { - float: left; - width: 50%; -} - -#settingsmenu, #importexport, #embed { - position: absolute; - top: 55px; - right: 20px; - display: none; -} - -.note { - color: #ddd; - font-size: 11px; - font-weight: bold; -} - -.selected { - background: #eee !important; - background: linear-gradient(#EEE, #F0F0F0) !important; -} - -.stickyChat { - background-color: #f1f1f1 !important; - right: 0px !important; - top: 36px; - 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) { - #editbar ul li { - padding: 4px 1px; - } -} - -@media only screen and (min-device-width: 320px) and (max-device-width: 720px) { - #editbar ul li { - padding: 4px 3px; - } - #users { - right: 0; - top: 36px; - bottom: 33px; - border-radius: none; - } - #mycolorpicker { - left: -72px; /* #mycolorpicker:width - #users:width */ - } - #editorcontainer { - margin-bottom: 33px; - } - #editbar ul#menu_right { - background: #f7f7f7; - background: linear-gradient(#f7f7f7, #f1f1f1 80%); - width: 100%; - overflow: hidden; - height: 32px; - position: fixed; - bottom: 0; - border-top: 1px solid #ccc; - } - #editbar ul#menu_right li:last-child { - height: 24px; - border-radius: 0; - margin-top: 0; - border: 0; - float: right; - } - #chaticon { - bottom: 3px; - right: 55px; - border-right: none; - border-radius: 0; - background: #f7f7f7; - background: linear-gradient(#f7f7f7, #f1f1f1 80%); - border: 0; - } - #chatbox { - bottom: 32px; - right: 0; - border-top-right-radius: 0; - border-right: none; - } - #editbar ul li a span { - top: -3px; - } - #usericonback { - margin-top: 4px; - } - #qrcode { - display: none; - } - #editbar ul#menu_right li:not(:last-child) { - display: block; - } - #editbar ul#menu_right > li { - background: none; - border: none; - margin-top: 4px; - padding: 4px 8px; - } - .selected { - background: none !important; - } - #timesliderlink { - display: none !important; - } - .popup { - border-radius: 0; - box-sizing: border-box; - width: 100%; - } - #settingsmenu, #importexport, #embed { - left: 0; - top: 0; - bottom: 33px; - right: 0; - } - .separator { - display: none; - } - #online_count { - line-height: 24px; - } -} \ No newline at end of file diff --git a/static/css/timeslider.css b/static/css/timeslider.css deleted file mode 100644 index 926c80123..000000000 --- a/static/css/timeslider.css +++ /dev/null @@ -1,106 +0,0 @@ -#editorcontainerbox {overflow:auto; top:40px;} - -#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 { - -khtml-user-select:none; - -moz-user-select:none; - -ms-user-select:none; - -webkit-user-select:none; - background-image:url(../../static/img/crushed_current_location.png); - cursor:pointer; - height:61px; - left:0; - position:absolute; - top:0; - user-select:none; - width:13px; -} -#ui-slider-bar { - -khtml-user-select:none; - -moz-user-select:none; - -ms-user-select:none; - -webkit-user-select:none; - cursor:pointer; - height:35px; - margin-left:5px; - margin-right:148px; - position:relative; - top:20px; - user-select:none; -} - -#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:30px;} -#editbarright {float:right;} -#returnbutton {color:#222; font-size:16px; line-height:29px; margin-top:0; padding-right:6px;} -#importexport {top:118px;} -#importexport .popup {width:185px;} - -/* 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/static/custom/.gitignore b/static/custom/.gitignore deleted file mode 100644 index 8cdbf1832..000000000 --- a/static/custom/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!js.template -!css.template \ No newline at end of file diff --git a/static/img/etherpad_lite_icons.png b/static/img/etherpad_lite_icons.png deleted file mode 100644 index cadf5ed2b..000000000 Binary files a/static/img/etherpad_lite_icons.png and /dev/null differ diff --git a/static/js/AttributePoolFactory.js b/static/js/AttributePoolFactory.js deleted file mode 100644 index 00b58dbb3..000000000 --- a/static/js/AttributePoolFactory.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * 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. - */ - -exports.createAttributePool = function () { - var p = {}; - p.numToAttrib = {}; // e.g. {0: ['foo','bar']} - p.attribToNum = {}; // e.g. {'foo,bar': 0} - p.nextNum = 0; - - p.putAttrib = function (attrib, dontAddIfAbsent) { - var str = String(attrib); - if (str in p.attribToNum) { - return p.attribToNum[str]; - } - if (dontAddIfAbsent) { - return -1; - } - var num = p.nextNum++; - p.attribToNum[str] = num; - p.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; - return num; - }; - - p.getAttrib = function (num) { - var pair = p.numToAttrib[num]; - if (!pair) { - return pair; - } - return [pair[0], pair[1]]; // return a mutable copy - }; - - p.getAttribKey = function (num) { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[0]; - }; - - p.getAttribValue = function (num) { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[1]; - }; - - p.eachAttrib = function (func) { - for (var n in p.numToAttrib) { - var pair = p.numToAttrib[n]; - func(pair[0], pair[1]); - } - }; - - p.toJsonable = function () { - return { - numToAttrib: p.numToAttrib, - nextNum: p.nextNum - }; - }; - - p.fromJsonable = function (obj) { - p.numToAttrib = obj.numToAttrib; - p.nextNum = obj.nextNum; - p.attribToNum = {}; - for (var n in p.numToAttrib) { - p.attribToNum[String(p.numToAttrib[n])] = Number(n); - } - return p; - }; - - return p; -} diff --git a/static/js/pad_modals.js b/static/js/pad_modals.js deleted file mode 100644 index b78e28f40..000000000 --- a/static/js/pad_modals.js +++ /dev/null @@ -1,374 +0,0 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -/** - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var padutils = require('/pad_utils').padutils; -var paddocbar = require('/pad_docbar').paddocbar; - -var padmodals = (function() -{ - -/*var clearFeedbackEmail = function() {}; - function clearFeedback() { - clearFeedbackEmail(); - $("#feedbackbox-message").val(''); - } - - var sendingFeedback = false; - function setSendingFeedback(v) { - v = !! v; - if (sendingFeedback != v) { - sendingFeedback = v; - if (v) { - $("#feedbackbox-send").css('opacity', 0.75); - } - else { - $("#feedbackbox-send").css('opacity', 1); - } - } - }*/ - - var sendingInvite = false; - - function setSendingInvite(v) - { - v = !! v; - if (sendingInvite != v) - { - sendingInvite = v; - if (v) - { - $(".sharebox-send").css('opacity', 0.75); - } - else - { - $("#sharebox-send").css('opacity', 1); - } - } - } - - var clearShareBoxTo = function() - {}; - - function clearShareBox() - { - clearShareBoxTo(); - } - - var pad = undefined; - var self = { - init: function(_pad) - { - pad = _pad; - - self.initFeedback(); - self.initShareBox(); - }, - initFeedback: function() - { -/*var emailField = $("#feedbackbox-email"); - clearFeedbackEmail = - padutils.makeFieldLabeledWhenEmpty(emailField, '(your email address)').clear; - clearFeedback();*/ - - $("#feedbackbox-hide").click(function() - { - self.hideModal(); - }); -/*$("#feedbackbox-send").click(function() { - self.sendFeedbackEmail(); - });*/ - - $("#feedbackbutton").click(function() - { - self.showFeedback(); - }); - }, - initShareBox: function() - { - $("#sharebutton").click(function() - { - self.showShareBox(); - }); - $("#sharebox-hide").click(function() - { - self.hideModal(); - }); - $("#sharebox-send").click(function() - { - self.sendInvite(); - }); - - $("#sharebox-url").click(function() - { - $("#sharebox-url").focus().select(); - }); - - clearShareBoxTo = padutils.makeFieldLabeledWhenEmpty($("#sharebox-to"), "(email addresses)").clear; - clearShareBox(); - - $("#sharebox-subject").val(self.getDefaultShareBoxSubjectForName(pad.getUserName())); - $("#sharebox-message").val(self.getDefaultShareBoxMessageForName(pad.getUserName())); - - $("#sharebox-stripe .setsecurity").click(function() - { - self.hideModal(); - paddocbar.setShownPanel('security'); - }); - }, - getDefaultShareBoxMessageForName: function(name) - { - return (name || "Somebody") + " has shared an EtherPad document with you." + "\n\n" + "View it here:\n\n" + padutils.escapeHtml($(".sharebox-url").val() + "\n"); - }, - getDefaultShareBoxSubjectForName: function(name) - { - return (name || "Somebody") + " invited you to an EtherPad document"; - }, - relayoutWithBottom: function(px) - { - $("#modaloverlay").height(px); - $("#sharebox").css('left', Math.floor(($(window).width() - $("#sharebox").outerWidth()) / 2)); - $("#feedbackbox").css('left', Math.floor(($(window).width() - $("#feedbackbox").outerWidth()) / 2)); - }, - showFeedback: function() - { - self.showModal("#feedbackbox"); - }, - showShareBox: function() - { - // when showing the dialog, if it still says "Somebody" invited you - // then we fill in the updated username if there is one; - // otherwise, we don't touch it, perhaps the user is happy with it - var msgbox = $("#sharebox-message"); - if (msgbox.val() == self.getDefaultShareBoxMessageForName(null)) - { - msgbox.val(self.getDefaultShareBoxMessageForName(pad.getUserName())); - } - var subjBox = $("#sharebox-subject"); - if (subjBox.val() == self.getDefaultShareBoxSubjectForName(null)) - { - subjBox.val(self.getDefaultShareBoxSubjectForName(pad.getUserName())); - } - - if (pad.isPadPublic()) - { - $("#sharebox-stripe").get(0).className = 'sharebox-stripe-public'; - } - else - { - $("#sharebox-stripe").get(0).className = 'sharebox-stripe-private'; - } - - self.showModal("#sharebox", 500); - $("#sharebox-url").focus().select(); - }, - showModal: function(modalId, duration) - { - $(".modaldialog").hide(); - $(modalId).show().css( - { - 'opacity': 0 - }).animate( - { - 'opacity': 1 - }, duration); - $("#modaloverlay").show().css( - { - 'opacity': 0 - }).animate( - { - 'opacity': 1 - }, duration); - }, - hideModal: function(duration) - { - padutils.cancelActions('hide-feedbackbox'); - padutils.cancelActions('hide-sharebox'); - $("#sharebox-response").hide(); - $(".modaldialog").animate( - { - 'opacity': 0 - }, duration, function() - { - $("#modaloverlay").hide(); - }); - $("#modaloverlay").animate( - { - 'opacity': 0 - }, duration, function() - { - $("#modaloverlay").hide(); - }); - }, - hideFeedbackLaterIfNoOtherInteraction: function() - { - return padutils.getCancellableAction('hide-feedbackbox', function() - { - self.hideModal(); - }); - }, - hideShareboxLaterIfNoOtherInteraction: function() - { - return padutils.getCancellableAction('hide-sharebox', function() - { - self.hideModal(); - }); - }, -/* sendFeedbackEmail: function() { - if (sendingFeedback) { - return; - } - var message = $("#feedbackbox-message").val(); - if (! message) { - return; - } - var email = ($("#feedbackbox-email").hasClass('editempty') ? '' : - $("#feedbackbox-email").val()); - var padId = pad.getPadId(); - var username = pad.getUserName(); - setSendingFeedback(true); - $("#feedbackbox-response").html("Sending...").get(0).className = ''; - $("#feedbackbox-response").show(); - $.ajax({ - type: 'post', - url: '/ep/pad/feedback', - data: { - feedback: message, - padId: padId, - username: username, - email: email - }, - success: success, - error: error - }); - var hideCall = self.hideFeedbackLaterIfNoOtherInteraction(); - function success(msg) { - setSendingFeedback(false); - clearFeedback(); - $("#feedbackbox-response").html("Thanks for your feedback").get(0).className = 'goodresponse'; - $("#feedbackbox-response").show(); - window.setTimeout(function() { - $("#feedbackbox-response").fadeOut('slow', function() { - hideCall(); - }); - }, 1500); - } - function error(e) { - setSendingFeedback(false); - $("#feedbackbox-response").html("Could not send feedback. Please email us at feedback"+"@"+"etherpad.com instead.").get(0).className = 'badresponse'; - $("#feedbackbox-response").show(); - } - },*/ - sendInvite: function() - { - if (sendingInvite) - { - return; - } - if (!pad.isFullyConnected()) - { - displayErrorMessage("Error: Connection to the server is down or flaky."); - return; - } - var message = $("#sharebox-message").val(); - if (!message) - { - displayErrorMessage("Please enter a message body before sending."); - return; - } - var emails = ($("#sharebox-to").hasClass('editempty') ? '' : $("#sharebox-to").val()) || ''; - // find runs of characters that aren't obviously non-email punctuation - var emailArray = emails.match(/[^\s,:;<>\"\'\/\(\)\[\]{}]+/g) || []; - if (emailArray.length == 0) - { - displayErrorMessage('Please enter at least one "To:" address.'); - $("#sharebox-to").focus().select(); - return; - } - for (var i = 0; i < emailArray.length; i++) - { - var addr = emailArray[i]; - if (!addr.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/)) - { - displayErrorMessage('"' + padutils.escapeHtml(addr) + '" does not appear to be a valid email address.'); - return; - } - } - var subject = $("#sharebox-subject").val(); - if (!subject) - { - subject = self.getDefaultShareBoxSubjectForName(pad.getUserName()); - $("#sharebox-subject").val(subject); // force the default subject - } - - var padId = pad.getPadId(); - var username = pad.getUserName(); - setSendingInvite(true); - $("#sharebox-response").html("Sending...").get(0).className = ''; - $("#sharebox-response").show(); - $.ajax( - { - type: 'post', - url: '/ep/pad/emailinvite', - data: { - message: message, - toEmails: emailArray.join(','), - subject: subject, - username: username, - padId: padId - }, - success: success, - error: error - }); - var hideCall = self.hideShareboxLaterIfNoOtherInteraction(); - - function success(msg) - { - setSendingInvite(false); - $("#sharebox-response").html("Email invitation sent!").get(0).className = 'goodresponse'; - $("#sharebox-response").show(); - window.setTimeout(function() - { - $("#sharebox-response").fadeOut('slow', function() - { - hideCall(); - }); - }, 1500); - } - - function error(e) - { - setSendingFeedback(false); - $("#sharebox-response").html("An error occurred; no email was sent.").get(0).className = 'badresponse'; - $("#sharebox-response").show(); - } - - function displayErrorMessage(msgHtml) - { - $("#sharebox-response").html(msgHtml).get(0).className = 'badresponse'; - $("#sharebox-response").show(); - } - } - }; - return self; -}()); - -exports.padmodals = padmodals; diff --git a/static/js/pad_savedrevs.js b/static/js/pad_savedrevs.js deleted file mode 100644 index 6d37dfa81..000000000 --- a/static/js/pad_savedrevs.js +++ /dev/null @@ -1,526 +0,0 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -/** - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var padutils = require('/pad_utils').padutils; -var paddocbar = require('/pad_docbar').paddocbar; - -var padsavedrevs = (function() -{ - - function reversedCopy(L) - { - var L2 = L.slice(); - L2.reverse(); - return L2; - } - - function makeRevisionBox(revisionInfo, rnum) - { - var box = $('
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '
    '); - setBoxLabel(box, revisionInfo.label); - setBoxTimestamp(box, revisionInfo.timestamp); - box.find(".srauthor").html("by " + padutils.escapeHtml(revisionInfo.savedBy)); - var viewLink = '/ep/pad/view/' + pad.getPadId() + '/' + revisionInfo.id; - box.find(".srview").attr('href', viewLink); - var restoreLink = 'javascript:void(require('+JSON.stringify(module.id)+').padsavedrevs.restoreRevision(' + JSON.stringify(rnum) + ');'; - box.find(".srrestore").attr('href', restoreLink); - box.find(".srname").click(function(evt) - { - editRevisionLabel(rnum, box); - }); - return box; - } - - function setBoxLabel(box, label) - { - box.find(".srname").html(padutils.escapeHtml(label)).attr('title', label); - } - - function setBoxTimestamp(box, timestamp) - { - box.find(".srtime").html(padutils.escapeHtml( - padutils.timediff(new Date(timestamp)))); - } - - function getNthBox(n) - { - return $("#savedrevisions .srouterbox").eq(n); - } - - function editRevisionLabel(rnum, box) - { - var input = $(''); - box.find(".srnameedit").remove(); // just in case - var label = box.find(".srname"); - input.width(label.width()); - input.height(label.height()); - input.css('top', label.position().top); - input.css('left', label.position().left); - label.after(input); - label.css('opacity', 0); - - function endEdit() - { - input.remove(); - label.css('opacity', 1); - } - var rev = currentRevisionList[rnum]; - var oldLabel = rev.label; - input.blur(function() - { - var newLabel = input.val(); - if (newLabel && newLabel != oldLabel) - { - relabelRevision(rnum, newLabel); - } - endEdit(); - }); - input.val(rev.label).focus().select(); - padutils.bindEnterAndEscape(input, function onEnter() - { - input.blur(); - }, function onEscape() - { - input.val('').blur(); - }); - } - - function relabelRevision(rnum, newLabel) - { - var rev = currentRevisionList[rnum]; - $.ajax( - { - type: 'post', - url: '/ep/pad/saverevisionlabel', - data: { - userId: pad.getUserId(), - padId: pad.getPadId(), - revId: rev.id, - newLabel: newLabel - }, - success: success, - error: error - }); - - function success(text) - { - var newRevisionList = JSON.parse(text); - self.newRevisionList(newRevisionList); - pad.sendClientMessage( - { - type: 'revisionLabel', - revisionList: reversedCopy(currentRevisionList), - savedBy: pad.getUserName(), - newLabel: newLabel - }); - } - - function error(e) - { - alert("Oops! There was an error saving that revision label. Please try again later."); - } - } - - var currentRevisionList = []; - - function setRevisionList(newRevisionList, noAnimation) - { - // deals with changed labels and new added revisions - for (var i = 0; i < currentRevisionList.length; i++) - { - var a = currentRevisionList[i]; - var b = newRevisionList[i]; - if (b.label != a.label) - { - setBoxLabel(getNthBox(i), b.label); - } - } - for (var j = currentRevisionList.length; j < newRevisionList.length; j++) - { - var newBox = makeRevisionBox(newRevisionList[j], j); - $("#savedrevs-scrollinner").append(newBox); - newBox.css('left', j * REVISION_BOX_WIDTH); - } - var newOnes = (newRevisionList.length > currentRevisionList.length); - currentRevisionList = newRevisionList; - if (newOnes) - { - setDesiredScroll(getMaxScroll()); - if (noAnimation) - { - setScroll(desiredScroll); - } - - if (!noAnimation) - { - var nameOfLast = currentRevisionList[currentRevisionList.length - 1].label; - displaySavedTip(nameOfLast); - } - } - } - - function refreshRevisionList() - { - for (var i = 0; i < currentRevisionList.length; i++) - { - var r = currentRevisionList[i]; - var box = getNthBox(i); - setBoxTimestamp(box, r.timestamp); - } - } - - var savedTipAnimator = padutils.makeShowHideAnimator(function(state) - { - if (state == -1) - { - $("#revision-notifier").css('opacity', 0).css('display', 'block'); - } - else if (state == 0) - { - $("#revision-notifier").css('opacity', 1); - } - else if (state == 1) - { - $("#revision-notifier").css('opacity', 0).css('display', 'none'); - } - else if (state < 0) - { - $("#revision-notifier").css('opacity', 1); - } - else if (state > 0) - { - $("#revision-notifier").css('opacity', 1 - state); - } - }, false, 25, 300); - - function displaySavedTip(text) - { - $("#revision-notifier .name").html(padutils.escapeHtml(text)); - savedTipAnimator.show(); - padutils.cancelActions("hide-revision-notifier"); - var hideLater = padutils.getCancellableAction("hide-revision-notifier", function() - { - savedTipAnimator.hide(); - }); - window.setTimeout(hideLater, 3000); - } - - var REVISION_BOX_WIDTH = 120; - var curScroll = 0; // distance between left of revisions and right of view - var desiredScroll = 0; - - function getScrollWidth() - { - return REVISION_BOX_WIDTH * currentRevisionList.length; - } - - function getViewportWidth() - { - return $("#savedrevs-scrollouter").width(); - } - - function getMinScroll() - { - return Math.min(getViewportWidth(), getScrollWidth()); - } - - function getMaxScroll() - { - return getScrollWidth(); - } - - function setScroll(newScroll) - { - curScroll = newScroll; - $("#savedrevs-scrollinner").css('right', newScroll); - updateScrollArrows(); - } - - function setDesiredScroll(newDesiredScroll, dontUpdate) - { - desiredScroll = Math.min(getMaxScroll(), Math.max(getMinScroll(), newDesiredScroll)); - if (!dontUpdate) - { - updateScroll(); - } - } - - function updateScroll() - { - updateScrollArrows(); - scrollAnimator.scheduleAnimation(); - } - - function updateScrollArrows() - { - $("#savedrevs-scrollleft").toggleClass("disabledscrollleft", desiredScroll <= getMinScroll()); - $("#savedrevs-scrollright").toggleClass("disabledscrollright", desiredScroll >= getMaxScroll()); - } - var scrollAnimator = padutils.makeAnimationScheduler(function() - { - setDesiredScroll(desiredScroll, true); // re-clamp - if (Math.abs(desiredScroll - curScroll) < 1) - { - setScroll(desiredScroll); - return false; - } - else - { - setScroll(curScroll + (desiredScroll - curScroll) * 0.5); - return true; - } - }, 50, 2); - - var isSaving = false; - - function setIsSaving(v) - { - isSaving = v; - rerenderButton(); - } - - function haveReachedRevLimit() - { - var mv = pad.getPrivilege('maxRevisions'); - return (!(mv < 0 || mv > currentRevisionList.length)); - } - - function rerenderButton() - { - if (isSaving || (!pad.isFullyConnected()) || haveReachedRevLimit()) - { - $("#savedrevs-savenow").css('opacity', 0.75); - } - else - { - $("#savedrevs-savenow").css('opacity', 1); - } - } - - var scrollRepeatTimer = null; - var scrollStartTime = 0; - - function setScrollRepeatTimer(dir) - { - clearScrollRepeatTimer(); - scrollStartTime = +new Date; - scrollRepeatTimer = window.setTimeout(function f() - { - if (!scrollRepeatTimer) - { - return; - } - self.scroll(dir); - var scrollTime = (+new Date) - scrollStartTime; - var delay = (scrollTime > 2000 ? 50 : 300); - scrollRepeatTimer = window.setTimeout(f, delay); - }, 300); - $(document).bind('mouseup', clearScrollRepeatTimer); - } - - function clearScrollRepeatTimer() - { - if (scrollRepeatTimer) - { - window.clearTimeout(scrollRepeatTimer); - scrollRepeatTimer = null; - } - $(document).unbind('mouseup', clearScrollRepeatTimer); - } - - var pad = undefined; - var self = { - init: function(initialRevisions, _pad) - { - pad = _pad; - self.newRevisionList(initialRevisions, true); - - $("#savedrevs-savenow").click(function() - { - self.saveNow(); - }); - $("#savedrevs-scrollleft").mousedown(function() - { - self.scroll('left'); - setScrollRepeatTimer('left'); - }); - $("#savedrevs-scrollright").mousedown(function() - { - self.scroll('right'); - setScrollRepeatTimer('right'); - }); - $("#savedrevs-close").click(function() - { - paddocbar.setShownPanel(null); - }); - - // update "saved n minutes ago" times - window.setInterval(function() - { - refreshRevisionList(); - }, 60 * 1000); - }, - restoreRevision: function(rnum) - { - var rev = currentRevisionList[rnum]; - var warning = ("Restoring this revision will overwrite the current" + " text of the pad. " + "Are you sure you want to continue?"); - var hidePanel = paddocbar.hideLaterIfNoOtherInteraction(); - var box = getNthBox(rnum); - if (confirm(warning)) - { - box.find(".srtwirly").show(); - $.ajax( - { - type: 'get', - url: '/ep/pad/getrevisionatext', - data: { - padId: pad.getPadId(), - revId: rev.id - }, - success: success, - error: error - }); - } - - function success(resultJson) - { - untwirl(); - var result = JSON.parse(resultJson); - padeditor.restoreRevisionText(result); - window.setTimeout(function() - { - hidePanel(); - }, 0); - } - - function error(e) - { - untwirl(); - alert("Oops! There was an error retreiving the text (revNum= " + rev.revNum + "; padId=" + pad.getPadId()); - } - - function untwirl() - { - box.find(".srtwirly").hide(); - } - }, - showReachedLimit: function() - { - alert("Sorry, you do not have privileges to save more than " + pad.getPrivilege('maxRevisions') + " revisions."); - }, - newRevisionList: function(lst, noAnimation) - { - // server gives us list with newest first; - // we want chronological order - var L = reversedCopy(lst); - setRevisionList(L, noAnimation); - rerenderButton(); - }, - saveNow: function() - { - if (isSaving) - { - return; - } - if (!pad.isFullyConnected()) - { - return; - } - if (haveReachedRevLimit()) - { - self.showReachedLimit(); - return; - } - setIsSaving(true); - var savedBy = pad.getUserName() || "unnamed"; - pad.callWhenNotCommitting(submitSave); - - function submitSave() - { - $.ajax( - { - type: 'post', - url: '/ep/pad/saverevision', - data: { - padId: pad.getPadId(), - savedBy: savedBy, - savedById: pad.getUserId(), - revNum: pad.getCollabRevisionNumber() - }, - success: success, - error: error - }); - } - - function success(text) - { - setIsSaving(false); - var newRevisionList = JSON.parse(text); - self.newRevisionList(newRevisionList); - pad.sendClientMessage( - { - type: 'newRevisionList', - revisionList: newRevisionList, - savedBy: savedBy - }); - } - - function error(e) - { - setIsSaving(false); - alert("Oops! The server failed to save the revision. Please try again later."); - } - }, - handleResizePage: function() - { - updateScrollArrows(); - }, - handleIsFullyConnected: function(isConnected) - { - rerenderButton(); - }, - scroll: function(dir) - { - var minScroll = getMinScroll(); - var maxScroll = getMaxScroll(); - if (dir == 'left') - { - if (desiredScroll > minScroll) - { - var n = Math.floor((desiredScroll - 1 - minScroll) / REVISION_BOX_WIDTH); - setDesiredScroll(Math.max(0, n) * REVISION_BOX_WIDTH + minScroll); - } - } - else if (dir == 'right') - { - if (desiredScroll < maxScroll) - { - var n = Math.floor((maxScroll - desiredScroll - 1) / REVISION_BOX_WIDTH); - setDesiredScroll(maxScroll - Math.max(0, n) * REVISION_BOX_WIDTH); - } - } - } - }; - return self; -}()); - -exports.padsavedrevs = padsavedrevs; diff --git a/static/js/plugins.js b/static/js/plugins.js deleted file mode 100644 index 3e226eba1..000000000 --- a/static/js/plugins.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -var plugins = { - callHook: function(hookName, args) - { - var global = (function () {return this}()); - var hook = ((global.clientVars || {}).hooks || {})[hookName]; - if (hook === undefined) return []; - var res = []; - for (var i = 0, N = hook.length; i < N; i++) - { - var plugin = hook[i]; - var pluginRes = eval(plugin.plugin)[plugin.original || hookName](args); - if (pluginRes != undefined && pluginRes != null) res = res.concat(pluginRes); - } - return res; - }, - - callHookStr: function(hookName, args, sep, pre, post) - { - if (sep == undefined) sep = ''; - if (pre == undefined) pre = ''; - if (post == undefined) post = ''; - var newCallhooks = []; - var callhooks = plugins.callHook(hookName, args); - for (var i = 0, ii = callhooks.length; i < ii; i++) { - newCallhooks[i] = pre + callhooks[i] + post; - } - return newCallhooks.join(sep || ""); - } -}; - -exports.plugins = plugins; diff --git a/static/js/prefixfree.js b/static/js/prefixfree.js deleted file mode 100644 index 1f62dc341..000000000 --- a/static/js/prefixfree.js +++ /dev/null @@ -1,421 +0,0 @@ -/** - * StyleFix 1.0.1 - * @author Lea Verou - * MIT license - */ - -(function(){ - -if(!window.addEventListener) { - return; -} - -var self = window.StyleFix = { - link: function(link) { - try { - // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets - if(link.rel !== 'stylesheet' || !link.sheet.cssRules || link.hasAttribute('data-noprefix')) { - return; - } - } - catch(e) { - return; - } - if(link.href == "data:text/css,"){ - return false; - } - var url = link.href || link.getAttribute('data-href'), - base = url.replace(/[^\/]+$/, ''), - parent = link.parentNode, - xhr = new XMLHttpRequest(); - - xhr.open('GET', url); - - xhr.onreadystatechange = function() { - if(xhr.readyState === 4) { - var css = xhr.responseText; - - if(css && link.parentNode) { - css = self.fix(css, true, link); - - // Convert relative URLs to absolute, if needed - if(base) { - css = css.replace(/url\((?:'|")?(.+?)(?:'|")?\)/gi, function($0, url) { - if(!/^([a-z]{3,10}:|\/|#)/i.test(url)) { // If url not absolute & not a hash - // May contain sequences like /../ and /./ but those DO work - return 'url("' + base + url + '")'; - } - - return $0; - }); - - // behavior URLs shoudn’t be converted (Issue #19) - css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + base, 'gi'), '$1'); - } - - var style = document.createElement('style'); - style.textContent = css; - style.media = link.media; - style.disabled = link.disabled; - style.setAttribute('data-href', link.getAttribute('href')); - - parent.insertBefore(style, link); - parent.removeChild(link); - } - } - }; - - xhr.send(null); - - link.setAttribute('data-inprogress', ''); - }, - - styleElement: function(style) { - var disabled = style.disabled; - - style.textContent = self.fix(style.textContent, true, style); - - style.disabled = disabled; - }, - - styleAttribute: function(element) { - var css = element.getAttribute('style'); - - css = self.fix(css, false, element); - - element.setAttribute('style', css); - }, - - process: function() { - // Linked stylesheets - $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); - - // Inline stylesheets - $('style').forEach(StyleFix.styleElement); - - // Inline styles - $('[style]').forEach(StyleFix.styleAttribute); - }, - - register: function(fixer, index) { - (self.fixers = self.fixers || []) - .splice(index === undefined? self.fixers.length : index, 0, fixer); - }, - - fix: function(css, raw) { - for(var i=0; i 3) { - parts.pop(); - - var shorthand = parts.join('-'); - - if(supported(shorthand) && properties.indexOf(shorthand) === -1) { - properties.push(shorthand); - } - } - } - }, - supported = function(property) { - return StyleFix.camelCase(property) in dummy; - } - - // Some browsers have numerical indices for the properties, some don't - if(style.length > 0) { - for(var i=0; i - - - - - - Etherpad Lite Timeslider - - - - - - - -
    -
    -
    -
    - -
    - -
    - -
    - -
    - Etherpad v1.1 Etherpad is free software - -
    - Full screen -
    -
    - -
    - -
    -
    -
    -
    -

    Server Notice:

    hide - -

    -
    -
    - - - - -
    - -
    - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - - - Return to pad -
    - -
    - - - - - - -
    -

    - - -

    -
     
    - - - - - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    - - -
    - - -
    - - - - - - - - -