diff --git a/Makefile b/Makefile index 01f30701b..ab720f284 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,20 @@ -doc_dirs = doc $(wildcard doc/*/) -outdoc_dirs = out $(addprefix out/,$(doc_dirs)) doc_sources = $(wildcard doc/*/*.md) $(wildcard doc/*.md) outdoc_files = $(addprefix out/,$(doc_sources:.md=.html)) -docs: $(outdoc_files) +docassets = $(addprefix out/,$(wildcard doc/_assets/*)) + +VERSION = $(shell node -e "console.log( require('./src/package.json').version )") + +docs: $(outdoc_files) $(docassets) + +out/doc/_assets/%: doc/_assets/% + mkdir -p $(@D) + cp $< $@ out/doc/%.html: doc/%.md mkdir -p $(@D) node tools/doc/generate.js --format=html --template=doc/template.html $< > $@ + cat $@ | sed 's/__VERSION__/${VERSION}/' > $@ clean: rm -rf out/ diff --git a/README.plugins b/README.plugins deleted file mode 100644 index 72c456447..000000000 --- a/README.plugins +++ /dev/null @@ -1,16 +0,0 @@ -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/bin/convert.js b/bin/convert.js index ec792717e..4bbdd667c 100644 --- a/bin/convert.js +++ b/bin/convert.js @@ -1,9 +1,9 @@ var startTime = new Date().getTime(); var fs = require("fs"); -var ueberDB = require("ueberDB"); -var mysql = require("mysql"); -var async = require("async"); +var ueberDB = require("../src/node_modules/ueberDB"); +var mysql = require("../src/node_modules/ueberDB/node_modules/mysql"); +var async = require("../src/node_modules/async"); 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"); diff --git a/bin/migrateDirtyDBtoMySQL.js b/bin/migrateDirtyDBtoMySQL.js new file mode 100644 index 000000000..f2bc8efe2 --- /dev/null +++ b/bin/migrateDirtyDBtoMySQL.js @@ -0,0 +1,11 @@ +var dirty = require("../src/node_modules/ueberDB/node_modules/dirty")('var/dirty.db'); +var db = require("../src/node/db/DB"); + +db.init(function() { + db = db.db; + dirty.on("load", function() { + dirty.forEach(function(key, value) { + db.set(key, value); + }); + }); +}); diff --git a/doc/_assets/style.css b/doc/_assets/style.css new file mode 100644 index 000000000..fe1343af4 --- /dev/null +++ b/doc/_assets/style.css @@ -0,0 +1,44 @@ +body.apidoc { + width: 60%; + min-width: 10cm; + margin: 0 auto; +} + +#header { + background-color: #5a5; + padding: 10px; + color: #111; +} + +a, +a:active { + color: #272; +} +a:focus, +a:hover { + color: #050; +} + +#apicontent a.mark, +#apicontent a.mark:active { + float: right; + color: #BBB; + font-size: 0.7cm; + text-decoration: none; +} +#apicontent a.mark:focus, +#apicontent a.mark:hover { + color: #AAA; +} + +#apicontent code { + padding: 1px; + background-color: #EEE; + border-radius: 4px; + border: 1px solid #DDD; +} +#apicontent pre>code { + display: block; + overflow: auto; + padding: 5px; +} \ No newline at end of file diff --git a/doc/all.md b/doc/all.md index c0cbf369f..f1e071a6d 100644 --- a/doc/all.md +++ b/doc/all.md @@ -1,3 +1,4 @@ @include documentation @include api/api +@include plugins @include database diff --git a/doc/api/api.md b/doc/api/api.md index b96fa0c8e..eb5bb9c9b 100644 --- a/doc/api/api.md +++ b/doc/api/api.md @@ -1,7 +1,8 @@ @include embed_parameters @include http_api -@include hooks +@include hooks_overview @include hooks_client-side @include hooks_server-side @include editorInfo -@include changeset_library \ No newline at end of file +@include changeset_library +@include pluginfw \ No newline at end of file diff --git a/doc/api/hooks.md b/doc/api/hooks_overview.md similarity index 100% rename from doc/api/hooks.md rename to doc/api/hooks_overview.md diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 854b43394..c60d810e8 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -49,7 +49,8 @@ Called from: src/node/server.js Things in context: -1. app - the main application object (helpful for adding new paths and such) +1. app - the main express application object (helpful for adding new paths and such) +2. server - the http server object This hook gets called after the application object has been created, but before it starts listening. This is similar to the expressConfigure hook, but it's not guaranteed that the application object will have all relevant configuration variables. @@ -64,6 +65,42 @@ This hook gets called upon the rendering of an ejs template block. For any speci Have a look at `src/templates/pad.html` and `src/templates/timeslider.html` to see which blocks are available. +## padCreate +Called from: src/node/db/Pad.js + +Things in context: + +1. pad - the pad instance + +This hook gets called when a new pad was created. + +## padLoad +Called from: src/node/db/Pad.js + +Things in context: + +1. pad - the pad instance + +This hook gets called when an pad was loaded. If a new pad was created and loaded this event will be emitted too. + +## padUpdate +Called from: src/node/db/Pad.js + +Things in context: + +1. pad - the pad instance + +This hook gets called when an existing pad was updated. + +## padRemove +Called from: src/node/db/Pad.js + +Things in context: + +1. padID + +This hook gets called when an existing pad was removed/deleted. + ## socketio Called from: src/node/hooks/express/socketio.js @@ -71,6 +108,7 @@ Things in context: 1. app - the application object 2. io - the socketio object +3. server - the http server object I have no idea what this is useful for, someone else will have to add this description. diff --git a/doc/api/pluginfw.md b/doc/api/pluginfw.md new file mode 100644 index 000000000..2189c74ca --- /dev/null +++ b/doc/api/pluginfw.md @@ -0,0 +1,14 @@ +# Plugin Framework +`require("ep_etherpad-lite/static/js/plugingfw/plugins")` + +## plugins.update +`require("ep_etherpad-lite/static/js/plugingfw/plugins").update()` will use npm to list all installed modules and read their ep.json files, registering the contained hooks. +A hook registration is a pairs of a hook name and a function reference (filename for require() plus function name) + +## hooks.callAll +`require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value})` will call all hook functions registered for `hook_name` with `{argname:value}`. + +## hooks.aCallAll +? + +## ... diff --git a/doc/plugins.md b/doc/plugins.md new file mode 100644 index 000000000..3717c1111 --- /dev/null +++ b/doc/plugins.md @@ -0,0 +1,106 @@ +# Plugins +Etherpad-Lite allows you to extend its functionality with plugins. A plugin registers hooks (functions) for certain events (thus certain features) in Etherpad-lite to execute its own functionality based on these events. + +Publicly available plugins can be found in the npm registry (see ). Etherpad-lite's naming convention for plugins is to prefix your plugins with `ep_`. So, e.g. it's `ep_flubberworms`. Thus you can install plugins from npm, using `npm install ep_flubberworm` in etherpad-lite's root directory. + +You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will list all installed plugins and those available on npm. It even provides functionality to search through all available plugins. + +## Folder structure +A basic plugin usually has the following folder structure: +``` +ep_/ + | static/ + | templates/ + + ep.json + + package.json +``` +If your plugin includes client-side hooks, put them in `static/js/`. If you're adding in CSS or image files, you should put those files in `static/css/ `and `static/image/`, respectively, and templates go into `templates/`. + +A Standard directory structure like this makes it easier to navigate through your code. That said, do note, that this is not actually *required* to make your plugin run. + +## Plugin definition +Your plugin definition goes into `ep.json`. In this file you register your hooks, indicate the parts of your plugin and the order of execution. (A documentation of all available events to hook into can be found in chapter [hooks](#all_hooks).) + +A hook registration is a pairs of a hook name and a function reference (filename to require() + exported function name) + +```json +{ + "parts": [ + { + "name": "nameThisPartHoweverYouWant", + "hooks": { + "authenticate" : "ep_/:FUNCTIONNAME1", + "expressCreateServer": "ep_/:FUNCTIONNAME2" + }, + "client_hooks": { + "acePopulateDOMLine": "ep_plugin/:FUNCTIONNAME3" + } + } + ] +} +``` + +Etherpad-lite will expect the part of the hook definition before the colon to be a javascript file and will try to require it. The part after the colon is expected to be a valid function identifier of that module. So, you have to export your hooks, using [`module.exports`](http://nodejs.org/docs/latest/api/modules.html#modules_modules) and register it in `ep.json` as `ep_/path/to/:FUNCTIONNAME`. +You can omit the `FUNCTIONNAME` part, if the exported function has got the same name as the hook. So `"authorize" : "ep_flubberworm/foo"` will call the function `exports.authorize` in `ep_flubberworm/foo.js` + +### Client hooks and server hooks +There are server hooks, which will be executed on the server (e.g. `expressCreateServer`), and there are client hooks, which are executed on the client (e.g. `acePopulateDomLine`). Be sure to not make assumptions about the environment your code is running in, e.g. don't try to access `process`, if you know your code will be run on the client, and likewise, don't try to access `window` on the server... + +### Parts +As your plugins become more and more complex, you will find yourself in the need to manage dependencies between plugins. E.g. you want the hooks of a certain plugin to be executed before (or after) yours. You can also manage these dependencies in your plugin definition file `ep.json`: + +```javascript +{ + "parts": [ + { + "name": "onepart", + "pre": [], + "post": ["ep_onemoreplugin/partone"] + "hooks": { + "storeBar": "ep_monospace/plugin:storeBar", + "getFoo": "ep_monospace/plugin:getFoo", + } + }, + { + "name": "otherpart", + "pre": ["ep_my_example/somepart", "ep_otherplugin/main"], + "post": [], + "hooks": { + "someEvent": "ep_my_example/otherpart:someEvent", + "another": "ep_my_example/otherpart:another" + } + } + ] +} +``` + +Usually a plugin will add only one functionality at a time, so it will probably only use one `part` definition to register its hooks. However, sometimes you have to put different (unrelated) functionalities into one plugin. For this you will want use parts, so other plugins can depend on them. + +#### pre/post +The `"pre"` and `"post"` definitions, affect the order in which parts of a plugin are executed. This ensures that plugins and their hooks are executed in the correct order. + +`"pre"` lists parts that must be executed *before* the defining part. `"post"` lists parts that must be executed *after* the defining part. + +You can, on a basic level, think of this as double-ended dependency listing. If you have a dependency on another plugin, you can make sure it loads before yours by putting it in `"pre"`. If you are setting up things that might need to be used by a plugin later, you can ensure proper order by putting it in `"post"`. + +Note that it would be far more sane to use `"pre"` in almost any case, but if you want to change config variables for another plugin, or maybe modify its environment, `"post"` could definitely be useful. + +Also, note that dependencies should *also* be listed in your package.json, so they can be `npm install`'d automagically when your plugin gets installed. + +## Package definition +Your plugin must also contain a [package definition file](http://npmjs.org/doc/json.html), called package.json, in the project root - this file contains various metadata relevant to your plugin, such as the name and version number, author, project hompage, contributors, a short description, etc. If you publish your plugin on npm, these metadata are used for package search etc., but it's necessary for Etherpad-lite plugins, even if you don't publish your plugin. + +```json +{ + "name": "ep_PLUGINNAME", + "version": "0.0.1", + "description": "DESCRIPTION", + "author": "USERNAME (REAL NAME) ", + "contributors": [], + "dependencies": {"MODULE": "0.3.20"}, + "engines": { "node": ">= 0.6.0"} +} +``` + +## Templates +If your plugin adds or modifies the front end HTML (e.g. adding buttons or changing their functions), you should put the necessary HTML code for such operations in `templates/`, in files of type ".ejs", since Etherpad-Lite uses EJS for HTML templating. See the following link for more information about EJS: . \ No newline at end of file diff --git a/doc/template.html b/doc/template.html index 2eb939872..6416da943 100644 --- a/doc/template.html +++ b/doc/template.html @@ -2,12 +2,12 @@ - __SECTION__ Etherpad-Lite Manual & Documentation - + __SECTION__ - Etherpad Lite v__VERSION__ Manual & Documentation +
diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 2a6625c85..28b2dd91e 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -141,22 +141,6 @@ exports.getAuthor = function (author, callback) db.get("globalAuthor:" + author, callback); } -/** - * Returns the Author Name of the author - * @param {String} author The id of the author - * @param {Function} callback callback(err, name) - */ - -exports.getAuthorName = function (authorID, callback) -{ - db.getSub("globalAuthor:" + author, ["name"], callback); - console.log(authorID); - db.getSub("globalAuthor:" + authorID, ["name"], function(err, authorName){ - if(ERR(err, callback)) return; - callback(null, {authorName: authorName}); - }); -} - /** diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index ad2d59f38..dba791fd2 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -16,6 +16,7 @@ var padMessageHandler = require("../handler/PadMessageHandler"); var readOnlyManager = require("./ReadOnlyManager"); var crypto = require("crypto"); var randomString = require("../utils/randomstring"); +var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); //serialization/deserialization attributes var attributeBlackList = ["id"]; @@ -86,6 +87,12 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { // set the author to pad if(author) authorManager.addPad(author, this.id); + + if (this.head == 0) { + hooks.callAll("padCreate", {'pad':this}); + } else { + hooks.callAll("padUpdate", {'pad':this}); + } }; //save all attributes to the database @@ -368,6 +375,7 @@ Pad.prototype.init = function init(text, callback) { _this.appendRevision(firstChangeset, ''); } + hooks.callAll("padLoad", {'pad':_this}); callback(null); }); }; @@ -467,6 +475,7 @@ Pad.prototype.remove = function remove(callback) { { db.remove("pad:"+padID); padManager.unloadPad(padID); + hooks.callAll("padRemove", {'padID':padID}); callback(); } ], function(err) diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 1f4a6f2cc..eb3f6188a 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -1,4 +1,5 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); +var http = require('http'); var express = require('express'); var settings = require('../utils/Settings'); var fs = require('fs'); @@ -42,22 +43,24 @@ exports.createServer = function () { } exports.restartServer = function () { + if (server) { console.log("Restarting express server"); server.close(); } - server = express.createServer(); + var app = express(); // New syntax for express v3 + server = http.createServer(app); - server.use(function (req, res, next) { + app.use(function (req, res, next) { res.header("Server", serverName); next(); }); - server.configure(function() { - hooks.callAll("expressConfigure", {"app": server}); + app.configure(function() { + hooks.callAll("expressConfigure", {"app": app}); }); - hooks.callAll("expressCreateServer", {"app": server}); + hooks.callAll("expressCreateServer", {"app": app, "server": server}); server.listen(settings.port, settings.ip); -} \ No newline at end of file +} diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index fc274a075..97a0d602f 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -12,14 +12,10 @@ exports.expressCreateServer = function (hook_name, args, cb) { errors: [], }; - res.send(eejs.require( - "ep_etherpad-lite/templates/admin/plugins.html", - render_args), {}); + res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins.html", render_args) ); }); args.app.get('/admin/plugins/info', function(req, res) { - res.send(eejs.require( - "ep_etherpad-lite/templates/admin/plugins-info.html", - {}), {}); + res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", {}) ); }); } diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 4f5dad4f6..3c5956835 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -16,9 +16,6 @@ exports.gracefulShutdown = function(err) { console.log("graceful shutdown..."); - //stop the http server - exports.app.close(); - //do the db shutdown db.db.doShutdown(function() { console.log("db sucessfully closed."); @@ -35,10 +32,14 @@ exports.gracefulShutdown = function(err) { 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()); - }); + // Handle errors + args.app.use(function(err, req, res, next){ + // if an error occurs Connect will pass it down + // through these "error-handling" middleware + // allowing you to respond however you like + res.send(500, { error: 'Sorry, something bad happened!' }); + console.error(err.stack? err.stack : err.toString()); + }) //connect graceful shutdown with sigint and uncaughtexception if(os.type().indexOf("Windows") == -1) { diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index 60ece0add..af5cbed39 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -56,7 +56,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { ERR(err); if(err == "notfound") - res.send('404 - Not Found', 404); + res.send(404, '404 - Not Found'); else res.send(html); }); diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index 24ec2c3d0..29782b692 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -7,7 +7,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { //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); + res.send(404, 'Such a padname is forbidden'); } else { @@ -19,7 +19,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { var query = url.parse(req.url).query; if ( query ) real_url += '?' + query; res.header('Location', real_url); - res.send('You should be redirected to ' + real_url + '', 302); + res.send(302, 'You should be redirected to ' + real_url + ''); } //the pad id was fine, so just render it else diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 4f780cb0b..546ba2af6 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -3,6 +3,7 @@ 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 webaccess = require("ep_etherpad-lite/node/hooks/express/webaccess"); var padMessageHandler = require("../../handler/PadMessageHandler"); @@ -10,19 +11,28 @@ 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); + var io = socketio.listen(args.server); /* 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); + + // Use connect's cookie parser, because it knows how to parse signed cookies + connect.cookieParser(webaccess.secret)(data, {}, function(err){ + if(err) { + console.error(err); + accept("Couldn't parse request cookies. ", false); + return; + } + + data.sessionID = data.signedCookies.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); + }); }); }); @@ -62,5 +72,5 @@ exports.expressCreateServer = function (hook_name, args, cb) { socketIORouter.setSocketIO(io); socketIORouter.addComponent("pad", padMessageHandler); - hooks.callAll("socketio", {"app": args.app, "io": io}); + hooks.callAll("socketio", {"app": args.app, "io": io, "server": args.server}); } diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index ffced0476..41bf38805 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -56,10 +56,10 @@ exports.basicAuth = function (req, res, next) { res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); if (req.headers.authorization) { setTimeout(function () { - res.send('Authentication required', 401); + res.send(401, 'Authentication required'); }, 1000); } else { - res.send('Authentication required', 401); + res.send(401, 'Authentication required'); } })); } @@ -88,14 +88,13 @@ exports.basicAuth = function (req, res, next) { }); } -var secret = null; +exports.secret = null; 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 @@ -104,13 +103,14 @@ exports.expressConfigure = function (hook_name, args, cb) { if (!exports.sessionStore) { exports.sessionStore = new express.session.MemoryStore(); - secret = randomString(32); + exports.secret = randomString(32); } + + args.app.use(express.cookieParser(exports.secret)); args.app.sessionStore = exports.sessionStore; args.app.use(express.session({store: args.app.sessionStore, - key: 'express_sid', - secret: secret})); + key: 'express_sid' })); args.app.use(exports.basicAuth); } diff --git a/src/node/padaccess.js b/src/node/padaccess.js index a3d1df332..4388ab946 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -15,7 +15,7 @@ module.exports = function (req, res, callback) { callback(); //no access } else { - res.send("403 - Can't touch this", 403); + res.send(403, "403 - Can't touch this"); } }); } diff --git a/src/node/server.js b/src/node/server.js index cca76c1f9..327fa166f 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -21,27 +21,49 @@ * limitations under the License. */ -var log4js = require('log4js'); -var settings = require('./utils/Settings'); -var db = require('./db/DB'); -var async = require('async'); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); +var log4js = require('log4js') + , async = require('async') + ; + +// set up logger +log4js.replaceConsole(); + +var settings + , db + , plugins + , hooks; var npm = require("npm/lib/npm.js"); -hooks.plugins = plugins; - -//set loglevel -log4js.setGlobalLogLevel(settings.loglevel); - async.waterfall([ + // load npm + function(callback) { + npm.load({}, function(er) { + callback(er) + }) + }, + + // load everything + function(callback) { + settings = require('./utils/Settings'); + db = require('./db/DB'); + plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); + hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); + hooks.plugins = plugins; + + //set loglevel + log4js.setGlobalLogLevel(settings.loglevel); + callback(); + }, + //initalize the database function (callback) { db.init(callback); }, - plugins.update, + function(callback) { + plugins.update(callback) + }, function (callback) { console.info("Installed plugins: " + plugins.formatPlugins()); diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index dd34ac5ee..3d7894d52 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -37,7 +37,7 @@ exports.ip = "0.0.0.0"; /** * The Port ep-lite should listen to */ -exports.port = 9001; +exports.port = process.env.PORT || 9001; /* * The Type of the database */ diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index 1f5336733..c6b237139 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -48,7 +48,7 @@ CachingMiddleware.prototype = new function () { var old_res = {}; var supportsGzip = - req.header('Accept-Encoding', '').indexOf('gzip') != -1; + (req.get('Accept-Encoding') || '').indexOf('gzip') != -1; var path = require('url').parse(req.url).path; var cacheKey = (new Buffer(path)).toString('base64').replace(/[\/\+=]/g, ''); diff --git a/src/package.json b/src/package.json index a4097dc2f..c3c4968a6 100644 --- a/src/package.json +++ b/src/package.json @@ -5,9 +5,10 @@ "keywords" : ["etherpad", "realtime", "collaborative", "editor"], "author" : "Peter 'Pita' Martischka - Primary Technology Ltd", "contributors" : [ - { "name": "John McLear", - "name": "Hans Pinckaers", - "name": "Robin Buse" } + { "name": "John McLear" }, + { "name": "Hans Pinckaers" }, + { "name": "Robin Buse" }, + { "name": "Marcel Klehr" } ], "dependencies" : { "yajsml" : "1.1.6", @@ -15,17 +16,18 @@ "require-kernel" : "1.0.5", "resolve" : "0.2.x", "socket.io" : "0.9.x", - "ueberDB" : "0.1.7", + "ueberDB" : "0.1.8", "async" : "0.1.22", - "express" : "2.5.x", - "connect" : "1.x", + "express" : "3.x", + "connect" : "2.4.x", "clean-css" : "0.3.2", "uglify-js" : "1.2.5", "formidable" : "1.0.9", - "log4js" : "0.4.1", + "log4js" : "0.5.x", "jsdom-nocontextifiy" : "0.2.10", "async-stacktrace" : "0.0.2", - "npm" : "1.1.24", + "npm" : "1.1.x", + "npm-registry-client" : "0.2.10", "ejs" : "0.6.1", "graceful-fs" : "1.1.5", "slide" : "1.1.3", @@ -42,5 +44,5 @@ "engines" : { "node" : ">=0.6.0", "npm" : ">=1.0" }, - "version" : "1.1.2" + "version" : "1.1.4" } diff --git a/src/static/css/pad.css b/src/static/css/pad.css index df9dde143..5ee6b3c56 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -783,6 +783,23 @@ input[type=checkbox] { padding: 4px 1px } } +@media screen and (max-width: 400px){ + #editorcontainer { + top: 68px; + } + #editbar { + height: 62px; + } + .toolbar ul.menu_right { + float: left; + margin-top:2px; + } + .popup { + width:100%; + max-width:300px; + top: 72px !important; + } +} @media only screen and (min-device-width: 320px) and (max-device-width: 720px) { #users { top: 36px; diff --git a/src/static/js/ace.js b/src/static/js/ace.js index e50f75c76..83ad9447b 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -122,6 +122,11 @@ function Ace2Editor() return info.ace_getDebugProperty(prop); }; + editor.getInInternationalComposition = function() + { + return info.ace_getInInternationalComposition(); + }; + // prepareUserChangeset: // Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes // to the latest base text into a Changeset, which is returned (as a string if encodeAsString). diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 652a3d259..2e56b950f 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1173,7 +1173,7 @@ function Ace2Inner(){ //if (! top.BEFORE) top.BEFORE = []; //top.BEFORE.push(magicdom.root.dom.innerHTML); //if (! isEditable) return; // and don't reschedule - if (window.parent.parent.inInternationalComposition) + if (inInternationalComposition) { // don't do idle input incorporation during international input composition idleWorkTimer.atLeast(500); @@ -3729,7 +3729,7 @@ function Ace2Inner(){ thisKeyDoesntTriggerNormalize = true; } - if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!window.parent.parent.inInternationalComposition)) + if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) { if (type != "keyup" || !incorpIfQuick()) { @@ -4589,9 +4589,24 @@ function Ace2Inner(){ } } + + var inInternationalComposition = false; function handleCompositionEvent(evt) { - window.parent.parent.handleCompositionEvent(evt); + // international input events, fired in FF3, at least; allow e.g. Japanese input + if (evt.type == "compositionstart") + { + inInternationalComposition = true; + } + else if (evt.type == "compositionend") + { + inInternationalComposition = false; + } + } + + editorInfo.ace_getInInternationalComposition = function () + { + return inInternationalComposition; } function bindTheEventHandlers() diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index d149b2565..b700fc490 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -111,7 +111,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) function handleUserChanges() { - if (window.parent.parent.inInternationalComposition) return; + if (editor.getInInternationalComposition()) return; if ((!getSocket()) || channelState == "CONNECTING") { if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) @@ -288,7 +288,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) var apool = msg.apool; // When inInternationalComposition, msg pushed msgQueue. - if (msgQueue.length > 0 || window.parent.parent.inInternationalComposition) { + if (msgQueue.length > 0 || editor.getInInternationalComposition()) { if (msgQueue.length > 0) oldRev = msgQueue[msgQueue.length - 1].newRev; else oldRev = rev; @@ -358,6 +358,14 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) { var userInfo = msg.userInfo; var id = userInfo.userId; + + // Avoid a race condition when setting colors. If our color was set by a + // query param, ignore our own "new user" message's color value. + if (id === initialUserInfo.userId && initialUserInfo.globalUserColor) + { + msg.userInfo.colorId = initialUserInfo.globalUserColor; + } + if (userSet[id]) { diff --git a/src/static/js/colorutils.js b/src/static/js/colorutils.js index 5fbefb4df..74a2e4635 100644 --- a/src/static/js/colorutils.js +++ b/src/static/js/colorutils.js @@ -24,6 +24,13 @@ var colorutils = {}; +// Check that a given value is a css hex color value, e.g. +// "#ffffff" or "#fff" +colorutils.isCssHex = function(cssColor) +{ + return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor); +} + // "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] colorutils.css2triple = function(cssColor) { diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 6a75de43a..dd4fd1e54 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -311,7 +311,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class ['insertorder', 'first'] ].concat( _.map(state.lineAttributes,function(value,key){ - if (window.console) console.log([key, value]) + if (typeof(window)!= 'undefined' && window.console) console.log([key, value]) return [key, value]; }) ); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 737f5dc62..897770405 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -43,6 +43,7 @@ var padmodals = require('./pad_modals').padmodals; var padsavedrevs = require('./pad_savedrevs'); var paduserlist = require('./pad_userlist').paduserlist; var padutils = require('./pad_utils').padutils; +var colorutils = require('./colorutils').colorutils; var createCookie = require('./pad_utils').createCookie; var readCookie = require('./pad_utils').readCookie; @@ -50,22 +51,6 @@ var randomString = require('./pad_utils').randomString; var hooks = require('./pluginfw/hooks'); -window.inInternationalComposition = false; -var inInternationalComposition = window.inInternationalComposition; - -window.handleCompositionEvent = function handleCompositionEvent(evt) - { - // international input events, fired in FF3, at least; allow e.g. Japanese input - if (evt.type == "compositionstart") - { - this.inInternationalComposition = true; - } - else if (evt.type == "compositionend") - { - this.inInternationalComposition = false; - } - } - function createCookie(name, value, days, path) { if (days) @@ -114,6 +99,7 @@ function getParams() var showControls = params["showControls"]; var showChat = params["showChat"]; var userName = params["userName"]; + var userColor = params["userColor"]; var showLineNumbers = params["showLineNumbers"]; var useMonospaceFont = params["useMonospaceFont"]; var IsnoColors = params["noColors"]; @@ -162,6 +148,11 @@ function getParams() // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. settings.globalUserName = decodeURIComponent(userName); } + if(userColor) + // If the userColor is set as a parameter, set a global value to use once we have initiated the pad. + { + settings.globalUserColor = decodeURIComponent(userColor); + } if(rtl) { if(rtl == "true") @@ -363,6 +354,14 @@ function handshake() pad.myUserInfo.name = settings.globalUserName; $('#myusernameedit').attr({"value":settings.globalUserName}); // Updates the current users UI } + if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) + { + + // Add a 'globalUserColor' property to myUserInfo, so collabClient knows we have a query parameter. + pad.myUserInfo.globalUserColor = settings.globalUserColor; + pad.notifyChangeColor(settings.globalUserColor); // Updates pad.myUserInfo.colorId + paduserlist.setMyUserInfo(pad.myUserInfo); + } } //This handles every Message after the clientVars else @@ -1025,6 +1024,7 @@ var settings = { , noColors: false , useMonospaceFontGlobal: false , globalUserName: false +, globalUserColor: false , rtlIsTrue: false }; diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index 09f3d79f2..5a9e7b9b6 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -85,11 +85,13 @@ var padeditor = (function() if (value == "false") return false; return defaultValue; } - self.ace.setProperty("rtlIsTrue", settings.rtlIsTrue); var v; + v = getOption('rtlIsTrue', false); + self.ace.setProperty("rtlIsTrue", v); + v = getOption('showLineNumbers', true); self.ace.setProperty("showslinenumbers", v); padutils.setCheckbox($("#options-linenoscheck"), v); diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index e7c6fb809..d668e549e 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -1,7 +1,12 @@ var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var npm = require("npm"); -var registry = require("npm/lib/utils/npm-registry-client/index.js"); +var RegClient = require("npm-registry-client") + +var registry = new RegClient( +{ registry: "http://registry.npmjs.org" +, cache: npm.cache } +); var withNpm = function (npmfn, final, cb) { npm.load({}, function (er) { @@ -72,7 +77,7 @@ exports.search = function(query, cache, cb) { cb(null, exports.searchCache); } else { registry.get( - "/-/all", null, 600, false, true, + "/-/all", 600, false, true, function (er, data) { if (er) return cb(er); exports.searchCache = data; diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 12ba94a27..e02c1331a 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -1,7 +1,5 @@ var npm = require("npm/lib/npm.js"); var readInstalled = require("./read-installed.js"); -var relativize = require("npm/lib/utils/relativize.js"); -var readJson = require("npm/lib/utils/read-json.js"); var path = require("path"); var async = require("async"); var fs = require("fs"); diff --git a/src/static/js/pluginfw/read-installed.js b/src/static/js/pluginfw/read-installed.js index cc03b3579..800ee32cd 100644 --- a/src/static/js/pluginfw/read-installed.js +++ b/src/static/js/pluginfw/read-installed.js @@ -94,8 +94,21 @@ var npm = require("npm/lib/npm.js") , path = require("path") , asyncMap = require("slide").asyncMap , semver = require("semver") - , readJson = require("npm/lib/utils/read-json.js") - , log = require("npm/lib/utils/log.js") + , log = require("log4js").getLogger('pluginfw') + +function readJson(file, callback) { + fs.readFile(file, function(er, buf) { + if(er) { + callback(er); + return; + } + try { + callback( null, JSON.parse(buf.toString()) ) + } catch(er) { + callback(er) + } + }) +} module.exports = readInstalled @@ -274,7 +287,7 @@ function findUnmet (obj) { } }) - log.verbose([obj._id], "returning") + log.debug([obj._id], "returning") return obj } diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index e630bde0e..a2156b16b 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -144,13 +144,12 @@ function handleClientVars(message) require('./pad_impexp').padimpexp.init(); //change export urls when the slider moves - var export_rev_regex = /(\/\d+)?\/export/ BroadcastSlider.onSlider(function(revno) { // export_links is a jQuery Array, so .each is allowed. export_links.each(function() { - this.setAttribute('href', this.href.replace(export_rev_regex, '/' + revno + '/export')); + this.setAttribute('href', this.href.replace( /(.+?)\/\w+\/(\d+\/)?export/ , '$1/' + padId + '/' + revno + '/export')); }); }); diff --git a/src/templates/index.html b/src/templates/index.html index 4a45d6a54..cdd9346de 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -2,6 +2,29 @@ Etherpad Lite + @@ -9,9 +32,11 @@