diff --git a/.gitignore b/.gitignore index 0bd7f066..2fbb3220 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 @@ -11,6 +9,5 @@ bin/convertSettings.json *~ *.patch src/static/js/jquery.js -src/static/js/prefixfree.js npm-debug.log *.DS_Store diff --git a/README.plugins b/README.plugins new file mode 100644 index 00000000..72c45644 --- /dev/null +++ b/README.plugins @@ -0,0 +1,16 @@ +So, a plugin is an npm package whose name starts with ep_ and that contains a file ep.json +require("ep_etherpad-lite/static/js/plugingfw/plugins").update() will use npm to list all installed modules and read their ep.json files. These will contain registrations for hooks which are loaded +A hook registration is a pairs of a hook name and a function reference (filename for require() plus function name) +require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value}) will call all hook functions registered for hook_name +That is the basis. +Ok, so that was a slight simplification: inside ep.json, hook registrations are grouped into groups called "parts". Parts from all plugins are ordered using a topological sort according to "pre" and "post" pointers to other plugins/parts (just like dependencies, but non-installed plugins are silently ignored). +This ordering is honored when you do callAll(hook_name) - hook functions for that hook_name are called in that order +Ordering between plugins is undefined, only parts are ordered. + +A plugin usually has one part, but it van have multiple. +This is so that it can insert some hook registration before that of another plugin, and another one after. +This is important for e.g. registering URL-handlers for the express webserver, if you have some very generic and some very specific url-regexps +So, that's basically it... apart from client-side hooks +which works the same way, but uses a separate member of the part (part.client_hooks vs part.hooks), and where the hook function must obviously reside in a file require():able from the client... +One thing more: The main etherpad tree is actually a plugin itself, called ep_etherpad-lite, and it has it's own ep.json... +was that clear? \ No newline at end of file diff --git a/available_plugins/ep_fintest/ep.json b/available_plugins/ep_fintest/ep.json index f9e91491..4ec8e392 100644 --- a/available_plugins/ep_fintest/ep.json +++ b/available_plugins/ep_fintest/ep.json @@ -25,7 +25,8 @@ "hooks": { "somehookname": "ep_fintest/otherpart:somehook", "morehook": "ep_fintest/otherpart:morehook", - "expressCreateServer": "ep_fintest/otherpart:expressServer" + "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 index ca259f44..718fb095 100644 --- a/available_plugins/ep_fintest/otherpart.js +++ b/available_plugins/ep_fintest/otherpart.js @@ -14,3 +14,12 @@ exports.expressServer = function (hook_name, args, cb) { res.send("Abra cadabra"); }); } + +exports.eejsBlock_editbarMenuLeft = function (hook_name, args, cb) { + args.content = args.content + '\ +
  • \ + \ +
  • \ + '; + return cb(); +} diff --git a/bin/installDeps.sh b/bin/installDeps.sh index e2a43c53..2acebd82 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -81,21 +81,6 @@ if [ $DOWNLOAD_JQUERY = "true" ]; then curl -lo src/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 "src/static/js/prefixfree.js" ]; then - VERSION=$(cat src/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 src/static/js/prefixfree.js -k https://raw.github.com/LeaVerou/prefixfree/master/prefixfree.js || exit 1 -fi - #Remove all minified data to force node creating it new echo "Clear minfified cache..." rm -f var/minified* diff --git a/settings.json.template b/settings.json.template index 3ae0a981..7aaa5d7e 100644 --- a/settings.json.template +++ b/settings.json.template @@ -50,6 +50,12 @@ /* This setting is used if you need http basic auth */ // "httpAuth" : "user:pass", + /* This setting is used for http basic auth for admin pages. If not set, the admin page won't be accessible from web*/ + // "adminHttpAuth" : "user:pass", + /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ - "loglevel": "INFO" + "loglevel": "INFO", + + /* cache 6 hours = 1000*60*60*6 */ + "maxAge": 21600000 } diff --git a/settings.json.template_windows b/settings.json.template_windows index da661fca..35b54d8d 100644 --- a/settings.json.template_windows +++ b/settings.json.template_windows @@ -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 index 59cbf3aa..6bc77735 100644 --- a/src/ep.json +++ b/src/ep.json @@ -8,7 +8,9 @@ { "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": "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/src/node/db/Pad.js b/src/node/db/Pad.js index c384f172..b4a39c17 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -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 @@ -34,7 +39,7 @@ var Pad = function Pad(id) { 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/src/node/db/PadManager.js b/src/node/db/PadManager.js index 4e3a3199..5f08b1b1 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -115,7 +115,13 @@ exports.doesPadExists = function(padId, callback) db.get("pad:"+padId, function(err, value) { if(ERR(err, callback)) return; - callback(null, value != null && value.atext); + if(value != null && value.atext){ + callback(null, true); + } + else + { + callback(null, false); + } }); } diff --git a/src/node/eejs/examples/bar.ejs b/src/node/eejs/examples/bar.ejs new file mode 100644 index 00000000..6a2cc4ba --- /dev/null +++ b/src/node/eejs/examples/bar.ejs @@ -0,0 +1,9 @@ +a +<% e.begin_block("bar"); %> + A + <% e.begin_block("foo"); %> + XX + <% e.end_block(); %> + B +<% e.end_block(); %> +b diff --git a/src/node/eejs/examples/foo.ejs b/src/node/eejs/examples/foo.ejs new file mode 100644 index 00000000..daee5f8e --- /dev/null +++ b/src/node/eejs/examples/foo.ejs @@ -0,0 +1,7 @@ +<% e.inherit("./bar.ejs"); %> + +<% e.begin_define_block("foo"); %> + YY + <% e.super(); %> + ZZ +<% e.end_define_block(); %> diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js new file mode 100644 index 00000000..90c69e59 --- /dev/null +++ b/src/node/eejs/index.js @@ -0,0 +1,115 @@ +/* + * 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"); + +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) { + if (args == undefined) args = {}; + + if ((name.indexOf("./") == 0 || name.indexOf("../") == 0) && exports.info.file_stack.length) { + name = path.join(path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path), name); + } + var ejspath = require.resolve(name) + + 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/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index c3ee231c..866edeb0 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -190,6 +190,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") @@ -203,6 +208,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 diff --git a/src/node/handler/TimesliderMessageHandler.js b/src/node/handler/TimesliderMessageHandler.js index da859779..a6cf8f4d 100644 --- a/src/node/handler/TimesliderMessageHandler.js +++ b/src/node/handler/TimesliderMessageHandler.js @@ -166,6 +166,7 @@ function createTimesliderClientVars (padId, callback) hooks: [], initialStyledContents: {} }; + var pad; var initialChangesets = []; @@ -180,6 +181,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) { diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js new file mode 100644 index 00000000..fa7e7077 --- /dev/null +++ b/src/node/hooks/express/adminplugins.js @@ -0,0 +1,51 @@ +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) { + 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, 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/specialpages.js b/src/node/hooks/express/specialpages.js index 13cfd821..585a7eab 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -1,12 +1,12 @@ 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) { - var filePath = path.normalize(__dirname + "/../../../static/index.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.send(eejs.require("ep_etherpad-lite/templates/index.html"), { maxAge: exports.maxAge }); }); //serve robots.txt @@ -34,15 +34,13 @@ exports.expressCreateServer = function (hook_name, args, cb) { //serve pad.html under /p args.app.get('/p/:pad', function(req, res, next) { - var filePath = path.normalize(__dirname + "/../../../static/pad.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.send(eejs.require("ep_etherpad-lite/templates/pad.html"), { maxAge: exports.maxAge }); }); //serve timeslider.html under /p/$padname/timeslider args.app.get('/p/:pad/timeslider', function(req, res, next) { - var filePath = path.normalize(__dirname + "/../../../static/timeslider.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html"), { maxAge: exports.maxAge }); }); } \ No newline at end of file diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 8e9f967a..d0e28737 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -6,14 +6,30 @@ var settings = require('../../utils/Settings'); //checks for basic http auth exports.basicAuth = function (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; + + // When handling HTTP-Auth, an undefined password will lead to no authorization at all + var pass = settings.httpAuth || ''; + + if (req.path.indexOf('/admin') == 0) { + var pass = settings.adminHttpAuth; + + } + + // Just pass if password is an empty string + if (pass === '') { + return next(); + } + + + // If a password has been set and auth headers are present... + if (pass && req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { + // ...check login and password + if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() === pass) { + return next(); } } + // Otherwise return Auth required Headers, delayed for 1 second, if auth failed. res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); if (req.headers.authorization) { setTimeout(function () { @@ -25,8 +41,7 @@ exports.basicAuth = function (req, res, next) { } exports.expressConfigure = function (hook_name, args, cb) { - // Activate http basic auth if it has been defined in settings.json - if(settings.httpAuth != null) args.app.use(exports.basicAuth); + args.app.use(exports.basicAuth); // 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. diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index f569d4b9..b5d7b472 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -29,7 +29,6 @@ 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'); @@ -109,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) { + 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); } } diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 24237de4..12fcc55c 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -85,6 +85,11 @@ exports.loglevel = "INFO"; */ exports.httpAuth = null; +/** + * Http basic auth, with "user:password" format + */ +exports.adminHttpAuth = null; + //checks if abiword is avaiable exports.abiwordAvailable = function() { diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index b8b7e1f1..70d5a08c 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -18,13 +18,11 @@ 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 = path.normalize(ROOT_DIR + '../../var/'); -console.log(CACHE_DIR) +var CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined; var responseCache = {}; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js new file mode 100644 index 00000000..4c1bba24 --- /dev/null +++ b/src/node/utils/randomstring.js @@ -0,0 +1,16 @@ +/** + * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids + */ +var randomString = function randomString(len) +{ + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var randomstring = ''; + for (var i = 0; i < len; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return randomstring; +}; + +module.exports = randomString; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index f4af3b18..14c93f5c 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -22,7 +22,6 @@ , "chat.js" , "excanvas.js" , "farbtastic.js" - , "prefixfree.js" ] , "timeslider.js": [ "jquery.js" diff --git a/src/package.json b/src/package.json index 6253ecb1..0d4ad538 100644 --- a/src/package.json +++ b/src/package.json @@ -23,7 +23,12 @@ "log4js" : "0.4.1", "jsdom-nocontextifiy" : "0.2.10", "async-stacktrace" : "0.0.2", - "npm" : "1.1", + "npm" : "1.1", + "ejs" : "0.6.1", + "node.extend" : "1.0.0", + "graceful-fs" : "1.1.5", + "slide" : "1.1.3", + "semver" : "1.0.13", "underscore" : "1.3.1" }, "devDependencies": { diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 969d0027..19d148a3 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -24,7 +24,7 @@ a img } /* menu */ -#editbar ul +.toolbar ul { position: relative; list-style: none; @@ -35,18 +35,20 @@ a img } -#editbar +.toolbar { background: #f7f7f7; background: linear-gradient(#f7f7f7, #f1f1f1 80%); border-bottom: 1px solid #ccc; - height: 32px; overflow: hidden; padding-top: 3px; - width: 100%; + position: absolute; + left: 0; + right: 0; + height: 32px; } -#editbar ul li +.toolbar ul li { background: #fff; background: linear-gradient(#fff, #f0f0f0); @@ -61,52 +63,52 @@ a img width: 18px; } -#editbar ul li a +.toolbar ul li a { text-decoration: none; color: #ccc; position: absolute; } -#editbar ul li a span +.toolbar ul li a span { position: relative; top:-2px } -#editbar ul li:hover { +.toolbar ul li:hover { background: #fff; background: linear-gradient(#f4f4f4, #e4e4e4); } -#editbar ul li:active { +.toolbar 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 +.toolbar ul li.separator { border: inherit; background: inherit; visibility:hidden; width: 0px; } -#editbar ul li a +.toolbar ul li a { display: block; } -#editbar ul li a img +.toolbar ul li a img { padding: 1px; } -#editbar ul +.toolbar ul { float: left; } -#editbar ul#menu_right +.toolbar ul.menu_right { float: right; } @@ -320,7 +322,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } z-index: 10; } -#editbarsavetable +.toolbarsavetable { position:absolute; top: 6px; @@ -328,7 +330,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } height: 24px; } -#editbarsavetable td, #editbartable td +.toolbarsavetable td, .toolbartable td { white-space: nowrap; } @@ -688,14 +690,15 @@ a#topbarmaximize { background: url(static/img/maximize_maximized.png); } -#editbarinner h1 { +.toolbarinner h1 { line-height: 29px; font-size: 16px; padding-left: 6pt; margin-top: 0; + white-space: nowrap; } -#editbarinner h1 a { +.toolbarinner h1 a { font-size: 12px; } @@ -1034,6 +1037,9 @@ margin-top: 1px; background-position: 0px -183px; display: inline-block; } +.buttonicon-savedRevision { + background-position: 0px -493px +} #usericon { @@ -1173,13 +1179,13 @@ input[type=checkbox] { } @media screen and (max-width: 600px) { - #editbar ul li { + .toolbar ul li { padding: 4px 1px; } } @media only screen and (min-device-width: 320px) and (max-device-width: 720px) { - #editbar ul li { + .toolbar ul li { padding: 4px 3px; } #users { @@ -1194,7 +1200,7 @@ input[type=checkbox] { #editorcontainer { margin-bottom: 33px; } - #editbar ul#menu_right { + .toolbar ul.menu_right { background: #f7f7f7; background: linear-gradient(#f7f7f7, #f1f1f1 80%); width: 100%; @@ -1204,7 +1210,7 @@ input[type=checkbox] { bottom: 0; border-top: 1px solid #ccc; } - #editbar ul#menu_right li:last-child { + .toolbar ul.menu_right li:last-child { height: 24px; border-radius: 0; margin-top: 0; @@ -1226,7 +1232,7 @@ input[type=checkbox] { border-top-right-radius: 0; border-right: none; } - #editbar ul li a span { + .toolbar ul li a span { top: -3px; } #usericonback { @@ -1235,10 +1241,10 @@ input[type=checkbox] { #qrcode { display: none; } - #editbar ul#menu_right li:not(:last-child) { + .toolbar ul.menu_right li:not(:last-child) { display: block; } - #editbar ul#menu_right > li { + .toolbar ul.menu_right > li { background: none; border: none; margin-top: 4px; @@ -1267,4 +1273,4 @@ input[type=checkbox] { #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 index 926c8012..38e4cfb1 100644 --- a/src/static/css/timeslider.css +++ b/src/static/css/timeslider.css @@ -42,10 +42,10 @@ #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;} +#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); @@ -71,8 +71,11 @@ #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;} +#importexport{ + top:118px; + width:185px; +} /* lists */ .list-bullet2, .list-indent2, .list-number2 {margin-left:3em;} diff --git a/src/static/img/etherpad_lite_icons.png b/src/static/img/etherpad_lite_icons.png index cadf5ed2..27867d42 100644 Binary files a/src/static/img/etherpad_lite_icons.png and b/src/static/img/etherpad_lite_icons.png differ diff --git a/src/static/img/star.png b/src/static/img/star.png new file mode 100644 index 00000000..e0c7099e Binary files /dev/null and b/src/static/img/star.png differ diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 685d45df..4dfcc64e 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -233,14 +233,16 @@ require.setGlobalKeyPath("require");\n\ iframeHTML.push(doctype); iframeHTML.push(""); + iframeHTML.push(''); + + hooks.callAll("aceInitInnerdocbodyHead", { + iframeHTML: iframeHTML + }); // For compatability's sake transform in and out. for (var i = 0, ii = iframeHTML.length; i < ii; i++) { iframeHTML[i] = JSON.stringify(iframeHTML[i]); } - hooks.callAll("aceInitInnerdocbodyHead", { - iframeHTML: iframeHTML - }); for (var i = 0, ii = iframeHTML.length; i < ii; i++) { iframeHTML[i] = JSON.parse(iframeHTML[i]); } @@ -262,6 +264,11 @@ require.setGlobalKeyPath("require");\n\ // Inject my plugins into my child. iframeHTML.push('\ - - - - - - diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html new file mode 100644 index 00000000..7dcb6fa3 --- /dev/null +++ b/src/templates/admin/plugins.html @@ -0,0 +1,217 @@ + + + Plugin manager + + + + + + + <% if (errors.length) { %> +
    + <% errors.forEach(function (item) { %> +
    <%= item.toString() %>
    + <% }) %> +
    + <% } %> + + +

    Installed plugins

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

    Search for plugins to install

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

    + Please wait: + +

    + +
    +
    + + + diff --git a/src/static/index.html b/src/templates/index.html similarity index 100% rename from src/static/index.html rename to src/templates/index.html diff --git a/src/templates/pad.html b/src/templates/pad.html new file mode 100644 index 00000000..e589fb55 --- /dev/null +++ b/src/templates/pad.html @@ -0,0 +1,307 @@ +<% + var settings = require("ep_etherpad-lite/node/utils/Settings"); +%> + + + + Etherpad Lite + + + + + + <% e.begin_block("styles"); %> + + + + <% e.end_block(); %> + + + +
    + + +
    + +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    Loading...
    +
    + + + + + + + +
    + +
    + Chat + + 0 +
    + +
    +
    Chat
    +
    +
    +
    + +
    +
    +
    + +
     
    + +
    +
    +
    + +
    + <% e.begin_block("modals"); %> +
    +
    +
    Connecting...
    +
    Reestablishing connection...
    +
    +

    Disconnected.

    +

    Opened in another window.

    +

    No Authorization.

    +
    +

    We're having trouble talking to the EtherPad lite synchronization server. You may be connecting through an incompatible firewall or proxy server.

    +
    +
    +

    We were unable to connect to the EtherPad lite synchronization server. This may be due to an incompatibility with your web browser or internet connection.

    +
    +
    +

    You seem to have opened this pad in another browser window. If you'd like to use this window instead, you can reconnect.

    +
    +
    +

    Lost connection with the EtherPad lite synchronization server. This may be due to a loss of network connectivity.

    +
    +
    +

    Server not responding. This may be due to network connectivity issues or high load on the server.

    +
    +
    +

    Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.

    +
    +
    +

    This pad was deleted.

    +
    +
    +

    If this continues to happen, please let us know

    +
    +
    + +
    +
    +
    + +
    + <% e.end_block(); %> +
    + + <% e.begin_block("scripts"); %> + + + + <% if (settings.minify) { %> + + <% } %> + + <% e.end_block(); %> + diff --git a/src/static/timeslider.html b/src/templates/timeslider.html similarity index 98% rename from src/static/timeslider.html rename to src/templates/timeslider.html index 413fbe80..02a34315 100644 --- a/src/static/timeslider.html +++ b/src/templates/timeslider.html @@ -119,8 +119,8 @@
    -
    -
    +
    +
    diff --git a/static/js/prefixfree.js b/static/js/prefixfree.js deleted file mode 100644 index 1f62dc34..00000000 --- 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