diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/node/hooks/express/adminplugins.js | 4 | ||||
-rw-r--r-- | src/node/hooks/express/socketio.js | 18 | ||||
-rw-r--r-- | src/node/hooks/express/webaccess.js | 117 | ||||
-rw-r--r-- | src/node/server.js | 5 | ||||
-rw-r--r-- | src/node/utils/Settings.js | 15 | ||||
-rw-r--r-- | src/package.json | 1 | ||||
-rw-r--r-- | src/static/css/admin.css | 261 | ||||
-rw-r--r-- | src/static/js/admin/plugins.js | 132 | ||||
-rw-r--r-- | src/static/js/pad_editbar.js | 12 | ||||
-rw-r--r-- | src/static/js/pluginfw/hooks.js | 64 | ||||
-rw-r--r-- | src/static/js/pluginfw/installer.js | 34 | ||||
-rw-r--r-- | src/templates/admin/plugins.html | 253 | ||||
-rw-r--r-- | src/templates/pad.html | 290 |
13 files changed, 689 insertions, 517 deletions
diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index fa7e7077..7b21206c 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -21,13 +21,15 @@ exports.expressCreateServer = function (hook_name, args, cb) { 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, function (progress) { + installer.search(query, true, function (progress) { if (progress.results) socket.emit("search-result", progress); socket.emit("progress", progress); diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index e040f7ac..6774b653 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -7,11 +7,27 @@ 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']); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 48b5edae..028d8ab1 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -2,55 +2,108 @@ 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) { - - // 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(); + var hookResultMangle = function (cb) { + return function (err, data) { + return cb(!err && data.length && data[0]); + } } - - - // 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(); + + 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)); } - // Do not require auth for static paths...this could be a bit brittle - else if (req.path.match(/^\/(static|javascripts|pluginfw)/)) { - return next(); + + 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)); } - // 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 () { - res.send('Authentication required', 401); - }, 1000); - } else { - res.send('Authentication required', 401); + /* 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) { - 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. 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/server.js b/src/node/server.js index 6b443edb..9d2c52e4 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -30,6 +30,7 @@ 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 = ""; @@ -88,11 +89,11 @@ async.waterfall([ //let the server listen app.listen(settings.port, settings.ip); console.log("Server is listening at " + settings.ip + ":" + settings.port); - if(settings.adminHttpAuth){ + if(!_.isEmpty(settings.users)){ console.log("Plugin admin page listening at " + settings.ip + ":" + settings.port + "/admin/plugins"); } else{ - console.log("Admin username and password not set in settings.json. To access admin please uncomment and edit adminHttpAuth in settings.json"); + console.log("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/src/node/utils/Settings.js b/src/node/utils/Settings.js index 12fcc55c..cb6a6403 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -80,15 +80,12 @@ exports.abiword = null; */ exports.loglevel = "INFO"; -/** - * Http basic auth, with "user:password" format - */ -exports.httpAuth = null; - -/** - * Http basic auth, with "user:password" format - */ -exports.adminHttpAuth = 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() diff --git a/src/package.json b/src/package.json index 83441da0..eda385b2 100644 --- a/src/package.json +++ b/src/package.json @@ -17,6 +17,7 @@ "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", diff --git a/src/static/css/admin.css b/src/static/css/admin.css index 67706fa2..b2dfc1c5 100644 --- a/src/static/css/admin.css +++ b/src/static/css/admin.css @@ -1,138 +1,123 @@ -body { - margin: 0; - height: 100%; - 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 { - border-top: 1px solid #999; - margin-top: 160px; - padding: 15px; - background: #eee; - background: -webkit-linear-gradient(#fff,#ccc); - background: -moz-linear-gradient(#fff,#ccc); - background: -ms-linear-gradient(#fff,#ccc); - background: -o-linear-gradient(#fff,#ccc); - opacity: .9; - box-shadow: 0px 1px 8px rgba(0,0,0,0.3); - max-width: 1200px; - margin: auto; -} -#inner { - width: 300px; - margin: 0 auto; -} -#button:hover { - cursor: pointer; - background: #666; - background: -webkit-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); - background: -moz-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); - background: -ms-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); - background: -o-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); -} -#button:active { - box-shadow: inset 0 1px 12px rgba(0,0,0,0.9); - background: #444; -} -#label { - text-align: left; - text-shadow: 0 1px 1px #fff; - margin: 16px auto 0; -} -form { - border: 1px solid #bbb; - border-radius: 3px; - position: relative; -} -button, input { - font-weight: bold; - font-size: 15px; -} -input[type="button"] { - height:30px; -} -input[type="text"] { - border-radius: 3px; - box-sizing: border-box; - -moz-box-sizing: border-box; - padding: 10px 45px 10px 10px; - *padding: 0; /* IE7 hack */ - width: 100%; - outline: none; - border: none; -} -button{ - display:block; -} -@media only screen and (min-device-width: 320px) and (max-device-width: 720px) { - body { - background: #bbb; - background: -webkit-linear-gradient(#aaa,#eee 60%) center fixed; - background: -moz-linear-gradient(#aaa,#eee 60%) center fixed; - background: -ms-linear-gradient(#aaa,#eee 60%) center fixed; - } - #wrapper { - margin-top: 0; - } - #inner { - width: 95%; - } - #label { - text-align: center; - } -} - -table { - border-collapse: collapse; -} -td, th { - border: 1px solid black; - padding-left: 10px; - padding-right: 10px; - padding-top: 2px; - padding-bottom: 2px; -} -.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 #999999; - background: #eeeeee; -} -.dialog .title { - margin: 0; - padding: 2px; - border-bottom: 3px solid #999999; - font-size: 24px; - line-height: 24px; - height: 24px; - overflow: hidden; -} -.dialog .title .close { - float: right; -} -.dialog .history { - background: #222222; - color: #eeeeee; - position: absolute; - top: 41px; - bottom: 10px; - left: 10px; - right: 10px; - padding: 2px; - overflow: auto; -} +body {
+ margin: 0;
+ height: 100%;
+ 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;
+}
+.seperator {
+ 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/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js new file mode 100644 index 00000000..6ae085c2 --- /dev/null +++ b/src/static/js/admin/plugins.js @@ -0,0 +1,132 @@ +$(document).ready(function () { + var socket = io.connect().of("/pluginfw/installer"); + + $('.search-results').data('query', { + pattern: '', + offset: 0, + limit: 4, + }); + + var doUpdate = false; + + var search = function () { + socket.emit("search", $('.search-results').data('query')); + } + + function updateHandlers() { + $("#progress.dialog .close").unbind('click').click(function () { + $("#progress.dialog").hide(); + }); + + $("#do-search").unbind('click').click(function () { + var query = $('.search-results').data('query'); + query.pattern = $("#search-query")[0].value; + query.offset = 0; + search(); + }); + + $(".do-install").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("install", row.find(".name").html()); + }); + + $(".do-uninstall").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("uninstall", row.find(".name").html()); + }); + + $(".do-prev-page").unbind('click').click(function (e) { + var query = $('.search-results').data('query'); + query.offset -= query.limit; + if (query.offset < 0) { + query.offset = 0; + } + search(); + }); + $(".do-next-page").unbind('click').click(function (e) { + var query = $('.search-results').data('query'); + var total = $('.search-results').data('total'); + if (query.offset + query.limit < total) { + query.offset += query.limit; + } + search(); + }); + } + + updateHandlers(); + + socket.on('progress', function (data) { + if (data.progress > 0 && $('#progress.dialog').data('progress') > data.progress) return; + + $("#progress.dialog .close").hide(); + $("#progress.dialog").show(); + + $('#progress.dialog').data('progress', data.progress); + + var message = "Unknown status"; + if (data.message) { + message = "<span class='status'>" + data.message.toString() + "</span>"; + } + if (data.error) { + message = "<span class='error'>" + data.error.toString() + "<span>"; + } + $("#progress.dialog .message").html(message); + $("#progress.dialog .history").append("<div>" + message + "</div>"); + + if (data.progress >= 1) { + if (data.error) { + $("#progress.dialog .close").show(); + } else { + if (doUpdate) { + doUpdate = false; + socket.emit("load"); + } + $("#progress.dialog").hide(); + } + } + }); + + socket.on('search-result', function (data) { + var widget=$(".search-results"); + + widget.data('query', data.query); + widget.data('total', data.total); + + widget.find('.offset').html(data.query.offset); + widget.find('.limit').html(data.query.offset + data.query.limit); + widget.find('.total').html(data.total); + + widget.find(".results *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = widget.find(".template tr").clone(); + + for (attr in plugin) { + row.find("." + attr).html(plugin[attr]); + } + widget.find(".results").append(row); + } + + updateHandlers(); + }); + + socket.on('installed-results', function (data) { + $("#installed-plugins *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = $("#installed-plugin-template").clone(); + + for (attr in plugin.package) { + row.find("." + attr).html(plugin.package[attr]); + } + $("#installed-plugins").append(row); + } + updateHandlers(); + }); + + socket.emit("load"); + search(); + +}); diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 95d19505..dec32b46 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -182,7 +182,7 @@ var padeditbar = (function() //hide all modules if(moduleName == "none") { - $("#editbar ul#menu_right > li").removeClass("selected"); + $(".toolbar ul.menu_right li").removeClass("selected"); for(var i=0;i<modules.length;i++) { //skip the userlist @@ -200,11 +200,11 @@ var padeditbar = (function() else { var nth_child = indexOf(modules, moduleName) + 1; - if (nth_child > 0 && nth_child <= (modules.length-1)) { - $("#editbar ul#menu_right li:not(:nth-child(" + nth_child + "))").removeClass("selected"); - $("#editbar ul#menu_right li:nth-child(" + nth_child + ")").toggleClass("selected"); - } - if(modules[modules.length-1] === moduleName) $("#editbar ul#menu_right li").removeClass("selected"); + if (nth_child > 0 && nth_child <= (modules.length-1)) { + $(".toolbar ul.menu_right li:not(:nth-child(" + nth_child + "))").removeClass("selected"); + $(".toolbar ul.menu_right li:nth-child(" + nth_child + ")").toggleClass("selected"); + } + if(modules[modules.length-1] === moduleName) $(".toolbar ul.menu_right li").removeClass("selected"); //hide all modules that are not selected and show the selected one for(var i=0;i<modules.length;i++) { diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index c4cd5aeb..49e46c60 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -4,27 +4,63 @@ var _; /* FIXME: Ugly hack, in the future, use same code for server & client */ if (plugins.isClient) { var async = require("ep_etherpad-lite/static/js/pluginfw/async"); - _ = require("ep_etherpad-lite/static/js/underscore"); + var _ = require("ep_etherpad-lite/static/js/underscore"); } else { var async = require("async"); - _ = require("underscore"); + var _ = require("underscore"); } exports.bubbleExceptions = true var hookCallWrapper = function (hook, hook_name, args, cb) { if (cb === undefined) cb = function (x) { return x; }; + + // Normalize output to list for both sync and async cases + var normalize = function(x) { + if (x == undefined) return []; + return x; + } + var normalizedhook = function () { + return normalize(hook.hook_fn(hook_name, args, function (x) { + return cb(normalize(x)); + })); + } + if (exports.bubbleExceptions) { - return hook.hook_fn(hook_name, args, cb); + return normalizedhook(); } else { try { - return hook.hook_fn(hook_name, args, cb); + return normalizedhook(); } catch (ex) { console.error([hook_name, hook.part.full_name, ex.stack || ex]); } } } +exports.syncMapFirst = function (lst, fn) { + var i; + var result; + for (i = 0; i < lst.length; i++) { + result = fn(lst[i]) + if (result.length) return result; + } + return undefined; +} + +exports.mapFirst = function (lst, fn, cb) { + var i = 0; + + next = function () { + if (i >= lst.length) return cb(undefined); + fn(lst[i++], function (err, result) { + if (err) return cb(err); + if (result.length) return cb(null, result); + next(); + }); + } + next(); +} + /* Don't use Array.concat as it flatterns arrays within the array */ exports.flatten = function (lst) { @@ -44,9 +80,9 @@ exports.flatten = function (lst) { exports.callAll = function (hook_name, args) { if (!args) args = {}; if (plugins.hooks[hook_name] === undefined) return []; - return exports.flatten(_.map(plugins.hooks[hook_name], function (hook) { + return _.flatten(_.map(plugins.hooks[hook_name], function (hook) { return hookCallWrapper(hook, hook_name, args); - })); + }), true); } exports.aCallAll = function (hook_name, args, cb) { @@ -59,7 +95,7 @@ exports.aCallAll = function (hook_name, args, cb) { hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); }, function (err, res) { - cb(null, exports.flatten(res)); + cb(null, _.flatten(res, true)); } ); } @@ -67,14 +103,22 @@ exports.aCallAll = function (hook_name, args, cb) { exports.callFirst = function (hook_name, args) { if (!args) args = {}; if (plugins.hooks[hook_name][0] === undefined) return []; - return exports.flatten(hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args)); + return exports.syncMapFirst(plugins.hooks[hook_name], function (hook) { + return hookCallWrapper(hook, hook_name, args); + }); } exports.aCallFirst = function (hook_name, args, cb) { if (!args) args = {}; if (!cb) cb = function () {}; - if (plugins.hooks[hook_name][0] === undefined) return cb(null, []); - hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(null, exports.flatten(res)); }); + if (plugins.hooks[hook_name] === undefined) return cb(null, []); + exports.mapFirst( + plugins.hooks[hook_name], + function (hook, cb) { + hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); + }, + cb + ); } exports.callAllStr = function(hook_name, args, sep, pre, post) { diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index 127a95aa..1bb8db9e 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -55,19 +55,41 @@ exports.install = function(plugin_name, cb) { ); }; -exports.search = function(pattern, cb) { +exports.searchCache = null; + +exports.search = function(query, cache, cb) { withNpm( function (cb) { - registry.get( - "/-/all", null, 600, false, true, + var getData = function (cb) { + if (cache && exports.searchCache) { + cb(null, exports.searchCache); + } else { + registry.get( + "/-/all", null, 600, false, true, + function (er, data) { + if (er) return cb(er); + exports.searchCache = data; + cb(er, data); + } + ); + } + } + getData( function (er, data) { if (er) return cb(er); var res = {}; + var i = 0; for (key in data) { - if (key.indexOf(plugins.prefix) == 0 && key.indexOf(pattern) != -1) - res[key] = data[key]; + if ( key.indexOf(plugins.prefix) == 0 + && key.indexOf(query.pattern) != -1) { + i++; + if (i > query.offset + && i <= query.offset + query.limit) { + res[key] = data[key]; + } + } } - cb(null, {results:res}); + cb(null, {results:res, query: query, total:i}); } ); }, diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index 6f38d048..6e9b4d2e 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -1,166 +1,87 @@ -<html> - <head> - <title>Plugin manager</title> - <link href="../../static/css/admin.css" rel="stylesheet" type="text/css" /> - <script src="../../static/js/jquery.js"></script> - <script src="../../socket.io/socket.io.js"></script> - <script> - $(document).ready(function () { - var socket = io.connect().of("/pluginfw/installer"); - - var doUpdate = false; - - function updateHandlers() { - $("#progress.dialog .close").unbind('click').click(function () { - $("#progress.dialog").hide(); - }); - - $("#do-search").unbind('click').click(function () { - if ($("#search-query")[0].value != "") - socket.emit("search", $("#search-query")[0].value); - }); - - $(".do-install").unbind('click').click(function (e) { - var row = $(e.target).closest("tr"); - doUpdate = true; - socket.emit("install", row.find(".name").html()); - }); - - $(".do-uninstall").unbind('click').click(function (e) { - var row = $(e.target).closest("tr"); - doUpdate = true; - socket.emit("uninstall", row.find(".name").html()); - }); - } - - updateHandlers(); - - socket.on('progress', function (data) { - $("#progress.dialog .close").hide(); - $("#progress.dialog").show(); - var message = "Unknown status"; - if (data.message) { - message = "<span class='status'>" + data.message.toString() + "</span>"; - } - if (data.error) { - message = "<span class='error'>" + data.error.toString() + "<span>"; - } - $("#progress.dialog .message").html(message); - $("#progress.dialog .history").append("<div>" + message + "</div>"); - - if (data.progress >= 1) { - if (data.error) { - $("#progress.dialog .close").show(); - } else { - if (doUpdate) { - doUpdate = false; - socket.emit("load"); - } - $("#progress.dialog").hide(); - } - } - }); - - socket.on('search-result', function (data) { - $("#search-results *").remove(); - for (plugin_name in data.results) { - var plugin = data.results[plugin_name]; - var row = $("#search-result-template").clone(); - - for (attr in plugin) { - row.find("." + attr).html(plugin[attr]); - } - $("#search-results").append(row); - } - updateHandlers(); - }); - - socket.on('installed-results', function (data) { - $("#installed-plugins *").remove(); - for (plugin_name in data.results) { - var plugin = data.results[plugin_name]; - var row = $("#installed-plugin-template").clone(); - - for (attr in plugin.package) { - row.find("." + attr).html(plugin.package[attr]); - } - $("#installed-plugins").append(row); - } - updateHandlers(); - }); - - socket.emit("load"); - - }); - </script> - </head> - <body> - <div id="wrapper"> - - <% if (errors.length) { %> - <div class="errors"> - <% errors.forEach(function (item) { %> - <div class="error"><%= item.toString() %></div> - <% }) %> - </div> - <% } %> - - - <h1>Installed plugins</h1> - <table> - <thead> - <tr> - <th>Name</th> - <th>Description</th> - <td></td> - </tr> - </thead> - <tbody class="template"> - <tr id="installed-plugin-template"> - <td class="name"></td> - <td class="description"></td> - <td class="actions"> - <input type="button" value="Uninstall" class="do-uninstall"> - </td> - </tr> - </tbody> - <tbody id="installed-plugins"> - </tbody> - </table> - - <h1>Search for plugins to install</h1> - <form> - <input type="text" name="search" value="" id="search-query"> - <input type="button" value="Search" id="do-search"> - </form> - <table> - <thead> - <tr> - <th>Name</th> - <th>Description</th> - <td></td> - </tr> - </thead> - <tbody class="template"> - <tr id="search-result-template"> - <td class="name"></td> - <td class="description"></td> - <td class="actions"> - <input type="button" value="Install" class="do-install"> - </td> - </tr> - </tbody> - <tbody id="search-results"> - </tbody> - </table> - - <div id="progress" class="dialog"> - <h1 class="title"> - Please wait: <span class="message"></span> - <input type="button" class="close" value="Close"> - </h1> - <div class="history"></div> - </div> - </div> - </body> -</html> +<html>
+ <head>
+ <title>Plugin manager</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
+ <link rel="stylesheet" href="../../static/css/admin.css">
+ <script src="../../static/js/jquery.js"></script>
+ <script src="../../socket.io/socket.io.js"></script>
+ <script src="../../static/js/admin/plugins.js"></script>
+ </head>
+ <body>
+ <div id="wrapper">
+
+ <% if (errors.length) { %>
+ <div class="errors">
+ <% errors.forEach(function (item) { %>
+ <div class="error"><%= item.toString() %></div>
+ <% }) %>
+ </div>
+ <% } %>
+
+
+ <h1>Etherpad Lite</h1>
+ <div class="seperator"></div>
+ <h2>Installed plugins</h2>
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Description</th>
+ <td></td>
+ </tr>
+ </thead>
+ <tbody class="template">
+ <tr id="installed-plugin-template">
+ <td class="name"></td>
+ <td class="description"></td>
+ <td class="actions">
+ <input type="button" value="Uninstall" class="do-uninstall">
+ </td>
+ </tr>
+ </tbody>
+ <tbody id="installed-plugins">
+ </tbody>
+ </table>
+
+ <div class="paged listing search-results">
+ <div class="seperator"></div>
+ <h2>Search for plugins to install</h2>
+ <form>
+ <input type="text" name="search" placeholder="Search term" id="search-query">
+ <input type="button" value="Search" id="do-search">
+ </form>
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Description</th>
+ <td></td>
+ </tr>
+ </thead>
+ <tbody class="template">
+ <tr>
+ <td class="name"></td>
+ <td class="description"></td>
+ <td class="actions">
+ <input type="button" value="Install" class="do-install">
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="results">
+ </tbody>
+ </table>
+ <input type="button" value="<<" class="do-prev-page">
+ <span class="offset"></span>..<span class="limit"></span> of <span class="total"></span>.
+ <input type="button" value=">>" class="do-next-page">
+ </div>
+
+ <div id="progress" class="dialog">
+ <h1 class="title">
+ Please wait: <span class="message"></span>
+ <input type="button" class="close" value="Close">
+ </h1>
+ <div class="history"></div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/src/templates/pad.html b/src/templates/pad.html index 91583b36..6181b062 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -11,79 +11,77 @@ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> <% e.begin_block("styles"); %> - <link href="../static/css/pad.css" rel="stylesheet"> - <link href="../static/custom/pad.css" rel="stylesheet"> - <style title="dynamicsyntax"></style> + <link href="../static/css/pad.css" rel="stylesheet"> + <link href="../static/custom/pad.css" rel="stylesheet"> + <style title="dynamicsyntax"></style> <% e.end_block(); %> <!-- head and body had been removed intentionally --> <div id="editbar" class="toolbar"> <ul class="menu_left"> - <% e.begin_block("editbarMenuLeft"); %> - <li id="bold" onClick="window.pad&&pad.editbarClick('bold');return false"> - <a class="buttonicon buttonicon-bold" title="Bold (ctrl-B)"></a> - </li> - <li id="italic" onClick="window.pad&&pad.editbarClick('italic'); return false;"> - <a class="buttonicon buttonicon-italic" title="Italics (ctrl-I)"></a> - </li> - <li id="underline" onClick="window.pad&&pad.editbarClick('underline');return false;" > - <a class="buttonicon buttonicon-underline" title="Underline (ctrl-U)"></a> - </li> - <li id="strikethrough" onClick="window.pad&&pad.editbarClick('strikethrough');return false;"> - <a class="buttonicon buttonicon-strikethrough" title="Strikethrough"></a> - </li> - <li class="separator"></li> - <li id="oderedlist" onClick="window.pad&&pad.editbarClick('insertorderedlist');return false;"> - <a class="buttonicon buttonicon-insertorderedlist" title="Toggle Ordered List"></a> - </li> - <li id="unoderedlist" onClick="window.pad&&pad.editbarClick('insertunorderedlist');return false;"> - <a class="buttonicon buttonicon-insertunorderedlist" title="Toggle Bullet List"></a> - </li> - <li id="indent" onClick="window.pad&&pad.editbarClick('indent');return false;"> - <a class="buttonicon buttonicon-indent" title="Indent"></a> - </li> - <li id="outdent" onClick="window.pad&&pad.editbarClick('outdent');return false;"> - <a class="buttonicon buttonicon-outdent" title="Unindent"></a> - </li> - <li class="separator"></li> - <li id="undo" onClick="window.pad&&pad.editbarClick('undo');return false;"> - <a class="buttonicon buttonicon-undo" title="Undo (ctrl-Z)"></a> - </li> - <li id="redo" onClick="window.pad&&pad.editbarClick('redo');return false;"> - <a class="buttonicon buttonicon-redo" title="Redo (ctrl-Y)"></a> - </li> - <li class="separator"></li> - <li id="clearAuthorship" onClick="window.pad&&pad.editbarClick('clearauthorship');return false;"> - <a class="buttonicon buttonicon-clearauthorship" title="Clear Authorship Colors"></a> - </li> - <% e.end_block(); %> - </ul> + <% e.begin_block("editbarMenuLeft"); %> + <li id="bold" onClick="window.pad&&pad.editbarClick('bold');return false"> + <a class="buttonicon buttonicon-bold" title="Bold (ctrl-B)"></a> + </li> + <li id="italic" onClick="window.pad&&pad.editbarClick('italic'); return false;"> + <a class="buttonicon buttonicon-italic" title="Italics (ctrl-I)"></a> + </li> + <li id="underline" onClick="window.pad&&pad.editbarClick('underline');return false;" > + <a class="buttonicon buttonicon-underline" title="Underline (ctrl-U)"></a> + </li> + <li id="strikethrough" onClick="window.pad&&pad.editbarClick('strikethrough');return false;"> + <a class="buttonicon buttonicon-strikethrough" title="Strikethrough"></a> + </li> + <li class="separator"></li> + <li id="oderedlist" onClick="window.pad&&pad.editbarClick('insertorderedlist');return false;"> + <a class="buttonicon buttonicon-insertorderedlist" title="Toggle Ordered List"></a> + </li> + <li id="unoderedlist" onClick="window.pad&&pad.editbarClick('insertunorderedlist');return false;"> + <a class="buttonicon buttonicon-insertunorderedlist" title="Toggle Bullet List"></a> + </li> + <li id="indent" onClick="window.pad&&pad.editbarClick('indent');return false;"> + <a class="buttonicon buttonicon-indent" title="Indent"></a> + </li> + <li id="outdent" onClick="window.pad&&pad.editbarClick('outdent');return false;"> + <a class="buttonicon buttonicon-outdent" title="Unindent"></a> + </li> + <li class="separator"></li> + <li id="undo" onClick="window.pad&&pad.editbarClick('undo');return false;"> + <a class="buttonicon buttonicon-undo" title="Undo (ctrl-Z)"></a> + </li> + <li id="redo" onClick="window.pad&&pad.editbarClick('redo');return false;"> + <a class="buttonicon buttonicon-redo" title="Redo (ctrl-Y)"></a> + </li> + <li class="separator"></li> + <li id="clearAuthorship" onClick="window.pad&&pad.editbarClick('clearauthorship');return false;"> + <a class="buttonicon buttonicon-clearauthorship" title="Clear Authorship Colors"></a> + </li> + <% e.end_block(); %> + </ul> <ul class="menu_right"> - <% e.begin_block("editbarMenuRight"); %> - <li onClick="window.pad&&pad.editbarClick('savedRevision');return false;"> - <a id="settingslink" title="Mark this revision as a saved revision"> - <div class="buttonicon buttonicon-savedRevision"></div> - </a> - </li> - <li id="settingslink" onClick="window.pad&&pad.editbarClick('settings');return false;"> - <a class="buttonicon buttonicon-settings" id="settingslink" title="Settings of this pad"></a> - </li> - <li id="importexportlink" onClick="window.pad&&pad.editbarClick('import_export');return false;"> - <a class="buttonicon buttonicon-import_export" id="exportlink" title="Import/Export from/to different document formats"></a> - </li> - <li id="embedlink" onClick="window.pad&&pad.editbarClick('embed');return false;" > - <a class="buttonicon buttonicon-embed" id="embedlink" title="Share and Embed this pad"></a> - </li> - <li class="separator"></li> - <li id="timesliderlink" onClick="document.location = document.location.pathname+ '/timeslider'"> - <a class="buttonicon buttonicon-history" title="Show the history of this pad"></a> - </li> - <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" title="Show connected users"> - <span class="buttonicon buttonicon-showusers" id="usericonback"></span> - <span id="online_count">1</span> - </li> - <% e.end_block(); %> + <% e.begin_block("editbarMenuRight"); %> + <li id="settingslink" onClick="window.pad&&pad.editbarClick('settings');return false;"> + <a class="buttonicon buttonicon-settings" title="Settings of this pad"></a> + </li> + <li id="importexportlink" onClick="window.pad&&pad.editbarClick('import_export');return false;"> + <a class="buttonicon buttonicon-import_export" title="Import/Export from/to different document formats"></a> + </li> + <li id="embedlink" onClick="window.pad&&pad.editbarClick('embed');return false;"> + <a class="buttonicon buttonicon-embed" title="Share and Embed this pad"></a> + </li> + <li id="revisionlink" onClick="window.pad&&pad.editbarClick('savedRevision');return false;"> + <a class="buttonicon buttonicon-savedRevision" title="Mark this revision as a saved revision"></a> + </li> + <li class="separator"></li> + <li id="timesliderlink" onClick="document.location = document.location.pathname+ '/timeslider'"> + <a class="buttonicon buttonicon-history" title="Show the history of this pad"></a> + </li> + <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" title="Show connected users"> + <span class="buttonicon buttonicon-showusers" id="usericonback"></span> + <span id="online_count">1</span> + </li> + <% e.end_block(); %> </ul> </div> @@ -95,17 +93,17 @@ <button id="mycolorpickersave">Save</button> <button id="mycolorpickercancel">Cancel</button> <span id="mycolorpickerpreview" class="myswatchboxhoverable"></span> - </div> + </div> <div id="myswatchbox"><div id="myswatch"></div></div> <div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled"></div> - <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div> + <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div> </div> <div id="otherusers"> <div id="guestprompts"></div> <table id="otheruserstable" cellspacing="0" cellpadding="0" border="0"> <tr><td></td></tr> </table> - <div id="nootherusers"></div> + <div id="nootherusers"></div> </div> <div id="userlistbuttonarea"></div> </div> @@ -150,20 +148,20 @@ <div class="column"> <h2>Import from text file, HTML, PDF, Word, ODT or RTF</h2><br> <form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data"> - <div class="importformdiv" id="importformfilediv"> - <input type="file" name="file" size="15" id="importfileinput"> - <div class="importmessage" id="importmessagefail"></div> - </div> + <div class="importformdiv" id="importformfilediv"> + <input type="file" name="file" size="15" id="importfileinput"> + <div class="importmessage" id="importmessagefail"></div> + </div> <div id="import"></div> - <div class="importmessage" id="importmessagesuccess">Successful!</div> - <div class="importformdiv" id="importformsubmitdiv"> - <input type="hidden" name="padId" value="blpmaXT35R"> - <span class="nowrap"> - <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput"> - <img alt="" id="importstatusball" src="../static/img/loading.gif" align="top"> - <img alt="" id="importarrow" src="../static/img/leftarrow.png" align="top"> - </span> - </div> + <div class="importmessage" id="importmessagesuccess">Successful!</div> + <div class="importformdiv" id="importformsubmitdiv"> + <input type="hidden" name="padId" value="blpmaXT35R"> + <span class="nowrap"> + <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput"> + <img alt="" id="importstatusball" src="../static/img/loading.gif" align="top"> + <img alt="" id="importarrow" src="../static/img/leftarrow.png" align="top"> + </span> + </div> </form> </div> <div class="column"> @@ -223,79 +221,79 @@ <div id="modaloverlay"> <div id="modaloverlay-inner"></div> </div> - + <div id="mainmodals"> <% e.begin_block("modals"); %> - <div id="connectionbox" class="modaldialog"> - <div id="connectionboxinner" class="modaldialog-inner"> - <div class="connecting">Connecting...</div> - <div class="reconnecting">Reestablishing connection...</div> - <div class="disconnected"> - <h2 class="h2_disconnect">Disconnected.</h2> - <h2 class="h2_userdup">Opened in another window.</h2> - <h2 class="h2_unauth">No Authorization.</h2> - <div id="disconnected_looping"> - <p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p> - </div> - <div id="disconnected_initsocketfail"> - <p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p> - </div> - <div id="disconnected_userdup"> - <p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p> - </div> - <div id="disconnected_unknown"> - <p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p> - </div> - <div id="disconnected_slowcommit"> - <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p> - </div> - <div id="disconnected_unauth"> - <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p> - </div> - <div id="disconnected_deleted"> - <p>This pad was deleted.</p> - </div> - <div id="reconnect_advise"> - <p>If this continues to happen, please let us know</p> - </div> - <div id="reconnect_form"> - <button id="forcereconnect">Reconnect Now</button> - </div> + <div id="connectionbox" class="modaldialog"> + <div id="connectionboxinner" class="modaldialog-inner"> + <div class="connecting">Connecting...</div> + <div class="reconnecting">Reestablishing connection...</div> + <div class="disconnected"> + <h2 class="h2_disconnect">Disconnected.</h2> + <h2 class="h2_userdup">Opened in another window.</h2> + <h2 class="h2_unauth">No Authorization.</h2> + <div id="disconnected_looping"> + <p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p> + </div> + <div id="disconnected_initsocketfail"> + <p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p> + </div> + <div id="disconnected_userdup"> + <p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p> + </div> + <div id="disconnected_unknown"> + <p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p> + </div> + <div id="disconnected_slowcommit"> + <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p> + </div> + <div id="disconnected_unauth"> + <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p> + </div> + <div id="disconnected_deleted"> + <p>This pad was deleted.</p> + </div> + <div id="reconnect_advise"> + <p>If this continues to happen, please let us know</p> + </div> + <div id="reconnect_form"> + <button id="forcereconnect">Reconnect Now</button> </div> </div> - <form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;"> - <input type="hidden" class="padId" name="padId"> - <input type="hidden" class="diagnosticInfo" name="diagnosticInfo"> - <input type="hidden" class="missedChanges" name="missedChanges"> - </form> </div> + <form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;"> + <input type="hidden" class="padId" name="padId"> + <input type="hidden" class="diagnosticInfo" name="diagnosticInfo"> + <input type="hidden" class="missedChanges" name="missedChanges"> + </form> + </div> <% e.end_block(); %> </div> - + <% e.begin_block("scripts"); %> - <script type="text/javascript" src="../static/js/require-kernel.js"></script> - <script type="text/javascript" src="../static/js/jquery.js"></script> - <script type="text/javascript" src="../socket.io/socket.io.js"></script> - <script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define"></script> - <script type="text/javascript"> - document.domain = document.domain; - var clientVars = {}; - (function () { - require.setRootURI("../javascripts/src"); - require.setLibraryURI("../javascripts/lib"); - require.setGlobalKeyPath("require"); + <script type="text/javascript" src="../static/js/require-kernel.js"></script> + <script type="text/javascript" src="../static/js/jquery.js"></script> + <script type="text/javascript" src="../socket.io/socket.io.js"></script> + <script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define"></script> + <script type="text/javascript"> + document.domain = document.domain; + var clientVars = {}; + (function () { + require.setRootURI("../javascripts/src"); + require.setLibraryURI("../javascripts/lib"); + require.setGlobalKeyPath("require"); - var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); - plugins.update(function () { - require('ep_etherpad-lite/static/js/pad').init(); - }); + var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + plugins.update(function () { + require('ep_etherpad-lite/static/js/pad').init(); + }); - /* TODO: These globals shouldn't exist. */ - pad = require('ep_etherpad-lite/static/js/pad').pad; - chat = require('ep_etherpad-lite/static/js/chat').chat; - padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; - padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; - }()); - </script> + /* TODO: These globals shouldn't exist. */ + pad = require('ep_etherpad-lite/static/js/pad').pad; + chat = require('ep_etherpad-lite/static/js/chat').chat; + padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; + padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; + }()); + </script> <% e.end_block(); %> </html> |