aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--etherpad/src/etherpad/control/static_control.js15
-rw-r--r--etherpad/src/etherpad/utils.js52
-rw-r--r--etherpad/src/main.js12
-rw-r--r--infrastructure/framework-src/modules/execution.js5
-rw-r--r--infrastructure/framework-src/modules/sqlbase/sqlobj.js50
-rw-r--r--trunk/README.hooks24
-rw-r--r--trunk/etherpad/src/etherpad/admin/plugins.js247
-rw-r--r--trunk/etherpad/src/etherpad/control/admin/pluginmanager.js65
-rw-r--r--trunk/etherpad/src/plugins/kafoo/main.js16
-rw-r--r--trunk/etherpad/src/plugins/testplugin/controllers/testplugin.js57
-rw-r--r--trunk/etherpad/src/plugins/testplugin/hooks.js15
-rw-r--r--trunk/etherpad/src/plugins/testplugin/main.js23
-rw-r--r--trunk/etherpad/src/plugins/testplugin/static/js/main.js11
-rw-r--r--trunk/etherpad/src/plugins/testplugin/static/js/test.js1
-rw-r--r--trunk/etherpad/src/plugins/testplugin/templates/testplugin.ejs29
-rw-r--r--trunk/etherpad/src/static/js/plugins.js19
-rw-r--r--trunk/etherpad/src/templates/admin/pluginmanager.ejs126
18 files changed, 742 insertions, 26 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b25c15b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*~
diff --git a/etherpad/src/etherpad/control/static_control.js b/etherpad/src/etherpad/control/static_control.js
index 5c087b6..d938b26 100644
--- a/etherpad/src/etherpad/control/static_control.js
+++ b/etherpad/src/etherpad/control/static_control.js
@@ -19,12 +19,25 @@ import("dispatch.{Dispatcher,PrefixMatcher,forward}");
import("etherpad.utils.*");
import("etherpad.globals.*");
+import("etherpad.admin.plugins");
function onRequest() {
var staticBase = '/static';
var opts = {cache: isProduction()};
+ var disp = new Dispatcher();
+
+ /* FIXME: Is there a more effective way to do this? */
+ for (plugin in plugins.plugins) {
+ disp.addLocations([
+ [PrefixMatcher('/static/js/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/js/', opts)],
+ [PrefixMatcher('/static/css/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/css/', opts)],
+ [PrefixMatcher('/static/swf/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/swf/', opts)],
+ [PrefixMatcher('/static/html/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/html/', opts)],
+ [PrefixMatcher('/static/zip/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/zip/', opts)]]);
+ }
+
var serveFavicon = faststatic.singleFileServer(staticBase + '/favicon.ico', opts);
var serveCrossDomain = faststatic.singleFileServer(staticBase + '/crossdomain.xml', opts);
var serveStaticDir = faststatic.directoryServer(staticBase, opts);
@@ -35,8 +48,6 @@ function onRequest() {
var serveHtml = faststatic.directoryServer(staticBase+'/html/', opts);
var serveZip = faststatic.directoryServer(staticBase+'/zip/', opts);
- var disp = new Dispatcher();
-
disp.addLocations([
['/favicon.ico', serveFavicon],
['/robots.txt', serveRobotsTxt],
diff --git a/etherpad/src/etherpad/utils.js b/etherpad/src/etherpad/utils.js
index da9972f..e60c08a 100644
--- a/etherpad/src/etherpad/utils.js
+++ b/etherpad/src/etherpad/utils.js
@@ -34,6 +34,8 @@ import("etherpad.pro.pro_utils");
import("etherpad.pro.pro_config");
import("etherpad.pro.pro_accounts");
import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.log");
+import("etherpad.admin.plugins");
jimport("java.lang.System.out.print");
jimport("java.lang.System.out.println");
@@ -55,11 +57,18 @@ function randomUniquePadId() {
// template rendering
//----------------------------------------------------------------
-function renderTemplateAsString(filename, data) {
+function findTemplate(filename, plugin) {
+ if (plugin != undefined)
+ return '/plugins/' + plugin + '/templates/' + filename;
+ else
+ return '/templates/' + filename;
+}
+
+function renderTemplateAsString(filename, data, plugin) {
data = data || {};
data.helpers = helpers; // global helpers
- var f = "/templates/"+filename;
+ var f = findTemplate(filename, plugin); //"/templates/"+filename;
if (! appjet.scopeCache.ejs) {
appjet.scopeCache.ejs = {};
}
@@ -75,22 +84,25 @@ function renderTemplateAsString(filename, data) {
return html;
}
-function renderTemplate(filename, data) {
- response.write(renderTemplateAsString(filename, data));
+function renderTemplate(filename, data, plugin) {
+ response.write(renderTemplateAsString(filename, data, plugin));
if (request.acceptsGzip) {
response.setGzip(true);
}
}
-function renderHtml(bodyFileName, data) {
- var bodyHtml = renderTemplateAsString(bodyFileName, data);
+function renderHtml(bodyFileName, data, plugin) {
+ var bodyHtml = renderTemplateAsString(bodyFileName, data, plugin);
+ bodyHtml = plugins.callHookStr("renderPageBodyPre", {bodyFileName:bodyFileName, data:data, plugin:plugin}) +
+ bodyHtml +
+ plugins.callHookStr("renderPageBodyPost", {bodyFileName:bodyFileName, data:data, plugin:plugin});
response.write(renderTemplateAsString("html.ejs", {bodyHtml: bodyHtml}));
if (request.acceptsGzip) {
response.setGzip(true);
}
}
-function renderFramedHtml(contentHtml) {
+function renderFramedHtml(contentHtml, plugin) {
var getContentHtml;
if (typeof(contentHtml) == 'function') {
getContentHtml = contentHtml;
@@ -109,52 +121,52 @@ function renderFramedHtml(contentHtml) {
getContentHtml: getContentHtml,
isProDomainRequest: isProDomainRequest(),
renderGlobalProNotice: pro_utils.renderGlobalProNotice
- });
+ }, plugin);
}
-function renderFramed(bodyFileName, data) {
+function renderFramed(bodyFileName, data, plugin) {
function _getContentHtml() {
- return renderTemplateAsString(bodyFileName, data);
+ return renderTemplateAsString(bodyFileName, data, plugin);
}
renderFramedHtml(_getContentHtml);
}
-function renderFramedError(error) {
+function renderFramedError(error, plugin) {
var content = DIV({className: 'fpcontent'},
DIV({style: "padding: 2em 1em;"},
DIV({style: "padding: 1em; border: 1px solid #faa; background: #fdd;"},
B("Error: "), error)));
- renderFramedHtml(content);
+ renderFramedHtml(content, plugin);
}
-function renderNotice(bodyFileName, data) {
- renderNoticeString(renderTemplateAsString(bodyFileName, data));
+function renderNotice(bodyFileName, data, plugin) {
+ renderNoticeString(renderTemplateAsString(bodyFileName, data, plugin), plugin);
}
-function renderNoticeString(contentHtml) {
- renderFramed("notice.ejs", {content: contentHtml});
+function renderNoticeString(contentHtml, plugin) {
+ renderFramed("notice.ejs", {content: contentHtml}, plugin);
}
-function render404(noStop) {
+function render404(noStop, plugin) {
response.reset();
response.setStatusCode(404);
renderFramedHtml(DIV({className: "fpcontent"},
DIV({style: "padding: 2em 1em;"},
DIV({style: "border: 1px solid #aaf; background: #def; padding: 1em; font-size: 150%;"},
- "404 not found: "+request.path))));
+ "404 not found: "+request.path))), plugin);
if (! noStop) {
response.stop();
}
}
-function render500(ex) {
+function render500(ex, plugin) {
response.reset();
response.setStatusCode(500);
var trace = null;
if (ex && (!isProduction())) {
trace = exceptionutils.getStackTracePlain(ex);
}
- renderFramed("500_body.ejs", {trace: trace});
+ renderFramed("500_body.ejs", {trace: trace}, plugin);
}
function _renderEtherpadDotComHeader(data) {
diff --git a/etherpad/src/main.js b/etherpad/src/main.js
index 9cc1db2..8b08abb 100644
--- a/etherpad/src/main.js
+++ b/etherpad/src/main.js
@@ -34,6 +34,7 @@ import("etherpad.importexport.importexport");
import("etherpad.legacy_urls");
import("etherpad.control.aboutcontrol");
+import("etherpad.control.admin.pluginmanager");
import("etherpad.control.admincontrol");
import("etherpad.control.blogcontrol");
import("etherpad.control.connection_diagnostics_control");
@@ -68,6 +69,8 @@ import("etherpad.pad.dbwriter");
import("etherpad.pad.pad_migrations");
import("etherpad.pad.noprowatcher");
+import("etherpad.admin.plugins");
+
jimport("java.lang.System.out.println");
serverhandlers.startupHandler = function() {
@@ -92,6 +95,8 @@ serverhandlers.startupHandler = function() {
team_billing.onStartup();
collabroom_server.onStartup();
readLatestSessionsFromDisk();
+
+ plugins.callHook('serverStartup');
};
serverhandlers.resetHandler = function() {
@@ -99,6 +104,8 @@ serverhandlers.resetHandler = function() {
}
serverhandlers.shutdownHandler = function() {
+ plugins.callHook('serverShutdown');
+
appjet.cache.shutdownHandlerIsRunning = true;
log.callCatchingExceptions(writeSessionsToDisk);
@@ -353,6 +360,8 @@ function handlePath() {
// Default. Can be overridden in case of static files.
response.neverCache();
+ plugins.registerClientHandlerJS();
+
// these paths are handled identically on all sites/subdomains.
var commonDispatcher = new Dispatcher();
commonDispatcher.addLocations([
@@ -367,7 +376,7 @@ function handlePath() {
[DirMatcher('/ep/unit-tests/'), forward(testcontrol)],
[DirMatcher('/ep/pne-manual/'), forward(pne_manual_control)],
[DirMatcher('/ep/pro-help/'), forward(pro_help_control)]
- ]);
+ ].concat(plugins.callHook('handlePath')));
var etherpadDotComDispatcher = new Dispatcher();
etherpadDotComDispatcher.addLocations([
@@ -375,6 +384,7 @@ function handlePath() {
[DirMatcher('/ep/beta-account/'), forward(pro_beta_control)],
[DirMatcher('/ep/pro-signup/'), forward(pro_signup_control)],
[DirMatcher('/ep/about/'), forward(aboutcontrol)],
+ [DirMatcher('/ep/admin/pluginmanager'), forward(pluginmanager)],
[DirMatcher('/ep/admin/'), forward(admincontrol)],
[DirMatcher('/ep/blog/posts/'), blogcontrol.render_post],
[DirMatcher('/ep/blog/'), forward(blogcontrol)],
diff --git a/infrastructure/framework-src/modules/execution.js b/infrastructure/framework-src/modules/execution.js
index 1cec418..2f9d933 100644
--- a/infrastructure/framework-src/modules/execution.js
+++ b/infrastructure/framework-src/modules/execution.js
@@ -44,8 +44,11 @@ function fancyAssEval(initCode, mainCode) {
1);
}
var runner = Packages.net.appjet.oui.ScopeReuseManager.getEmpty(scalaF1(init));
+ var requestWrapper = null;
+ if (request.underlying !== undefined)
+ requestWrapper = new Packages.net.appjet.oui.RequestWrapper(request.underlying);
var ec = new Packages.net.appjet.oui.ExecutionContext(
- new Packages.net.appjet.oui.RequestWrapper(request.underlying),
+ requestWrapper,
null, runner);
return Packages.net.appjet.oui.ExecutionContextUtils.withContext(ec,
scalaF0(function() {
diff --git a/infrastructure/framework-src/modules/sqlbase/sqlobj.js b/infrastructure/framework-src/modules/sqlbase/sqlobj.js
index 4bc1263..e599c92 100644
--- a/infrastructure/framework-src/modules/sqlbase/sqlobj.js
+++ b/infrastructure/framework-src/modules/sqlbase/sqlobj.js
@@ -17,6 +17,7 @@
import("cache_utils.syncedWithCache");
import("sqlbase.sqlcommon.*");
import("jsutils.*");
+import("etherpad.log");
jimport("java.lang.System.out.println");
jimport("java.sql.Statement");
@@ -112,10 +113,13 @@ function _getJsValFromResultSet(rs, type, colName) {
} else {
r = null;
}
- } else if (type == java.sql.Types.INTEGER ||
+ } else if (type == java.sql.Types.BIGINT ||
+ type == java.sql.Types.INTEGER ||
type == java.sql.Types.SMALLINT ||
type == java.sql.Types.TINYINT) {
r = rs.getInt(colName);
+ } else if (type == java.sql.Types.DECIMAL) {
+ r = rs.getFloat(colName);
} else if (type == java.sql.Types.BIT) {
r = rs.getBoolean(colName);
} else {
@@ -192,8 +196,9 @@ function _resultRowToJsObj(resultSet) {
var metaData = resultSet.getMetaData();
var colCount = metaData.getColumnCount();
+
for (var i = 1; i <= colCount; i++) {
- var colName = metaData.getColumnName(i);
+ var colName = metaData.getColumnLabel(i);
var type = metaData.getColumnType(i);
resultObj[colName] = _getJsValFromResultSet(resultSet, type, colName);
}
@@ -338,6 +343,47 @@ function selectMulti(tableName, constraints, options) {
});
}
+function executeRaw(stmnt, params) {
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ for (var i = 0; i < params.length; i++) {
+ var v = params[i];
+
+ if (v === undefined) {
+ throw Error("value is undefined for key "+i);
+ }
+
+ if (typeof(v) == 'object' && v.isnull) {
+ pstmnt.setNull(i+1, v.type);
+ } else if (typeof(v) == 'string') {
+ pstmnt.setString(i+1, v);
+ } else if (typeof(v) == 'number') {
+ pstmnt.setInt(i+1, v);
+ } else if (typeof(v) == 'boolean') {
+ pstmnt.setBoolean(i+1, v);
+ } else if (v.valueOf && v.getDate && v.getHours) {
+ pstmnt.setTimestamp(i+1, new java.sql.Timestamp(+v));
+ } else {
+ throw Error("Cannot insert this type of javascript object: "+typeof(v)+" (key="+i+", value = "+v+")");
+ }
+ }
+
+ _qdebug(stmnt);
+ var resultSet = pstmnt.executeQuery();
+ var resultArray = [];
+
+ return closing(resultSet, function() {
+ while (resultSet.next()) {
+ resultArray.push(_resultRowToJsObj(resultSet));
+ }
+
+ return resultArray;
+ });
+ });
+ });
+}
+
/* returns number of rows updated */
function update(tableName, constraints, obj) {
var objKeys = keys(obj);
diff --git a/trunk/README.hooks b/trunk/README.hooks
new file mode 100644
index 0000000..d15949c
--- /dev/null
+++ b/trunk/README.hooks
@@ -0,0 +1,24 @@
+Hooks that plugins can provide
+
+All hooks must return either undefined/null or a list of return values. This might be an empty list or a list of just one value.
+
+handlePath
+ Registers new urls to serve
+ Parameters: None
+ Returns: Parameter suitable for Dispatcher
+renderPageBodyPre
+ Adds extra html before the body of a page
+ Parameters: bodyFileName, data, plugin
+ Returns: String(s) of html
+renderPageBodyPost
+ Adds extra html after the body of a page
+ Parameters: bodyFileName, data, plugin
+ Returns: String(s) of html
+serverStartup
+ Run right after server startup
+ Parameters: None
+ Returns: None
+serverShutdown
+ Run before server shutdown
+ Parameters: None
+ Returns: None
diff --git a/trunk/etherpad/src/etherpad/admin/plugins.js b/trunk/etherpad/src/etherpad/admin/plugins.js
new file mode 100644
index 0000000..41482fc
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/admin/plugins.js
@@ -0,0 +1,247 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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.
+ */
+
+import("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("exceptionutils");
+import("execution");
+
+jimport("java.io.File",
+ "java.io.DataInputStream",
+ "java.io.FileInputStream",
+ "java.lang.Byte",
+ "java.io.FileReader",
+ "java.io.BufferedReader",
+ "net.appjet.oui.JarVirtualFile");
+
+pluginsLoaded = false;
+pluginModules = {};
+plugins = {};
+hooks = {};
+clientHooks = {};
+
+function loadAvailablePlugin(pluginName) {
+ if (plugins[pluginName] != undefined)
+ return plugins[pluginName];
+
+ var pluginsDir = new Packages.java.io.File("src/plugins");
+
+ var pluginFile = new Packages.java.io.File(pluginsDir, pluginName + '/main.js');
+ if (pluginFile.exists()) {
+ var pluginModulePath = pluginFile.getPath().replace(new RegExp("src/\(.*\)\.js"), "$1").replace("/", ".", "g");
+ var importStmt = "import('" + pluginModulePath + "')";
+ try {
+ var res = execution.fancyAssEval(importStmt, "main;");
+ res = new res.init();
+ return res;
+ } catch (e) {
+ log.info({errorLoadingPlugin:exceptionutils.getStackTracePlain(e)});
+ }
+ }
+ return null;
+}
+
+function loadAvailablePlugins() {
+ var pluginsDir = new Packages.java.io.File("src/plugins");
+
+ var pluginNames = pluginsDir.list();
+
+ for (i = 0; i < pluginNames.length; i++) {
+ var plugin = loadAvailablePlugin(pluginNames[i]);
+ if (plugin != null)
+ pluginModules[pluginNames[i]] = plugin
+ }
+}
+
+function loadPluginHooks(pluginName) {
+ function registerHookNames(hookSet, type) {
+ return function (hook) {
+ var row = {hook:hook, type:type, plugin:pluginName};
+ if (hookSet[hook] == undefined) hookSet[hook] = [];
+ hookSet[hook].push(row);
+ return row;
+ }
+ }
+ plugins[pluginName] = pluginModules[pluginName].hooks.map(registerHookNames(hooks, 'server'));
+ if (pluginModules[pluginName].client != undefined && pluginModules[pluginName].client.hooks != undefined)
+ plugins[pluginName] = plugins[pluginName].concat(pluginModules[pluginName].client.hooks.map(registerHookNames(clientHooks, 'client')));
+}
+
+function unloadPluginHooks(pluginName) {
+ for (var hookSet in [hooks, clientHooks])
+ for (var hookName in hookSet) {
+ var hook = hookSet[hookName];
+ for (i = hook.length - 1; i >= 0; i--)
+ if (hook[i].plugin == pluginName)
+ hook.splice(i, 1);
+ }
+ delete plugins[pluginName];
+}
+
+function loadInstalledHooks() {
+ var sql = '' +
+ 'select ' +
+ ' hook.name as hook, ' +
+ ' hook_type.name as type, ' +
+ ' plugin.name as plugin, ' +
+ ' plugin_hook.original_name as original ' +
+ 'from ' +
+ ' plugin ' +
+ ' left outer join plugin_hook on ' +
+ ' plugin.id = plugin_hook.plugin_id ' +
+ ' left outer join hook on ' +
+ ' plugin_hook.hook_id = hook.id ' +
+ ' left outer join hook_type on ' +
+ ' hook.type_id = hook_type.id ' +
+ 'order by hook.name, plugin.name';
+
+ var rows = sqlobj.executeRaw(sql, {});
+ for (var i = 0; i < rows.length; i++) {
+ var row = rows[i];
+
+ if (plugins[row.plugin] == undefined)
+ plugins[row.plugin] = [];
+ plugins[row.plugin].push(row);
+
+ var hookSet;
+
+ if (row.type == 'server')
+ hookSet = hooks;
+ else if (row.type == 'client')
+ hookSet = clientHooks;
+
+ if (hookSet[row.hook] == undefined)
+ hookSet[row.hook] = [];
+ if (row.hook != 'null')
+ hookSet[row.hook].push(row);
+ }
+}
+
+function selectOrInsert(table, columns) {
+ var res = sqlobj.selectSingle(table, columns);
+ if (res !== null)
+ return res;
+ sqlobj.insert(table, columns);
+ return sqlobj.selectSingle(table, columns);
+}
+
+function saveInstalledHooks(pluginName) {
+ var plugin = sqlobj.selectSingle('plugin', {name:pluginName});
+
+ if (plugin !== null) {
+ sqlobj.deleteRows('plugin_hook', {plugin_id:plugin.id});
+ if (plugins[pluginName] === undefined)
+ sqlobj.deleteRows('plugin', {name:pluginName});
+ }
+
+ if (plugins[pluginName] !== undefined) {
+ if (plugin === null)
+ plugin = selectOrInsert('plugin', {name:pluginName});
+
+ for (var i = 0; i < plugins[pluginName].length; i++) {
+ var row = plugins[pluginName][i];
+
+ var hook_type = selectOrInsert('hook_type', {name:row.type});
+ var hook = selectOrInsert('hook', {name:row.hook, type_id:hook_type.id});
+
+ sqlobj.insert("plugin_hook", {plugin_id:plugin.id, hook_id:hook.id});
+ }
+ }
+}
+
+
+function loadPlugins() {
+ if (pluginsLoaded) return;
+ pluginsLoaded = true;
+ loadAvailablePlugins();
+ loadInstalledHooks();
+}
+
+
+/* User API */
+function enablePlugin(pluginName) {
+ loadPlugins();
+ loadPluginHooks(pluginName);
+ saveInstalledHooks(pluginName);
+ try {
+ pluginModules[pluginName].install();
+ } catch (e) {
+ unloadPluginHooks(pluginName);
+ saveInstalledHooks(pluginName);
+ throw e;
+ }
+ log.info({PLUGINS:plugins, HOOKS:hooks});
+}
+
+function disablePlugin(pluginName) {
+ loadPlugins();
+ try {
+ pluginModules[pluginName].uninstall();
+ } catch (e) {
+ log.info({errorUninstallingPlugin:exceptionutils.getStackTracePlain(e)});
+ }
+ unloadPluginHooks(pluginName);
+ saveInstalledHooks(pluginName);
+ log.info({PLUGINS:plugins, HOOKS:hooks});
+}
+
+function registerClientHandlerJS() {
+ loadPlugins();
+ for (pluginName in plugins) {
+ var plugin = pluginModules[pluginName];
+ if (plugin.client !== undefined) {
+ helpers.includeJs("plugins/" + pluginName + "/main.js");
+ if (plugin.client.modules != undefined)
+ for (j = 0; j < client.modules.length; j++)
+ helpers.includeJs("plugins/" + pluginName + "/" + plugin.client.modules[j] + ".js");
+ }
+ }
+ helpers.addClientVars({hooks:clientHooks});
+ helpers.includeJs("plugins.js");
+}
+
+function callHook(hookName, args) {
+ loadPlugins();
+ if (hooks[hookName] === undefined)
+ return [];
+ var res = [];
+ for (i = 0; i < hooks[hookName].length; i++) {
+ var plugin = hooks[hookName][i];
+ var pluginRes = pluginModules[plugin.plugin][plugin.original || hookName](args);
+ if (pluginRes != undefined && pluginRes != null)
+ res = res.concat(pluginRes);
+ }
+ return res;
+}
+
+function callHookStr(hookName, args, sep, pre, post) {
+ if (sep == undefined) sep = '';
+ if (pre == undefined) pre = '';
+ if (post == undefined) post = '';
+ return callHook(hookName, args).map(function (x) { return pre + x + post}).join(sep || "");
+}
diff --git a/trunk/etherpad/src/etherpad/control/admin/pluginmanager.js b/trunk/etherpad/src/etherpad/control/admin/pluginmanager.js
new file mode 100644
index 0000000..3fb017c
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/control/admin/pluginmanager.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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.
+ */
+
+import("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.admin.plugins");
+
+
+function onRequest() {
+ plugins.loadPlugins();
+
+ if (request.params.action == 'install') {
+ plugins.enablePlugin(request.params.plugin);
+ } else if (request.params.action == 'uninstall') {
+ plugins.disablePlugin(request.params.plugin);
+ } else if (request.params.action == 'reinstall') {
+ plugins.disablePlugin(request.params.plugin);
+ plugins.enablePlugin(request.params.plugin);
+ }
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: pro_utils.isProDomainRequest(),
+ userIsGuest: padusers.isGuest(padusers.getUserId()),
+ userId: padusers.getUserId(),
+ });
+
+ renderHtml("admin/pluginmanager.ejs",
+ {
+ pluginModules: plugins.pluginModules,
+ plugins: plugins.plugins,
+ config: appjet.config,
+ bodyClass: 'nonpropad',
+ isPro: pro_utils.isProDomainRequest(),
+ isProAccountHolder: pro_utils.isProDomainRequest() && ! padusers.isGuest(padusers.getUserId()),
+ account: getSessionProAccount(), // may be falsy
+ });
+ return true;
+}
diff --git a/trunk/etherpad/src/plugins/kafoo/main.js b/trunk/etherpad/src/plugins/kafoo/main.js
new file mode 100644
index 0000000..f645576
--- /dev/null
+++ b/trunk/etherpad/src/plugins/kafoo/main.js
@@ -0,0 +1,16 @@
+import("etherpad.log");
+
+function init() {
+ this.hooks = [];
+ this.description = 'KaBar plugin';
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing testplugin");
+}
+
+function uninstall() {
+ log.info("Uninstalling testplugin");
+}
diff --git a/trunk/etherpad/src/plugins/testplugin/controllers/testplugin.js b/trunk/etherpad/src/plugins/testplugin/controllers/testplugin.js
new file mode 100644
index 0000000..0c79e06
--- /dev/null
+++ b/trunk/etherpad/src/plugins/testplugin/controllers/testplugin.js
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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.
+ */
+
+import("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+
+function onRequest() {
+ var isPro = pro_utils.isProDomainRequest();
+ var userId = padusers.getUserId();
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: isPro,
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ renderHtml("testplugin.ejs",
+ {
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ }, 'testplugin');
+ return true;
+}
diff --git a/trunk/etherpad/src/plugins/testplugin/hooks.js b/trunk/etherpad/src/plugins/testplugin/hooks.js
new file mode 100644
index 0000000..493a2c2
--- /dev/null
+++ b/trunk/etherpad/src/plugins/testplugin/hooks.js
@@ -0,0 +1,15 @@
+import("etherpad.log");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+import("plugins.testplugin.controllers.testplugin");
+
+function serverStartup() {
+ log.info("Server startup for testplugin");
+}
+
+function serverShutdown() {
+ log.info("Server shutdown for testplugin");
+}
+
+function handlePath() {
+ return [[PrefixMatcher('/ep/testplugin/'), forward(testplugin)]];
+}
diff --git a/trunk/etherpad/src/plugins/testplugin/main.js b/trunk/etherpad/src/plugins/testplugin/main.js
new file mode 100644
index 0000000..49b447c
--- /dev/null
+++ b/trunk/etherpad/src/plugins/testplugin/main.js
@@ -0,0 +1,23 @@
+import("etherpad.log");
+import("plugins.testplugin.hooks");
+import("plugins.testplugin.static.js.main");
+
+function init() {
+ this.hooks = ['serverStartup', 'serverShutdown', 'handlePath'];
+ this.client = new main.init();
+ this.description = 'Test Plugin';
+ this.serverStartup = hooks.serverStartup;
+ this.serverShutdown = hooks.serverShutdown;
+ this.handlePath = hooks.handlePath;
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing testplugin");
+}
+
+function uninstall() {
+ log.info("Uninstalling testplugin");
+}
+
diff --git a/trunk/etherpad/src/plugins/testplugin/static/js/main.js b/trunk/etherpad/src/plugins/testplugin/static/js/main.js
new file mode 100644
index 0000000..f08b8f7
--- /dev/null
+++ b/trunk/etherpad/src/plugins/testplugin/static/js/main.js
@@ -0,0 +1,11 @@
+function init() {
+ this.hooks = ['kafoo'];
+ this.kafoo = kafoo;
+}
+
+function kafoo() {
+ alert('hej');
+}
+
+/* used on the client side only */
+testplugin = new init();
diff --git a/trunk/etherpad/src/plugins/testplugin/static/js/test.js b/trunk/etherpad/src/plugins/testplugin/static/js/test.js
new file mode 100644
index 0000000..0f30cd9
--- /dev/null
+++ b/trunk/etherpad/src/plugins/testplugin/static/js/test.js
@@ -0,0 +1 @@
+callHook("kafoo");
diff --git a/trunk/etherpad/src/plugins/testplugin/templates/testplugin.ejs b/trunk/etherpad/src/plugins/testplugin/templates/testplugin.ejs
new file mode 100644
index 0000000..f70ca8d
--- /dev/null
+++ b/trunk/etherpad/src/plugins/testplugin/templates/testplugin.ejs
@@ -0,0 +1,29 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<%
+ helpers.setHtmlTitle("Test plugin");
+ helpers.setBodyId("padbody");
+ helpers.addBodyClass("limwidth nonpropad nonprouser");
+ helpers.includeCss("pad2_ejs.css");
+ helpers.setRobotsPolicy({index: false, follow: false})
+ helpers.includeJQuery();
+ helpers.includeCometJs();
+ helpers.includeJs("json2.js");
+ helpers.includeJs("plugins/testplugin/test.js");
+ helpers.addToHead('\n<style type="text/css" title="dynamicsyntax"></style>\n');
+%>
+
+<div id="padpage">
+ Welcome to the test plugin
+</div>
diff --git a/trunk/etherpad/src/static/js/plugins.js b/trunk/etherpad/src/static/js/plugins.js
new file mode 100644
index 0000000..6d8804e
--- /dev/null
+++ b/trunk/etherpad/src/static/js/plugins.js
@@ -0,0 +1,19 @@
+function callHook(hookName, args) {
+ if (clientVars.hooks[hookName] === undefined)
+ return [];
+ var res = [];
+ for (i = 0; i < clientVars.hooks[hookName].length; i++) {
+ var plugin = clientVars.hooks[hookName][i];
+ var pluginRes = eval(plugin.plugin)[plugin.original || hookName](args);
+ if (pluginRes != undefined && pluginRes != null)
+ res = res.concat(pluginRes);
+ }
+ return res;
+}
+
+function callHookStr(hookName, args, sep, pre, post) {
+ if (sep == undefined) sep = '';
+ if (pre == undefined) pre = '';
+ if (post == undefined) post = '';
+ return callHook(hookName, args).map(function (x) { return pre + x + post}).join(sep || "");
+}
diff --git a/trunk/etherpad/src/templates/admin/pluginmanager.ejs b/trunk/etherpad/src/templates/admin/pluginmanager.ejs
new file mode 100644
index 0000000..4e08fc9
--- /dev/null
+++ b/trunk/etherpad/src/templates/admin/pluginmanager.ejs
@@ -0,0 +1,126 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<%
+ helpers.setHtmlTitle("Browse tags");
+ helpers.setBodyId("padbody");
+ helpers.addBodyClass("limwidth nonpropad nonprouser");
+ helpers.includeCss("pad2_ejs.css");
+ helpers.setRobotsPolicy({index: false, follow: false})
+ helpers.includeJQuery();
+ helpers.includeCometJs();
+ helpers.includeJs("json2.js");
+ helpers.addToHead('\n<style type="text/css" title="dynamicsyntax"></style>\n');
+
+ function inArray(item, arr) {
+ for (var i = 0; i < arr.length; i++)
+ if (arr[i] == item)
+ return true;
+ return false;
+ }
+%>
+
+<div id="padpage">
+ <div id="padtop">
+ <div id="topbar" style="margin: 7px; margin-top: 0px;">
+ <div id="topbarleft"><!-- --></div>
+ <div id="topbarright"><!-- --></div>
+ <div id="topbarcenter"><a href="/" id="topbaretherpad">EtherPad</a></div>
+ <% if (isProAccountHolder) { %>
+ <div id="accountnav"><%= toHTML(account.email) %><a href="/ep/account/sign-out">(sign out)</a></div>
+ <% } else if (isPro) { %>
+ <div id="accountnav"><a href="<%= signinUrl %>">sign in</a></div>
+ <% } %>
+ </div>
+ </div>
+ <div id="docbar" class="docbar-public">
+ <div id="docbarleft"><!-- --></div>
+ <div title="Browse pads by tag" id="docbarpadtitle"><span>Browse tags</span></div>
+
+ <div id="docbaroptions-outer"><a href="javascript:void(0)" id="docbaroptions">Pad Options</a></div>
+ <div id="docbarsavedrevs-outer"><a href="javascript:void(0)" id="docbarsavedrevs">Saved revisions</a></div>
+ <div id="docbarimpexp-outer"><a href="javascript:void(0)" id="docbarimpexp">Import/Export</a></div>
+ <div id="docbarslider-outer"><a target="_blank" href="/ep/pad/view/xx/latest" id="docbarslider">Time Slider</a></div>
+
+ </div>
+ </div>
+ <div id="padmain">
+
+ <div id="padsidebar">
+ <div id="padusers">
+ </div>
+
+ <div id="hdraggie"><!-- --></div>
+
+ <div id="padchat"></div>
+ </div> <!-- /padsidebar -->
+
+ <div id="padeditor">
+ <div id="editbar" class="enabledtoolbar">
+ <div id="editbarleft"><!-- --></div>
+ <div id="editbarright"><!-- --></div>
+
+ <div id="editbarinner">
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('bold'));" class="editbarbutton bold" title="Bold (ctrl-B)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('italic'));" class="editbarbutton italic" title="Italics (ctrl-I)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('underline'));" class="editbarbutton underline" title="Underline (ctrl-U)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('strikethrough'));" class="editbarbutton strikethrough" title="Strikethrough">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('clearauthorship'));" class="editbarbutton clearauthorship" title="Clear Authorship Colors">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('undo'));" class="editbarbutton undo" title="Undo (ctrl-Z)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('redo'));" class="editbarbutton redo" title="Redo (ctrl-Y)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('insertunorderedlist'));" class="editbarbutton insertunorderedlist" title="Toggle Bullet List">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('indent'));" class="editbarbutton indent" title="Indent List">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('outdent'));" class="editbarbutton outdent" title="Unindent List">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('save'));" class="editbarbutton save" title="Save Revision">&nbsp;</a>
+ </div>
+ </div>
+ <div style="height: 268px;" id="editorcontainerbox">
+ <div id="editorcontainer" style="padding:5pt; height: 600pt;">
+ <h1>Plugin manager</h1>
+ <table>
+ <tr>
+ <th>Module name</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ <% for (var plugin in pluginModules) { %>
+ <tr>
+ <td><%= pluginModules[plugin].description %></td>
+ <td>
+ <% if (plugins[plugin] !== undefined) { %>
+ Installed
+ <% } else { %>
+ Not installed
+ <% } %>
+ </td>
+ <td>
+ <% if (plugins[plugin] !== undefined) { %>
+ <a href="/ep/admin/pluginmanager/?plugin=<%= plugin %>&action=uninstall">Uninstall</a>
+ <a href="/ep/admin/pluginmanager/?plugin=<%= plugin %>&action=reinstall">Reinstall</a>
+ <% } else { %>
+ <a href="/ep/admin/pluginmanager/?plugin=<%= plugin %>&action=install">Install</a>
+ <% } %>
+ </td>
+ </tr>
+ <% } %>
+ </table>
+ </div>
+ </div>
+ </div><!-- /padeditor -->
+
+ <div id="bottomarea">
+ <div id="widthprefcheck" class="widthprefunchecked"><!-- --></div>
+ <div id="sidebarcheck" class="sidebarchecked"><!-- --></div>
+ </div>
+ </div>
+</div>