summaryrefslogtreecommitdiffstats
path: root/etherpad/src/etherpad
diff options
context:
space:
mode:
Diffstat (limited to 'etherpad/src/etherpad')
-rw-r--r--etherpad/src/etherpad/admin/plugins.js247
-rw-r--r--etherpad/src/etherpad/admin/shell.js127
-rw-r--r--etherpad/src/etherpad/billing/billing.js800
-rw-r--r--etherpad/src/etherpad/billing/fields.js219
-rw-r--r--etherpad/src/etherpad/billing/team_billing.js422
-rw-r--r--etherpad/src/etherpad/collab/collab_server.js778
-rw-r--r--etherpad/src/etherpad/collab/collabroom_server.js359
-rw-r--r--etherpad/src/etherpad/collab/genimg.js55
-rw-r--r--etherpad/src/etherpad/collab/json_sans_eval.js178
-rw-r--r--etherpad/src/etherpad/collab/readonly_server.js174
-rw-r--r--etherpad/src/etherpad/collab/server_utils.js204
-rw-r--r--etherpad/src/etherpad/control/aboutcontrol.js263
-rw-r--r--etherpad/src/etherpad/control/admin/pluginmanager.js71
-rw-r--r--etherpad/src/etherpad/control/admincontrol.js1482
-rw-r--r--etherpad/src/etherpad/control/blogcontrol.js199
-rw-r--r--etherpad/src/etherpad/control/connection_diagnostics_control.js87
-rw-r--r--etherpad/src/etherpad/control/global_pro_account_control.js143
-rw-r--r--etherpad/src/etherpad/control/historycontrol.js226
-rw-r--r--etherpad/src/etherpad/control/loadtestcontrol.js93
-rw-r--r--etherpad/src/etherpad/control/maincontrol.js54
-rw-r--r--etherpad/src/etherpad/control/pad/pad_changeset_control.js280
-rw-r--r--etherpad/src/etherpad/control/pad/pad_control.js754
-rw-r--r--etherpad/src/etherpad/control/pad/pad_importexport_control.js319
-rw-r--r--etherpad/src/etherpad/control/pad/pad_view_control.js287
-rw-r--r--etherpad/src/etherpad/control/pne_manual_control.js75
-rw-r--r--etherpad/src/etherpad/control/pne_tracker_control.js48
-rw-r--r--etherpad/src/etherpad/control/pro/account_control.js369
-rw-r--r--etherpad/src/etherpad/control/pro/admin/account_manager_control.js260
-rw-r--r--etherpad/src/etherpad/control/pro/admin/license_manager_control.js128
-rw-r--r--etherpad/src/etherpad/control/pro/admin/pro_admin_control.js280
-rw-r--r--etherpad/src/etherpad/control/pro/admin/pro_config_control.js54
-rw-r--r--etherpad/src/etherpad/control/pro/admin/team_billing_control.js447
-rw-r--r--etherpad/src/etherpad/control/pro/pro_main_control.js150
-rw-r--r--etherpad/src/etherpad/control/pro/pro_padlist_control.js200
-rw-r--r--etherpad/src/etherpad/control/pro_beta_control.js136
-rw-r--r--etherpad/src/etherpad/control/pro_signup_control.js173
-rw-r--r--etherpad/src/etherpad/control/scriptcontrol.js75
-rw-r--r--etherpad/src/etherpad/control/static_control.js76
-rw-r--r--etherpad/src/etherpad/control/statscontrol.js1214
-rw-r--r--etherpad/src/etherpad/control/store/eepnet_checkout_control.js757
-rw-r--r--etherpad/src/etherpad/control/store/storecontrol.js201
-rw-r--r--etherpad/src/etherpad/control/testcontrol.js74
-rw-r--r--etherpad/src/etherpad/db_migrations/m0000_test.js23
-rw-r--r--etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js38
-rw-r--r--etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js47
-rw-r--r--etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js29
-rw-r--r--etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js38
-rw-r--r--etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js73
-rw-r--r--etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js29
-rw-r--r--etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js67
-rw-r--r--etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js31
-rw-r--r--etherpad/src/etherpad/db_migrations/m0009_pad_tables.js31
-rw-r--r--etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js71
-rw-r--r--etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js33
-rw-r--r--etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js54
-rw-r--r--etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js102
-rw-r--r--etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js25
-rw-r--r--etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js35
-rw-r--r--etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js82
-rw-r--r--etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js24
-rw-r--r--etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js25
-rw-r--r--etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js57
-rw-r--r--etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js32
-rw-r--r--etherpad/src/etherpad/db_migrations/m0024_statistics_table.js42
-rw-r--r--etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js26
-rw-r--r--etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js37
-rw-r--r--etherpad/src/etherpad/db_migrations/m0027_pro_config.js27
-rw-r--r--etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js29
-rw-r--r--etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js31
-rw-r--r--etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js26
-rw-r--r--etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js24
-rw-r--r--etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js39
-rw-r--r--etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js42
-rw-r--r--etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js28
-rw-r--r--etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js45
-rw-r--r--etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js32
-rw-r--r--etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js26
-rw-r--r--etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js40
-rw-r--r--etherpad/src/etherpad/db_migrations/migration_runner.js148
-rw-r--r--etherpad/src/etherpad/debug.js26
-rw-r--r--etherpad/src/etherpad/globals.js49
-rw-r--r--etherpad/src/etherpad/helpers.js306
-rw-r--r--etherpad/src/etherpad/importexport/importexport.js241
-rw-r--r--etherpad/src/etherpad/legacy_urls.js37
-rw-r--r--etherpad/src/etherpad/licensing.js163
-rw-r--r--etherpad/src/etherpad/log.js255
-rw-r--r--etherpad/src/etherpad/metrics/metrics.js438
-rw-r--r--etherpad/src/etherpad/pad/activepads.js52
-rw-r--r--etherpad/src/etherpad/pad/chatarchive.js67
-rw-r--r--etherpad/src/etherpad/pad/dbwriter.js338
-rw-r--r--etherpad/src/etherpad/pad/easysync2migration.js675
-rw-r--r--etherpad/src/etherpad/pad/exporthtml.js383
-rw-r--r--etherpad/src/etherpad/pad/importhtml.js230
-rw-r--r--etherpad/src/etherpad/pad/model.js655
-rw-r--r--etherpad/src/etherpad/pad/noprowatcher.js110
-rw-r--r--etherpad/src/etherpad/pad/pad_migrations.js206
-rw-r--r--etherpad/src/etherpad/pad/pad_security.js237
-rw-r--r--etherpad/src/etherpad/pad/padevents.js170
-rw-r--r--etherpad/src/etherpad/pad/padusers.js397
-rw-r--r--etherpad/src/etherpad/pad/padutils.js191
-rw-r--r--etherpad/src/etherpad/pad/revisions.js103
-rw-r--r--etherpad/src/etherpad/pne/pne_utils.js149
-rw-r--r--etherpad/src/etherpad/pro/domains.js141
-rw-r--r--etherpad/src/etherpad/pro/pro_account_auto_signin.js101
-rw-r--r--etherpad/src/etherpad/pro/pro_accounts.js592
-rw-r--r--etherpad/src/etherpad/pro/pro_config.js92
-rw-r--r--etherpad/src/etherpad/pro/pro_ldap_support.js217
-rw-r--r--etherpad/src/etherpad/pro/pro_pad_db.js232
-rw-r--r--etherpad/src/etherpad/pro/pro_pad_editors.js104
-rw-r--r--etherpad/src/etherpad/pro/pro_padlist.js289
-rw-r--r--etherpad/src/etherpad/pro/pro_padmeta.js111
-rw-r--r--etherpad/src/etherpad/pro/pro_quotas.js141
-rw-r--r--etherpad/src/etherpad/pro/pro_utils.js169
-rw-r--r--etherpad/src/etherpad/quotas.js50
-rw-r--r--etherpad/src/etherpad/sessions.js203
-rw-r--r--etherpad/src/etherpad/statistics/exceptions.js231
-rw-r--r--etherpad/src/etherpad/statistics/statistics.js1248
-rw-r--r--etherpad/src/etherpad/store/checkout.js300
-rw-r--r--etherpad/src/etherpad/store/eepnet_checkout.js101
-rw-r--r--etherpad/src/etherpad/store/eepnet_trial.js241
-rw-r--r--etherpad/src/etherpad/testing/testutils.js23
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0000_test.js22
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js48
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js89
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js42
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js214
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js22
-rw-r--r--etherpad/src/etherpad/usage_stats/usage_stats.js162
-rw-r--r--etherpad/src/etherpad/utils.js464
133 files changed, 25373 insertions, 0 deletions
diff --git a/etherpad/src/etherpad/admin/plugins.js b/etherpad/src/etherpad/admin/plugins.js
new file mode 100644
index 0000000..385e2ca
--- /dev/null
+++ b/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(force) {
+ if (pluginsLoaded && force == undefined) 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;
+ }
+}
+
+function disablePlugin(pluginName) {
+ loadPlugins();
+ try {
+ pluginModules[pluginName].uninstall();
+ } catch (e) {
+ log.info({errorUninstallingPlugin:exceptionutils.getStackTracePlain(e)});
+ }
+ unloadPluginHooks(pluginName);
+ saveInstalledHooks(pluginName);
+}
+
+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 (var 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)
+ for (var j = 0; j < pluginRes.length; j++)
+ res.push(pluginRes[j]); /* Don't use Array.concat as it flatterns arrays within the array */
+ }
+ 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/etherpad/src/etherpad/admin/shell.js b/etherpad/src/etherpad/admin/shell.js
new file mode 100644
index 0000000..391d524
--- /dev/null
+++ b/etherpad/src/etherpad/admin/shell.js
@@ -0,0 +1,127 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("jsutils.cmp");
+import("jsutils.eachProperty");
+import("exceptionutils");
+import("execution");
+import("stringutils.trim");
+
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+
+function _splitCommand(cmd) {
+ var parts = [[], []];
+ var importing = true;
+ cmd.split("\n").forEach(function(l) {
+ if ((trim(l).length > 0) &&
+ (trim(l).indexOf("import") != 0)) {
+ importing = false;
+ }
+
+ if (importing) {
+ parts[0].push(l);
+ } else {
+ parts[1].push(l);
+ }
+ });
+
+ parts[0] = parts[0].join("\n");
+ parts[1] = parts[1].join("\n");
+ return parts;
+}
+
+function getResult(cmd) {
+ var resultString = (function() {
+ try {
+ var parts = _splitCommand(cmd);
+ result = execution.fancyAssEval(parts[0], parts[1]);
+ } catch (e) {
+ // if (e instanceof JavaException) {
+ // e = new net.appjet.bodylock.JSRuntimeException(e.getMessage(), e.javaException);
+ // }
+ if (appjet.config.devMode) {
+ (e.javaException || e.rhinoException || e).printStackTrace();
+ }
+ result = exceptionutils.getStackTracePlain(e);
+ }
+ var resultString;
+ try {
+ resultString = ((result && result.toString) ? result.toString() : String(result));
+ } catch (ex) {
+ resultString = "Error converting result to string: "+ex.toString();
+ }
+ return resultString;
+ })();
+ return resultString;
+}
+
+function _renderCommandShell() {
+ // run command if necessary
+ if (request.params.cmd) {
+ var cmd = request.params.cmd;
+ var resultString = getResult(cmd);
+
+ getSession().shellCommand = cmd;
+ getSession().shellResult = resultString;
+ response.redirect(request.path+(request.query?'?'+request.query:''));
+ }
+
+ var div = DIV({style: "padding: 4px; margin: 4px; background: #eee; "
+ + "border: 1px solid #338"});
+ // command div
+ var oldCmd = getSession().shellCommand || "";
+ var commandDiv = DIV({style: "width: 100%; margin: 4px 0;"});
+ commandDiv.push(FORM({style: "width: 100%;",
+ method: "POST", action: request.path + (request.query?'?'+request.query:'')},
+ TEXTAREA({name: "cmd",
+ style: "border: 1px solid #555;"
+ + "width: 100%; height: 160px; font-family: monospace;"},
+ html(oldCmd)),
+ INPUT({type: "submit"})));
+
+ // result div
+ var resultDiv = DIV({style: ""});
+ var isResult = getSession().shellResult != null;
+ if (isResult) {
+ resultDiv.push(DIV(
+ PRE({style: 'border: 1px solid #555; font-family: monospace; margin: 4px 0; padding: 4px;'},
+ getSession().shellResult)));
+ delete getSession().shellResult;
+ resultDiv.push(DIV({style: "text-align: right;"},
+ A({href: qpath({})}, "clear")));
+ } else {
+ resultDiv.push(P("result will go here"));
+ }
+
+ var t = TABLE({border: 0, cellspacing: 0, cellpadding: 0, width: "100%",
+ style: "width: 100%;"});
+ t.push(TR(TH({width: "49%", align: "left"}, " Command:"),
+ TH({width: "49%", align: "left"}, " "+(isResult ? "Result:" : ""))),
+ TR(TD({valign: "top", style: 'padding: 4px;'}, commandDiv),
+ TD({valign: "top", style: 'padding: 4px;'}, resultDiv)));
+ div.push(t);
+ return div;
+}
+
+function handleRequest() {
+ var body = BODY();
+ body.push(A({href: '/ep/admin/'}, html("&laquo; Admin")));
+ body.push(BR(), BR());
+ body.push(_renderCommandShell());
+ response.write(HTML(body));
+}
diff --git a/etherpad/src/etherpad/billing/billing.js b/etherpad/src/etherpad/billing/billing.js
new file mode 100644
index 0000000..444c233
--- /dev/null
+++ b/etherpad/src/etherpad/billing/billing.js
@@ -0,0 +1,800 @@
+/**
+ * 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.
+ */
+
+import("dateutils.*");
+import("fastJSON");
+import("jsutils.eachProperty");
+import("netutils.urlPost");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("stringutils.{md5,repeat}");
+
+import("etherpad.log.{custom=>eplog}");
+
+
+jimport("java.lang.System.out.println");
+
+function clearKeys(obj, keys) {
+ var newObj = {};
+ eachProperty(obj, function(k, v) {
+ var isCopied = false;
+ keys.forEach(function(key) {
+ if (k == key.name &&
+ key.valueTest(v)) {
+ newObj[k] = key.valueReplace(v);
+ isCopied = true;
+ }
+ });
+ if (! isCopied) {
+ if (typeof(obj[k]) == 'object') {
+ newObj[k] = clearKeys(v, keys);
+ } else {
+ newObj[k] = v;
+ }
+ }
+ });
+ return newObj;
+}
+
+function replaceWithX(s) {
+ return repeat("X", s.length);
+}
+
+function log(obj) {
+ eplog('billing', clearKeys(obj, [
+ {name: "ACCT",
+ valueTest: function(s) { return /^\d{15,16}$/.test(s) },
+ valueReplace: replaceWithX},
+ {name: "CVV2",
+ valueTest: function(s) { return /^\d{3,4}$/.test(s) },
+ valueReplace: replaceWithX}]));
+}
+
+var _USER = function() { return appjet.config['etherpad.paypal.user'] || "zamfir_1239051855_biz_api1.gmail.com"; }
+var _PWD = function() { return appjet.config['etherpad.paypal.pwd'] || "1239051867"; }
+var _SIGNATURE = function() { return appjet.config['etherpad.paypal.signature'] || "AQU0e5vuZCvSg-XJploSa.sGUDlpAwAy5fz.FhtfOQ25Qa9sFLDt7Bmp"; }
+var _RECEIVER = function() { return appjet.config['etherpad.paypal.receiver'] || "zamfir_1239051855_biz@gmail.com"; }
+var _paypalApiUrl = function() { return appjet.config['etherpad.paypal.apiUrl'] || "https://api-3t.sandbox.paypal.com/nvp"; }
+var _paypalWebUrl = function() { return appjet.config['etherpad.paypal.webUrl'] || "https://www.sandbox.paypal.com/cgi-bin/webscr"; }
+function paypalPurchaseUrl(token) {
+ return (appjet.config['etherpad.paypal.purchaseUrl'] || "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=")+token;
+}
+
+function getPurchase(id) {
+ return sqlobj.selectSingle('billing_purchase', {id: id});
+}
+
+function getPurchaseForCustomer(customerId) {
+ return sqlobj.selectSingle('billing_purchase', {customer: customerId});
+}
+
+function updatePurchase(id, fields) {
+ sqlobj.updateSingle('billing_purchase', {id: id}, fields);
+}
+
+function getInvoicesForPurchase(purchaseId) {
+ return sqlobj.selectMulti('billing_invoice', {purchase: purchaseId});
+}
+
+function getInvoice(id) {
+ return sqlobj.selectSingle('billing_invoice', {id: id});
+}
+
+function createInvoice() {
+ return _newInvoice();
+}
+
+function updateInvoice(id, fields) {
+ sqlobj.updateSingle('billing_invoice', {id: id}, fields)
+}
+
+function getTransaction(id) {
+ return sqlobj.selectSingle('billing_transaction', {id: id});
+}
+function getTransactionByExternalId(txnId) {
+ return sqlobj.selectSingle('billing_transaction', {txnId: txnId});
+}
+
+function getTransactionsForCustomer(customerId) {
+ return sqlobj.selectMulti('billing_transaction', {customer: customerId});
+}
+
+function getPendingTransactionsForCustomer(customerId) {
+ return sqlobj.selectMulti('billing_transaction', {customer: customerId, status: 'pending'});
+}
+
+function _updateTransaction(id, fields) {
+ return sqlobj.updateSingle('billing_transaction', {id: id}, fields);
+}
+
+function getAdjustments(invoiceId) {
+ return sqlobj.selectMulti('billing_adjustment', {invoice: invoiceId});
+}
+
+function createSubscription(customer, product, dollars, couponCode) {
+ var purchaseId = _newPurchase(customer, product, dollarsToCents(dollars), couponCode);
+ _purchaseActive(purchaseId);
+ updatePurchase(purchaseId, {type: 'subscription', paidThrough: nextMonth(noon(new Date))});
+ return purchaseId;
+}
+
+function _newPurchase(customer, product, cents, couponCode) {
+ var purchaseId = sqlobj.insert('billing_purchase', {
+ customer: customer,
+ product: product,
+ cost: cents,
+ coupon: couponCode,
+ status: 'inactive'
+ });
+ return purchaseId;
+}
+
+function _newInvoice() {
+ var invoiceId = sqlobj.insert('billing_invoice', {
+ time: new Date(),
+ purchase: -1,
+ amt: 0,
+ status: 'pending'
+ });
+ return invoiceId;
+}
+
+function _newTransaction(customer, cents) {
+ var transactionId = sqlobj.insert('billing_transaction', {
+ customer: customer,
+ time: new Date(),
+ amt: cents,
+ status: 'new'
+ });
+ return transactionId;
+}
+
+function _newAdjustment(transaction, invoice, cents) {
+ sqlobj.insert('billing_adjustment', {
+ transaction: transaction,
+ invoice: invoice,
+ time: new Date(),
+ amt: cents
+ });
+}
+
+function _transactionSuccess(transaction, txnId, payInfo) {
+ _updateTransaction(transaction, {
+ status: 'success', txnId: txnId, time: new Date(), payInfo: payInfo
+ });
+}
+
+function _transactionFailure(transaction, txnId) {
+ _updateTransaction(transaction, {
+ status: 'failure', txnId: txnId, time: new Date()
+ });
+}
+
+function _transactionPending(transaction, txnId) {
+ _updateTransaction(transaction, {
+ status: 'pending', txnId: txnId, time: new Date()
+ });
+}
+
+function _invoicePaid(invoice) {
+ updateInvoice(invoice, {status: 'paid'});
+}
+
+function _purchaseActive(purchase) {
+ updatePurchase(purchase, {status: 'active'});
+}
+
+function _purchaseExtend(purchase, monthCount) {
+ var expiration = getPurchase(purchase).paidThrough;
+ for (var i = monthCount; i > 0; i--) {
+ expiration = nextMonth(expiration);
+ }
+ // paying your invoice always makes you current.
+ if (expiration < new Date) {
+ expiration = nextMonth(new Date);
+ }
+ updatePurchase(purchase, {paidThrough: expiration});
+}
+
+function _doPost(url, body) {
+ try {
+ var ret = urlPost(url, body);
+ } catch (e) {
+ if (e.javaException) {
+ net.appjet.oui.exceptionlog.apply(e.javaException);
+ }
+ return { error: e };
+ }
+ return { value: ret };
+}
+
+function _doPaypalNvpPost(properties0) {
+ return {
+ status: 'failure',
+ errorMessage: "Billing has been discontinued. No new services may be purchased."
+ }
+ // var properties = {
+ // USER: _USER(),
+ // PWD: _PWD(),
+ // SIGNATURE: _SIGNATURE(),
+ // VERSION: "56.0"
+ // }
+ // eachProperty(properties0, function(k, v) {
+ // if (v !== undefined) {
+ // properties[k] = v;
+ // }
+ // })
+ // log({'type': 'api call', 'value': properties});
+ // var ret = _doPost(_paypalApiUrl(), properties);
+ // if (ret.error) {
+ // return {
+ // status: 'failure',
+ // exception: ret.error.javaException || ret.error,
+ // errorMessage: ret.error.message
+ // }
+ // }
+ // ret = ret.value;
+ // var paypalResponse = {};
+ // ret.content.split("&").forEach(function(x) {
+ // var parts = x.split("=");
+ // paypalResponse[decodeURIComponent(parts[0])] =
+ // decodeURIComponent(parts[1]);
+ // })
+ //
+ // var res = paypalResponse;
+ // log(res)
+ // if (res.ACK == "Success" || res.ACK == "SuccessWithWarning") {
+ // return {
+ // status: 'success',
+ // response: res
+ // }
+ // } else {
+ // errors = [];
+ // for (var i = 0; res['L_LONGMESSAGE'+i]; ++i) {
+ // errors.push(res['L_LONGMESSAGE'+i]);
+ // }
+ // return {
+ // status: 'failure',
+ // errorMessage: errors.join(", "),
+ // errorMessages: errors,
+ // response: res
+ // }
+ // }
+}
+
+// status -> 'completion', 'bad', 'redundant', 'possible_fraud'
+function handlePaypalNotification() {
+ var content = (typeof(request.content) == 'string' ? request.content : undefined);
+ if (! content) {
+ return new BillingResult('bad', "no content");
+ }
+ log({'type': 'paypal-notification', 'content': content});
+ var params = {};
+ content.split("&").forEach(function(x) {
+ var parts = x.split("=");
+ params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
+ });
+ var txnId = params.txn_id;
+ var properties = [];
+ for(var i in params) {
+ properties.push(i+" -> "+params[i]);
+ }
+ var debugString = properties.join(", ");
+ log({'type': 'parsed-paypal-notification', 'value': debugString});
+ var transaction = getTransactionByExternalId(txnId);
+ log({'type': 'notification-transaction', 'value': (transaction || {})});
+ if (_RECEIVER() != params.receiver_email) {
+ return new BillingResult('possible_fraud', debugString);
+ }
+ if (params.payment_status == "Completed" && transaction &&
+ (transaction.status == 'pending' || transaction.status == 'new')) {
+ var ret = _doPost(_paypalWebUrl(), "cmd=_notify-validate&"+content);
+ if (ret.error || ret.value.content != "VERIFIED") {
+ return new BillingResult('possible_fraud', debugString);
+ }
+ var invoice = getInvoice(params.invoice);
+ if (invoice.amt != dollarsToCents(params.mc_gross)) {
+ return new BillingResult('possible_fraud', debugString);
+ }
+
+ sqlcommon.inTransaction(function () {
+ _transactionSuccess(transaction.id, txnId, "via eCheck");
+ _invoicePaid(invoice.id);
+ _purchaseActive(invoice.purchase);
+ });
+ var purchase = getPurchase(invoice.purchase);
+ return new BillingResult('completion', debugString, null,
+ new PurchaseInfo(params.custom,
+ invoice.id,
+ transaction.id,
+ params.txn_id,
+ purchase.id,
+ centsToDollars(invoice.amt),
+ purchase.couponCode,
+ purchase.time,
+ undefined));
+ } else {
+ return new BillingResult('redundant', debugString);
+ }
+}
+
+function _expressCheckoutCustom(invoiceId, transactionId) {
+ return md5("zimki_sucks"+invoiceId+transactionId);
+}
+
+function PurchaseInfo(custom, invoiceId, transactionId, paypalId, purchaseId, dollars, couponCode, time, token, description) {
+ this.__defineGetter__("custom", function() { return custom });
+ this.__defineGetter__("invoiceId", function() { return invoiceId });
+ this.__defineGetter__("transactionId", function() { return transactionId });
+ this.__defineGetter__("paypalId", function() { return paypalId });
+ this.__defineGetter__("purchaseId", function() { return purchaseId });
+ this.__defineGetter__("cost", function() { return dollars });
+ this.__defineGetter__("couponCode", function() { return couponCode });
+ this.__defineGetter__("time", function() { return time });
+ this.__defineGetter__("token", function() { return token });
+ this.__defineGetter__("description", function() { return description });
+}
+
+function PayerInfo(paypalResult) {
+ this.__defineGetter__("payerId", function() { return paypalResult.response.PAYERID });
+ this.__defineGetter__("email", function() { return paypalResult.response.EMAIL });
+ this.__defineGetter__("businessName", function() { return paypalResult.response.BUSINESS });
+ this.__defineGetter__("nameSalutation", function() { return paypalResult.response.SALUTATION });
+ this.__defineGetter__("nameFirst", function() { return paypalResult.response.FIRSTNAME });
+ this.__defineGetter__("nameMiddle", function() { return paypalResult.response.MIDDLENAME });
+ this.__defineGetter__("nameLast", function() { return paypalResult.response.LASTNAME });
+}
+
+function BillingResult(status, debug, errorField, purchaseInfo, payerInfo) {
+ this.__defineGetter__("status", function() { return status });
+ this.__defineGetter__("debug", function() { return debug });
+ this.__defineGetter__("errorField", function() { return errorField });
+ this.__defineGetter__("purchaseInfo", function() { return purchaseInfo });
+ this.__defineGetter__("payerInfo", function() { return payerInfo });
+}
+
+function dollarsToCents(dollars) {
+ return Math.round(Number(dollars)*100);
+}
+
+function centsToDollars(cents) {
+ return Math.round(Number(cents)) / 100;
+}
+
+function verifyDollars(dollars) {
+ return Math.round(Number(dollars)*100)/100;
+}
+
+function beginExpressPurchase(invoiceId, customerId, productId, dollars, couponCode, successUrl, failureUrl, notifyUrl, authorizeOnly) {
+ var cents = dollarsToCents(dollars);
+ var time = new Date();
+ var purchaseId;
+ var transactionid;
+ if (! authorizeOnly) {
+ try {
+ sqlcommon.inTransaction(function() {
+ purchaseId = _newPurchase(customerId, productId, cents, couponCode);
+ updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
+ transactionId = _newTransaction(customerId, cents);
+ _newAdjustment(transactionId, invoiceId, cents);
+ });
+ } catch (e) {
+ if (e instanceof BillingResult) { return e; }
+ throw e;
+ }
+ }
+
+ var paypalResult =
+ _setExpressCheckout(invoiceId, transactionId, cents,
+ successUrl, failureUrl, notifyUrl, authorizeOnly);
+
+ if (paypalResult.status == 'success') {
+ var token = paypalResult.response.TOKEN;
+ return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+ _expressCheckoutCustom(invoiceId, transactionId),
+ invoiceId,
+ transactionId,
+ undefined,
+ purchaseId,
+ verifyDollars(dollars),
+ couponCode,
+ time,
+ token));
+ } else {
+ return new BillingResult('failure', paypalResult);
+ }
+}
+
+function _setExpressCheckout(invoiceId, transactionId, cents, successUrl, failureUrl, notifyUrl, authorizeOnly) {
+ var properties = {
+ INVNUM: invoiceId,
+
+ METHOD: 'SetExpressCheckout',
+ CUSTOM:
+ _expressCheckoutCustom(invoiceId, transactionId),
+ MAXAMT: centsToDollars(cents),
+ RETURNURL: successUrl,
+ CANCELURL: failureUrl,
+ NOTIFYURL: notifyUrl,
+ NOSHIPPING: 1,
+ PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
+
+ AMT: centsToDollars(cents)
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+function continueExpressPurchase(purchaseInfo, authorizeOnly) {
+ var paypalResult = _getExpressCheckoutDetails(purchaseInfo.token, authorizeOnly)
+ if (paypalResult.status == 'success') {
+ if (! authorizeOnly) {
+ if (paypalResult.response.INVNUM != purchaseInfo.invoiceId) {
+ return new BillingResult('failure', "invoice id mismatch");
+ }
+ }
+ if (paypalResult.response.CUSTOM !=
+ _expressCheckoutCustom(purchaseInfo.invoiceId, purchaseInfo.transactionId)) {
+ return new BillingResult('failure', "custom mismatch");
+ }
+ return new BillingResult('success', paypalResult, null, null, new PayerInfo(paypalResult));
+ } else {
+ return new BillingResult('failure', paypalResult);
+ }
+}
+
+function _getExpressCheckoutDetails(token, authorizeOnly) {
+ var properties = {
+ METHOD: 'GetExpresscheckoutDetails',
+ TOKEN: token,
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+function completeExpressPurchase(purchaseInfo, payerInfo, notifyUrl, authorizeOnly) {
+ var paypalResult = _doExpressCheckoutPayment(purchaseInfo, payerInfo, notifyUrl, authorizeOnly);
+
+ if (paypalResult.status == 'success') {
+ if (paypalResult.response.PAYMENTSTATUS == 'Completed') {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionSuccess(purchaseInfo.transactionId,
+ paypalResult.response.TRANSACTIONID, "via PayPal");
+ _invoicePaid(purchaseInfo.invoiceId);
+ _purchaseActive(purchaseInfo.purchaseId);
+ });
+ }
+ return new BillingResult('success', paypalResult);
+ } else if (paypalResult.response.PAYMENTSTATUS == 'Pending') {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionPending(purchaseInfo.transactionId,
+ paypalResult.response.TRANSACTIONID);
+ });
+ }
+ return new BillingResult('pending', paypalResult);
+ }
+ } else {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionFailure(purchaseInfo.transactionId,
+ (paypalResult.response ?
+ paypalResult.response.TRANSACTIONID || "" :
+ ""));
+ });
+ }
+ return new BillingResult('failure', paypalResult);
+ }
+}
+
+function _doExpressCheckoutPayment(purchaseInfo, payerInfo, notifyUrl, authorizeOnly) {
+ var properties = {
+ METHOD: 'DoExpressCheckoutPayment',
+ TOKEN: purchaseInfo.token,
+ PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
+
+ NOTIFYURL: notifyUrl,
+
+ PAYERID: payerInfo.payerId,
+
+ AMT: verifyDollars(purchaseInfo.cost), // dollars
+ INVNUM: purchaseInfo.invoiceId,
+ CUSTOM:
+ _expressCheckoutCustom(purchaseInfo.invoiceId, purchaseInfo.transactionId)
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+// which field has error? and, is it not user-correctable?
+var _directErrorCodes = {
+ '10502': ['cardExpiration'],
+ '10504': ['cardCvv'],
+ '10505': ['addressStreet', true],
+ '10508': ['cardExpiration'],
+ '10510': ['cardType'],
+ '10512': ['nameFirst'],
+ '10513': ['nameLast'],
+ '10519': ['cardNumber'],
+ '10521': ['cardNumber'],
+ '10527': ['cardNumber'],
+ '10534': ['cardNumber', true],
+ '10535': ['cardNumber'],
+ '10536': ['invoiceId', true],
+ '10537': ['addressCountry', true],
+ '10540': ['addressStreet', true],
+ '10541': ['cardNumber', true],
+ '10554': ['address', true],
+ '10555': ['address', true],
+ '10556': ['address', true],
+ '10561': ['address'],
+ '10562': ['cardExpiration'],
+ '10563': ['cardExpiration'],
+ '10565': ['addressCountry'],
+ '10566': ['cardType'],
+ '10571': ['cardCvv'],
+ '10701': ['address'],
+ '10702': ['addressStreet'],
+ '10703': ['addressStreet2'],
+ '10704': ['addressCity'],
+ '10705': ['addressState'],
+ '10706': ['addressZip'],
+ '10707': ['addressCountry'],
+ '10708': ['address'],
+ '10709': ['addressStreet'],
+ '10710': ['addressCity'],
+ '10711': ['addressState'],
+ '10712': ['addressZip'],
+ '10713': ['addressCountry'],
+ '10714': ['address'],
+ '10715': ['addressState'],
+ '10716': ['addressZip'],
+ '10717': ['addressZip'],
+ '10718': ['addressCity,addressState'],
+ '10748': ['cardCvv'],
+ '10752': ['card'],
+ '10756': ['address,card'],
+ '10759': ['cardNumber'],
+ '10762': ['cardCvv'],
+ '11611': function(response) {
+ var avsCode = response.AVSCODE;
+ var cvv2Match = response.CVV2MATCH;
+ var errorFields = [];
+ switch (avsCode) {
+ case 'N': case 'C': case 'A': case 'B':
+ case 'R': case 'S': case 'U': case 'G':
+ case 'I': case 'E':
+ errorFields.push('address');
+ }
+ switch (cvv2Match) {
+ case 'N':
+ errorFields.push('cardCvv');
+ }
+ return [errorFields.join(",")];
+ },
+ '15004': ['cardCvv'],
+ '15005': ['cardNumber'],
+ '15006': ['cardNumber'],
+ '15007': ['cardNumber']
+}
+
+function authorizePurchase(payinfo, notifyUrl) {
+ return directPurchase(undefined, undefined, undefined, 1, undefined, payinfo, notifyUrl, true);
+}
+
+function directPurchase(invoiceId, customerId, productId, dollars, couponCode, payinfo, notifyUrl, authorizeOnly) {
+ var time = new Date();
+ var cents = dollarsToCents(dollars);
+
+ var purchaseId, transactionId;
+
+ if (! authorizeOnly) {
+ try {
+ sqlcommon.inTransaction(function() {
+ purchaseId = _newPurchase(customerId, productId, cents, couponCode);
+ updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
+ transactionId = _newTransaction(customerId, cents);
+ _newAdjustment(transactionId, invoiceId, cents);
+ });
+ } catch (e) {
+ if (e instanceof BillingResult) { return e; }
+ if (e.javaException || e.rhinoException) {
+ throw e.javaException || e.rhinoException;
+ }
+ throw e;
+ }
+ }
+
+ var paypalResult = _doDirectPurchase(invoiceId, cents, payinfo, notifyUrl, authorizeOnly);
+
+ if (paypalResult.status == 'success') {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionSuccess(transactionId,
+ paypalResult.response.TRANSACTIONID,
+ payinfo.cardType+" ending in "+payinfo.cardNumber.substr(-4));
+ _invoicePaid(invoiceId);
+ _purchaseActive(purchaseId);
+ });
+ }
+ return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+ undefined,
+ invoiceId,
+ transactionId,
+ paypalResult.response.TRANSACTIONID,
+ purchaseId,
+ verifyDollars(dollars),
+ couponCode,
+ time,
+ undefined));
+ } else {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionFailure(transactionId,
+ (paypalResult.response ?
+ paypalResult.response.TRANSACTIONID || "":
+ ""));
+ });
+ }
+ return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
+ }
+}
+
+function _getErrorCodes(paypalResponse) {
+ var errorCodes = {userErrors: [], permanentErrors: []};
+ if (! paypalResponse) {
+ return undefined;
+ }
+ for (var i = 0; paypalResponse['L_ERRORCODE'+i]; ++i) {
+ var code = paypalResponse['L_ERRORCODE'+i];
+ var errorField = _directErrorCodes[code];
+ if (typeof(errorField) == 'function') {
+ errorField = errorField(paypalResponse);
+ }
+ if (errorField && errorField[1]) {
+ Array.prototype.push.apply(errorCodes.permanentErrors, errorField[0].split(","));
+ } else if (errorField) {
+ Array.prototype.push.apply(errorCodes.userErrors, errorField[0].split(","));
+ }
+ }
+ return errorCodes;
+}
+
+function _doDirectPurchase(invoiceId, cents, payinfo, notifyUrl, authorizeOnly) {
+ var properties = {
+ INVNUM: invoiceId,
+
+ METHOD: 'DoDirectPayment',
+ PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
+ IPADDRESS: request.clientAddr,
+ NOTIFYURL: notifyUrl,
+
+ CREDITCARDTYPE: payinfo.cardType,
+ ACCT: payinfo.cardNumber,
+ EXPDATE: payinfo.cardExpiration,
+ CVV2: payinfo.cardCvv,
+
+ SALUTATION: payinfo.nameSalutation,
+ FIRSTNAME: payinfo.nameFirst,
+ MIDDLENAME: payinfo.nameMiddle,
+ LASTNAME: payinfo.nameLast,
+ SUFFIX: payinfo.nameSuffix,
+
+ STREET: payinfo.addressStreet,
+ STREET2: payinfo.addressStreet2,
+ CITY: payinfo.addressCity,
+ STATE: payinfo.addressState,
+ COUNTRYCODE: payinfo.addressCountry,
+ ZIP: payinfo.addressZip,
+
+ AMT: centsToDollars(cents)
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+// function directAuthorization(payInfo, dollars, notifyUrl) {
+// var paypalResult = _doDirectPurchase(undefined, dollarsToCents(dollars), payInfo, notifyUrl, true);
+// if (paypalResult.status == 'success') {
+// return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+// undefined,
+// undefined,
+// paypalResult.response.TRANSACTIONID,
+// undefined,
+// verifyDollars(dollars),
+// undefined,
+// undefined,
+// undefined));
+// } else {
+// return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
+// }
+// }
+
+function asyncRecurringPurchase(invoiceId, purchaseId, oldTransactionId, paymentInfo, dollars, monthCount, notifyUrl) {
+ var time = new Date();
+ var cents = dollarsToCents(dollars);
+
+ var purchase, transactionId;
+
+ try {
+ sqlcommon.inTransaction(function() {
+ // purchaseId = _newPurchase(customerId, productId, cents, couponCode);
+ purchase = getPurchase(purchaseId);
+ updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
+ transactionId = _newTransaction(purchase.customer, cents);
+ _newAdjustment(transactionId, invoiceId, cents);
+ });
+ } catch (e) {
+ if (e instanceof BillingResult) { return e; }
+ if (e.rhinoException) {
+ throw e.rhinoException;
+ }
+ throw e;
+ }
+
+ // do transaction using previous transaction as template
+ var paypalResult;
+ if (cents == 0) {
+ // can't actually charge nothing, so fake it.
+ paypalResult = { status: 'success', response: { TRANSACTIONID: null }}
+ } else {
+ paypalResult = _doReferenceTransaction(invoiceId, cents, oldTransactionId, notifyUrl);
+ }
+
+ if (paypalResult.status == 'success') {
+ sqlcommon.inTransaction(function() {
+ _transactionSuccess(transactionId,
+ paypalResult.response.TRANSACTIONID,
+ paymentInfo);
+ _invoicePaid(invoiceId);
+ _purchaseActive(purchaseId);
+ _purchaseExtend(purchaseId, monthCount);
+ });
+ return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+ undefined,
+ invoiceId,
+ transactionId,
+ paypalResult.response.TRANSACTIONID,
+ purchaseId,
+ verifyDollars(dollars),
+ undefined,
+ time,
+ undefined));
+ } else {
+ sqlcommon.inTransaction(function() {
+ _transactionFailure(transactionId,
+ (paypalResult.response ?
+ paypalResult.response.TRANSACTIONID || "":
+ ""));
+ });
+ return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
+ }
+}
+
+function _doReferenceTransaction(invoiceId, cents, transactionId, notifyUrl) {
+ var properties = {
+ METHOD: 'DoReferenceTransaction',
+ PAYMENTACTION: 'Sale',
+
+ REFERENCEID: transactionId,
+ AMT: centsToDollars(cents),
+ INVNUM: invoiceId
+ }
+
+ return _doPaypalNvpPost(properties);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/billing/fields.js b/etherpad/src/etherpad/billing/fields.js
new file mode 100644
index 0000000..4a307ac
--- /dev/null
+++ b/etherpad/src/etherpad/billing/fields.js
@@ -0,0 +1,219 @@
+/**
+ * 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.
+ */
+
+
+// Taken from paypal's form
+var countryList = [
+ ["US", "United States"],
+ ["AL", "Albania"],
+ ["DZ", "Algeria"],
+ ["AD", "Andorra"],
+ ["AO", "Angola"],
+ ["AI", "Anguilla"],
+ ["AG", "Antigua and Barbuda"],
+ ["AR", "Argentina"],
+ ["AM", "Armenia"],
+ ["AW", "Aruba"],
+ ["AU", "Australia"],
+ ["AT", "Austria"],
+ ["AZ", "Azerbaijan Republic"],
+ ["BS", "Bahamas"],
+ ["BH", "Bahrain"],
+ ["BB", "Barbados"],
+ ["BE", "Belgium"],
+ ["BZ", "Belize"],
+ ["BJ", "Benin"],
+ ["BM", "Bermuda"],
+ ["BT", "Bhutan"],
+ ["BO", "Bolivia"],
+ ["BA", "Bosnia and Herzegovina"],
+ ["BW", "Botswana"],
+ ["BR", "Brazil"],
+ ["VG", "British Virgin Islands"],
+ ["BN", "Brunei"],
+ ["BG", "Bulgaria"],
+ ["BF", "Burkina Faso"],
+ ["BI", "Burundi"],
+ ["KH", "Cambodia"],
+ ["CA", "Canada"],
+ ["CV", "Cape Verde"],
+ ["KY", "Cayman Islands"],
+ ["TD", "Chad"],
+ ["CL", "Chile"],
+ ["C2", "China"],
+ ["CO", "Colombia"],
+ ["KM", "Comoros"],
+ ["CK", "Cook Islands"],
+ ["CR", "Costa Rica"],
+ ["HR", "Croatia"],
+ ["CY", "Cyprus"],
+ ["CZ", "Czech Republic"],
+ ["CD", "Democratic Republic of the Congo"],
+ ["DK", "Denmark"],
+ ["DJ", "Djibouti"],
+ ["DM", "Dominica"],
+ ["DO", "Dominican Republic"],
+ ["EC", "Ecuador"],
+ ["SV", "El Salvador"],
+ ["ER", "Eritrea"],
+ ["EE", "Estonia"],
+ ["ET", "Ethiopia"],
+ ["FK", "Falkland Islands"],
+ ["FO", "Faroe Islands"],
+ ["FM", "Federated States of Micronesia"],
+ ["FJ", "Fiji"],
+ ["FI", "Finland"],
+ ["FR", "France"],
+ ["GF", "French Guiana"],
+ ["PF", "French Polynesia"],
+ ["GA", "Gabon Republic"],
+ ["GM", "Gambia"],
+ ["DE", "Germany"],
+ ["GI", "Gibraltar"],
+ ["GR", "Greece"],
+ ["GL", "Greenland"],
+ ["GD", "Grenada"],
+ ["GP", "Guadeloupe"],
+ ["GT", "Guatemala"],
+ ["GN", "Guinea"],
+ ["GW", "Guinea Bissau"],
+ ["GY", "Guyana"],
+ ["HN", "Honduras"],
+ ["HK", "Hong Kong"],
+ ["HU", "Hungary"],
+ ["IS", "Iceland"],
+ ["IN", "India"],
+ ["ID", "Indonesia"],
+ ["IE", "Ireland"],
+ ["IL", "Israel"],
+ ["IT", "Italy"],
+ ["JM", "Jamaica"],
+ ["JP", "Japan"],
+ ["JO", "Jordan"],
+ ["KZ", "Kazakhstan"],
+ ["KE", "Kenya"],
+ ["KI", "Kiribati"],
+ ["KW", "Kuwait"],
+ ["KG", "Kyrgyzstan"],
+ ["LA", "Laos"],
+ ["LV", "Latvia"],
+ ["LS", "Lesotho"],
+ ["LI", "Liechtenstein"],
+ ["LT", "Lithuania"],
+ ["LU", "Luxembourg"],
+ ["MG", "Madagascar"],
+ ["MW", "Malawi"],
+ ["MY", "Malaysia"],
+ ["MV", "Maldives"],
+ ["ML", "Mali"],
+ ["MT", "Malta"],
+ ["MH", "Marshall Islands"],
+ ["MQ", "Martinique"],
+ ["MR", "Mauritania"],
+ ["MU", "Mauritius"],
+ ["YT", "Mayotte"],
+ ["MX", "Mexico"],
+ ["MN", "Mongolia"],
+ ["MS", "Montserrat"],
+ ["MA", "Morocco"],
+ ["MZ", "Mozambique"],
+ ["NA", "Namibia"],
+ ["NR", "Nauru"],
+ ["NP", "Nepal"],
+ ["NL", "Netherlands"],
+ ["AN", "Netherlands Antilles"],
+ ["NC", "New Caledonia"],
+ ["NZ", "New Zealand"],
+ ["NI", "Nicaragua"],
+ ["NE", "Niger"],
+ ["NU", "Niue"],
+ ["NF", "Norfolk Island"],
+ ["NO", "Norway"],
+ ["OM", "Oman"],
+ ["PW", "Palau"],
+ ["PA", "Panama"],
+ ["PG", "Papua New Guinea"],
+ ["PE", "Peru"],
+ ["PN", "Pitcairn Islands"],
+ ["PL", "Poland"],
+ ["PT", "Portugal"],
+ ["QA", "Qatar"],
+ ["CG", "Republic of the Congo"],
+ ["RE", "Reunion"],
+ ["RO", "Romania"],
+ ["RU", "Russia"],
+ ["VC", "Saint Vincent and the Grenadines"],
+ ["WS", "Samoa"],
+ ["SM", "San Marino"],
+ ["ST", "São Tomé and Príncipe"],
+ ["SA", "Saudi Arabia"],
+ ["SN", "Senegal"],
+ ["SC", "Seychelles"],
+ ["SL", "Sierra Leone"],
+ ["SG", "Singapore"],
+ ["SK", "Slovakia"],
+ ["SI", "Slovenia"],
+ ["SB", "Solomon Islands"],
+ ["SO", "Somalia"],
+ ["ZA", "South Africa"],
+ ["KR", "South Korea"],
+ ["ES", "Spain"],
+ ["LK", "Sri Lanka"],
+ ["SH", "St. Helena"],
+ ["KN", "St. Kitts and Nevis"],
+ ["LC", "St. Lucia"],
+ ["PM", "St. Pierre and Miquelon"],
+ ["SR", "Suriname"],
+ ["SJ", "Svalbard and Jan Mayen Islands"],
+ ["SZ", "Swaziland"],
+ ["SE", "Sweden"],
+ ["CH", "Switzerland"],
+ ["TW", "Taiwan"],
+ ["TJ", "Tajikistan"],
+ ["TZ", "Tanzania"],
+ ["TH", "Thailand"],
+ ["TG", "Togo"],
+ ["TO", "Tonga"],
+ ["TT", "Trinidad and Tobago"],
+ ["TN", "Tunisia"],
+ ["TR", "Turkey"],
+ ["TM", "Turkmenistan"],
+ ["TC", "Turks and Caicos Islands"],
+ ["TV", "Tuvalu"],
+ ["UG", "Uganda"],
+ ["UA", "Ukraine"],
+ ["AE", "United Arab Emirates"],
+ ["GB", "United Kingdom"],
+ ["UY", "Uruguay"],
+ ["VU", "Vanuatu"],
+ ["VA", "Vatican City State"],
+ ["VE", "Venezuela"],
+ ["VN", "Vietnam"],
+ ["WF", "Wallis and Futuna Islands"],
+ ["YE", "Yemen"],
+ ["ZM", "Zambia"],
+];
+
+var usaStateList = [
+ "", "AK", "AL", "AR", "AZ", "CA", "CO", "CT", "DC", "DE",
+ "FL", "GA", "HI", "IA", "ID", "IL", "IN", "KS", "KY", "LA",
+ "MA", "MD", "ME", "MI", "MN", "MO", "MS", "MT", "NC", "ND",
+ "NE", "NH", "NJ", "NM", "NV", "NY", "OH", "OK", "OR", "PA",
+ "RI", "SC", "SD", "TN", "TX", "UT", "VA", "VT", "WA", "WI",
+ "WV", "WY", "AA", "AE", "AP", "AS", "FM", "GU", "MH", "MP",
+ "PR", "PW", "VI"
+];
+
diff --git a/etherpad/src/etherpad/billing/team_billing.js b/etherpad/src/etherpad/billing/team_billing.js
new file mode 100644
index 0000000..ae8ae8a
--- /dev/null
+++ b/etherpad/src/etherpad/billing/team_billing.js
@@ -0,0 +1,422 @@
+/**
+ * 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.
+ */
+
+import("execution");
+import("exceptionutils");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+
+import("etherpad.billing.billing");
+import("etherpad.globals");
+import("etherpad.log");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_quotas");
+import("etherpad.store.checkout");
+import("etherpad.utils.renderTemplateAsString");
+
+jimport("java.lang.System.out.println");
+
+function recurringBillingNotifyUrl() {
+ return "";
+}
+
+function _billing() {
+ if (! appjet.cache.billing) {
+ appjet.cache.billing = {};
+ }
+ return appjet.cache.billing;
+}
+
+function _lpad(str, width, padDigit) {
+ str = String(str);
+ padDigit = (padDigit === undefined ? ' ' : padDigit);
+ var count = width - str.length;
+ var prepend = []
+ for (var i = 0; i < count; ++i) {
+ prepend.push(padDigit);
+ }
+ return prepend.join("")+str;
+}
+
+// utility functions
+
+function _dayToDateTime(date) {
+ return [date.getFullYear(), _lpad(date.getMonth()+1, 2, '0'), _lpad(date.getDate(), 2, '0')].join("-");
+}
+
+function _createInvoice(subscription) {
+ var maxUsers = getMaxUsers(subscription.customer);
+ var invoice = inTransaction(function() {
+ var invoiceId = billing.createInvoice();
+ billing.updateInvoice(
+ invoiceId,
+ {purchase: subscription.id,
+ amt: billing.dollarsToCents(calculateSubscriptionCost(maxUsers, subscription.coupon)),
+ users: maxUsers});
+ return billing.getInvoice(invoiceId);
+ });
+ if (invoice) {
+ resetMaxUsers(subscription.customer)
+ }
+ return invoice;
+}
+
+function getExpiredSubscriptions(date) {
+ return sqlobj.selectMulti('billing_purchase',
+ {type: 'subscription',
+ status: 'active',
+ paidThrough: ['<', _dayToDateTime(date)]});
+}
+
+function getAllSubscriptions() {
+ return sqlobj.selectMulti('billing_purchase', {type: 'subscription', status: 'active'});
+}
+
+function getSubscriptionForCustomer(customerId) {
+ return sqlobj.selectSingle('billing_purchase',
+ {type: 'subscription',
+ customer: customerId});
+}
+
+function getOrCreateInvoice(subscription) {
+ return inTransaction(function() {
+ var existingInvoice =
+ sqlobj.selectSingle('billing_invoice',
+ {purchase: subscription.id, status: 'pending'});
+ if (existingInvoice) {
+ return existingInvoice;
+ } else {
+ return _createInvoice(subscription);
+ }
+ });
+}
+
+function getLatestPendingInvoice(subscriptionId) {
+ return sqlobj.selectMulti('billing_invoice',
+ {purchase: subscriptionId, status: 'pending'},
+ {orderBy: '-time', limit: 1})[0];
+}
+
+function getLatestPaidInvoice(subscriptionId) {
+ return sqlobj.selectMulti('billing_invoice',
+ {purchase: subscriptionId, status: 'paid'},
+ {orderBy: '-time', limit: 1})[0];
+}
+
+function pendingTransactions(customer) {
+ return billing.getPendingTransactionsForCustomer(customer);
+}
+
+function checkPendingTransactions(transactions) {
+ // XXX: do nothing for now.
+ return transactions.length > 0;
+}
+
+function getRecurringBillingTransactionId(customerId) {
+ return sqlobj.selectSingle('billing_payment_info', {customer: customerId}).transaction;
+}
+
+function getRecurringBillingInfo(customerId) {
+ return sqlobj.selectSingle('billing_payment_info', {customer: customerId});
+}
+
+function clearRecurringBillingInfo(customerId) {
+ return sqlobj.deleteRows('billing_payment_info', {customer: customerId});
+}
+
+function setRecurringBillingInfo(customerId, fullName, email, paymentSummary, expiration, transactionId) {
+ var info = {
+ fullname: fullName,
+ email: email,
+ paymentsummary: paymentSummary,
+ expiration: expiration,
+ transaction: transactionId
+ }
+ inTransaction(function() {
+ if (sqlobj.selectSingle('billing_payment_info', {customer: customerId})) {
+ sqlobj.update('billing_payment_info', {customer: customerId}, info);
+ } else {
+ info.customer = customerId;
+ sqlobj.insert('billing_payment_info', info);
+ }
+ });
+}
+
+function createSubscription(customerId, couponCode) {
+ domainCacheClear(customerId);
+ return inTransaction(function() {
+ return billing.createSubscription(customerId, 'ONDEMAND', 0, couponCode);
+ });
+}
+
+function updateSubscriptionCouponCode(subscriptionId, couponCode) {
+ billing.updatePurchase(subscriptionId, {coupon: couponCode || ""});
+}
+
+function subscriptionChargeFailure(subscription, invoice, failureMessage) {
+ billing.updatePurchase(subscription.id,
+ {error: failureMessage, status: 'inactive'});
+ sendFailureEmail(subscription, invoice);
+}
+
+function subscriptionChargeSuccess(subscription, invoice) {
+ sendReceiptEmail(subscription, invoice);
+}
+
+function errorFieldsToMessage(errorCodes) {
+ var prefix = "Your payment information was rejected. Please verify your ";
+ var errorList = (errorCodes.permanentErrors ? errorCodes.permanentErrors : errorCodes.userErrors);
+
+ return prefix +
+ errorList.map(function(field) {
+ return checkout.billingCartFieldMap[field].d;
+ }).join(", ")+
+ "."
+}
+
+function getAllInvoices(customer) {
+ var purchase = getSubscriptionForCustomer(customer);
+ if (! purchase) {
+ return [];
+ }
+ return billing.getInvoicesForPurchase(purchase.id);
+}
+
+// scheduled charges
+
+function attemptCharge(invoice, subscription) {
+ var billingInfo = getRecurringBillingInfo(subscription.customer);
+ if (! billingInfo) {
+ subscriptionChargeFailure(subscription, invoice, "No billing information on file.");
+ return false;
+ }
+
+ var result =
+ billing.asyncRecurringPurchase(
+ invoice.id,
+ subscription.id,
+ billingInfo.transaction,
+ billingInfo.paymentsummary,
+ billing.centsToDollars(invoice.amt),
+ 1, // 1 month only for now
+ recurringBillingNotifyUrl);
+ if (result.status == 'success') {
+ subscriptionChargeSuccess(subscription, invoice);
+ return true;
+ } else {
+ subscriptionChargeFailure(subscription, invoice, errorFieldsToMessage(result.errorField));
+ return false;
+ }
+}
+
+function processSubscription(subscription) {
+ try {
+ var hasPendingTransactions = inTransaction(function() {
+ var transactions = pendingTransactions(subscription.customer);
+ if (checkPendingTransactions(transactions)) {
+ billing.log({type: 'pending-transactions-delay', subscription: subscription, transactions: transactions});
+ // there are actual pending transactions. wait until tomorrow.
+ return true;
+ } else {
+ return false;
+ }
+ });
+ if (hasPendingTransactions) {
+ return;
+ }
+ var invoice = getOrCreateInvoice(subscription);
+
+ return attemptCharge(invoice, subscription);
+ } catch (e) {
+ log.logException(e);
+ billing.log({message: "Thrown error",
+ exception: exceptionutils.getStackTracePlain(e),
+ subscription: subscription});
+ subscriptionChargeFailure(subscription, "Permanent failure. Please confirm your billing information.");
+ } finally {
+ domainCacheClear(subscription.customer);
+ }
+}
+
+function processAllSubscriptions() {
+ var subs = getExpiredSubscriptions(new Date);
+ println("processing "+subs.length+" subscriptions.");
+ subs.forEach(processSubscription);
+}
+
+function _scheduleNextDailyUpdate() {
+ // Run at 2:22am every day
+ var now = +(new Date);
+ var tomorrow = new Date(now + 1000*60*60*24);
+ tomorrow.setHours(2);
+ tomorrow.setMinutes(22);
+ tomorrow.setMilliseconds(222);
+ log.info("Scheduling next daily billing update for: "+tomorrow.toString());
+ var delay = +tomorrow - (+(new Date));
+ execution.scheduleTask('billing', "billingDailyUpdate", delay, []);
+}
+
+serverhandlers.tasks.billingDailyUpdate = function() {
+ return; // do nothing, there's no more billing.
+ // if (! globals.isProduction()) { return; }
+ // try {
+ // processAllSubscriptions();
+ // } finally {
+ // _scheduleNextDailyUpdate();
+ // }
+}
+
+function onStartup() {
+ execution.initTaskThreadPool("billing", 1);
+ _scheduleNextDailyUpdate();
+}
+
+// pricing
+
+function getMaxUsers(customer) {
+ return pro_quotas.getAccountUsageCount(customer);
+}
+
+function resetMaxUsers(customer) {
+ pro_quotas.resetAccountUsageCount(customer);
+}
+
+var COST_PER_USER = 8;
+
+function getCouponValue(couponCode) {
+ if (couponCode && couponCode.length == 8) {
+ return sqlobj.selectSingle('checkout_pro_referral', {id: couponCode});
+ }
+}
+
+function calculateSubscriptionCost(users, couponId) {
+ if (users <= globals.PRO_FREE_ACCOUNTS) {
+ return 0;
+ }
+ var coupon = getCouponValue(couponId);
+ var pctDiscount = (coupon ? coupon.pctDiscount : 0);
+ var freeUsers = (coupon ? coupon.freeUsers : 0);
+
+ var cost = (users - freeUsers) * COST_PER_USER;
+ cost = cost * (100-pctDiscount)/100;
+
+ return Math.max(0, cost);
+}
+
+// currentDomainsCache
+
+function _cache() {
+ if (! appjet.cache.currentDomainsCache) {
+ appjet.cache.currentDomainsCache = {};
+ }
+ return appjet.cache.currentDomainsCache;
+}
+
+function domainCacheClear(domain) {
+ delete _cache()[domain];
+}
+
+function _domainCacheGetOrUpdate(domain, f) {
+ if (domain in _cache()) {
+ return _cache()[domain];
+ }
+
+ _cache()[domain] = f();
+ return _cache()[domain];
+}
+
+// external API helpers
+
+function _getPaidThroughDate(domainId) {
+ return _domainCacheGetOrUpdate(domainId, function() {
+ var subscription = getSubscriptionForCustomer(domainId);
+ if (! subscription) {
+ return null;
+ } else {
+ return subscription.paidThrough;
+ }
+ });
+}
+
+// external API
+
+var GRACE_PERIOD_DAYS = 10;
+
+var CURRENT = 0;
+var PAST_DUE = 1;
+var SUSPENDED = 2;
+var NO_BILLING_INFO = 3;
+
+function getDomainStatus(domainId) {
+ var paidThrough = _getPaidThroughDate(domainId);
+
+ if (paidThrough == null) {
+ return NO_BILLING_INFO;
+ }
+ if (paidThrough.getTime() > new Date(Date.now()-86400*1000)) {
+ return CURRENT;
+ }
+ // less than GRACE_PERIOD_DAYS have passed since paidThrough date
+ if (paidThrough.getTime() > Date.now() - GRACE_PERIOD_DAYS*86400*1000) {
+ return PAST_DUE;
+ }
+ return SUSPENDED;
+}
+
+function getDomainDueDate(domainId) {
+ return _getPaidThroughDate(domainId);
+}
+
+function getDomainSuspensionDate(domainId) {
+ return new Date(_getPaidThroughDate(domainId).getTime() + GRACE_PERIOD_DAYS*86400*1000);
+}
+
+// emails
+
+function sendReceiptEmail(subscription, invoice) {
+ var paymentInfo = getRecurringBillingInfo(subscription.customer);
+ var coupon = getCouponValue(subscription.coupon);
+ var emailText = renderTemplateAsString('email/pro_payment_receipt.ejs', {
+ fullName: paymentInfo.fullname,
+ paymentSummary: paymentInfo.paymentsummary,
+ expiration: checkout.formatExpiration(paymentInfo.expiration),
+ invoiceNumber: invoice.id,
+ numUsers: invoice.users,
+ cost: billing.centsToDollars(invoice.amt),
+ dollars: checkout.dollars,
+ coupon: coupon,
+ globals: globals
+ });
+ var address = paymentInfo.email;
+ checkout.salesEmail(address, "sales@pad.spline.inf.fu-berlin.de", "EtherPad: Receipt for "+paymentInfo.fullname,
+ {}, emailText);
+}
+
+function sendFailureEmail(subscription, invoice, failureMessage) {
+ var domain = subscription.customer;
+ var subDomain = domains.getDomainRecord(domain).subDomain;
+ var paymentInfo = getRecurringBillingInfo(subscription.customer);
+ var emailText = renderTemplateAsString('email/pro_payment_failure.ejs', {
+ fullName: paymentInfo.fullname,
+ billingError: failureMessage,
+ balance: "US $"+checkout.dollars(billing.centsToDollars(invoice.amt)),
+ suspensionDate: checkout.formatDate(new Date(subscription.paidThrough.getTime()+GRACE_PERIOD_DAYS*86400*1000)),
+ billingAdminLink: "https://"+subDomain+".pad.spline.inf.fu-berlin.de/ep/admin/billing/"
+ });
+ var address = paymentInfo.email;
+ checkout.salesEmail(address, "sales@pad.spline.inf.fu-berlin.de", "EtherPad: Payment Failure for "+paymentInfo.fullname,
+ {}, emailText);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/collab/collab_server.js b/etherpad/src/etherpad/collab/collab_server.js
new file mode 100644
index 0000000..78c9921
--- /dev/null
+++ b/etherpad/src/etherpad/collab/collab_server.js
@@ -0,0 +1,778 @@
+/**
+ * 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.
+ */
+
+import("comet");
+import("ejs");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("etherpad.log");
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.pad.padevents");
+import("etherpad.pad.pad_security");
+import("etherpad.pro.pro_padmeta");
+import("fastJSON");
+import("fileutils.readFile");
+import("jsutils.{eachProperty,keys}");
+import("etherpad.collab.collabroom_server.*");
+import("etherpad.collab.readonly_server");
+jimport("java.util.concurrent.ConcurrentHashMap");
+
+var PADPAGE_ROOMTYPE = "padpage";
+
+function onStartup() {
+
+}
+
+function _padIdToRoom(padId) {
+ return "padpage/"+padId;
+}
+
+function _roomToPadId(roomName) {
+ return roomName.substring(roomName.indexOf("/")+1);
+}
+
+function removeFromMemory(pad) {
+ // notification so we can free stuff
+ if (getNumConnections(pad) == 0) {
+ var tempObj = pad.tempObj();
+ tempObj.revisionSockets = {};
+ }
+}
+
+function _getPadConnections(pad) {
+ return getRoomConnections(_padIdToRoom(pad.getId()));
+}
+
+function guestKnock(globalPadId, guestId, displayName) {
+ var askedSomeone = false;
+
+ // requires that we somehow have permission on this pad
+ model.accessPadGlobal(globalPadId, function(pad) {
+ var connections = _getPadConnections(pad);
+ connections.forEach(function(connection) {
+ // only send to pro users
+ if (! padusers.isGuest(connection.data.userInfo.userId)) {
+ askedSomeone = true;
+ var msg = { type: "SERVER_MESSAGE",
+ payload: { type: 'GUEST_PROMPT',
+ userId: guestId,
+ displayName: displayName } };
+ sendMessage(connection.connectionId, msg);
+ }
+ });
+ });
+
+ if (! askedSomeone) {
+ pad_security.answerKnock(guestId, globalPadId, "denied");
+ }
+}
+
+function _verifyUserId(userId) {
+ var result;
+ if (padusers.isGuest(userId)) {
+ // allow cookie-verified guest even if user has signed in
+ result = (userId == padusers.getGuestUserId());
+ }
+ else {
+ result = (userId == padusers.getUserId());
+ }
+ return result;
+}
+
+function _checkChangesetAndPool(cs, pool) {
+ Changeset.checkRep(cs);
+ Changeset.eachAttribNumber(cs, function(n) {
+ if (! pool.getAttrib(n)) {
+ throw new Error("Attribute pool is missing attribute "+n+" for changeset "+cs);
+ }
+ });
+}
+
+function _doWarn(str) {
+ log.warn(appjet.executionId+": "+str);
+}
+
+function _doInfo(str) {
+ log.info(appjet.executionId+": "+str);
+}
+
+function _getPadRevisionSockets(pad) {
+ var revisionSockets = pad.tempObj().revisionSockets;
+ if (! revisionSockets) {
+ revisionSockets = {}; // rev# -> socket id
+ pad.tempObj().revisionSockets = revisionSockets;
+ }
+ return revisionSockets;
+}
+
+function applyUserChanges(pad, baseRev, changeset, optSocketId, optAuthor) {
+ // changeset must be already adapted to the server's apool
+
+ var apool = pad.pool();
+ var r = baseRev;
+ while (r < pad.getHeadRevisionNumber()) {
+ r++;
+ var c = pad.getRevisionChangeset(r);
+ changeset = Changeset.follow(c, changeset, false, apool);
+ }
+
+ var prevText = pad.text();
+ if (Changeset.oldLen(changeset) != prevText.length) {
+ _doWarn("Can't apply USER_CHANGES "+changeset+" to document of length "+
+ prevText.length);
+ return;
+ }
+
+ var thisAuthor = '';
+ if (optSocketId) {
+ var connectionId = getSocketConnectionId(optSocketId);
+ if (connectionId) {
+ var connection = getConnection(connectionId);
+ if (connection) {
+ thisAuthor = connection.data.userInfo.userId;
+ }
+ }
+ }
+ if (optAuthor) {
+ thisAuthor = optAuthor;
+ }
+
+ pad.appendRevision(changeset, thisAuthor);
+ var newRev = pad.getHeadRevisionNumber();
+ if (optSocketId) {
+ _getPadRevisionSockets(pad)[newRev] = optSocketId;
+ }
+
+ var correctionChangeset = _correctMarkersInPad(pad.atext(), pad.pool());
+ if (correctionChangeset) {
+ pad.appendRevision(correctionChangeset);
+ }
+
+ ///// make document end in blank line if it doesn't:
+ if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
+ var nlChangeset = Changeset.makeSplice(
+ pad.text(), pad.text().length-1, 0, "\n");
+ pad.appendRevision(nlChangeset);
+ }
+
+ updatePadClients(pad);
+
+ activepads.touch(pad.getId());
+ padevents.onEditPad(pad, thisAuthor);
+}
+
+function updateClient(pad, connectionId) {
+ var conn = getConnection(connectionId);
+ if (! conn) {
+ return;
+ }
+ var lastRev = conn.data.lastRev;
+ var userId = conn.data.userInfo.userId;
+ var socketId = conn.socketId;
+ while (lastRev < pad.getHeadRevisionNumber()) {
+ var r = ++lastRev;
+ var author = pad.getRevisionAuthor(r);
+ var revisionSockets = _getPadRevisionSockets(pad);
+ if (revisionSockets[r] === socketId) {
+ sendMessage(connectionId, {type:"ACCEPT_COMMIT", newRev:r});
+ }
+ else {
+ var forWire = Changeset.prepareForWire(pad.getRevisionChangeset(r), pad.pool());
+ var msg = {type:"NEW_CHANGES", newRev:r,
+ changeset: forWire.translated,
+ apool: forWire.pool,
+ author: author};
+ sendMessage(connectionId, msg);
+ }
+ }
+ conn.data.lastRev = pad.getHeadRevisionNumber();
+ updateRoomConnectionData(connectionId, conn.data);
+}
+
+function updatePadClients(pad) {
+ _getPadConnections(pad).forEach(function(connection) {
+ updateClient(pad, connection.connectionId);
+ });
+
+ readonly_server.updatePadClients(pad);
+}
+
+function applyMissedChanges(pad, missedChanges) {
+ var userInfo = missedChanges.userInfo;
+ var baseRev = missedChanges.baseRev;
+ var committedChangeset = missedChanges.committedChangeset; // may be falsy
+ var furtherChangeset = missedChanges.furtherChangeset; // may be falsy
+ var apool = pad.pool();
+
+ if (! _verifyUserId(userInfo.userId)) {
+ return;
+ }
+
+ if (committedChangeset) {
+ var wireApool1 = (new AttribPool()).fromJsonable(missedChanges.committedChangesetAPool);
+ _checkChangesetAndPool(committedChangeset, wireApool1);
+ committedChangeset = pad.adoptChangesetAttribs(committedChangeset, wireApool1);
+ }
+ if (furtherChangeset) {
+ var wireApool2 = (new AttribPool()).fromJsonable(missedChanges.furtherChangesetAPool);
+ _checkChangesetAndPool(furtherChangeset, wireApool2);
+ furtherChangeset = pad.adoptChangesetAttribs(furtherChangeset, wireApool2);
+ }
+
+ var commitWasMissed = !! committedChangeset;
+ if (commitWasMissed) {
+ var commitSocketId = missedChanges.committedChangesetSocketId;
+ var revisionSockets = _getPadRevisionSockets(pad);
+ // was the commit really missed, or did the client just not hear back?
+ // look for later changeset by this socket
+ var r = baseRev;
+ while (r < pad.getHeadRevisionNumber()) {
+ r++;
+ var s = revisionSockets[r];
+ if (! s) {
+ // changes are too old, have to drop them.
+ return;
+ }
+ if (s == commitSocketId) {
+ commitWasMissed = false;
+ break;
+ }
+ }
+ }
+ if (! commitWasMissed) {
+ // commit already incorporated by the server
+ committedChangeset = null;
+ }
+
+ var changeset;
+ if (committedChangeset && furtherChangeset) {
+ changeset = Changeset.compose(committedChangeset, furtherChangeset, apool);
+ }
+ else {
+ changeset = (committedChangeset || furtherChangeset);
+ }
+
+ if (changeset) {
+ var author = userInfo.userId;
+
+ applyUserChanges(pad, baseRev, changeset, null, author);
+ }
+}
+
+function getAllPadsWithConnections() {
+ // returns array of global pad id strings
+ return getAllRoomsOfType(PADPAGE_ROOMTYPE).map(_roomToPadId);
+}
+
+function broadcastServerMessage(msgObj) {
+ var msg = {type: "SERVER_MESSAGE", payload: msgObj};
+ getAllRoomsOfType(PADPAGE_ROOMTYPE).forEach(function(roomName) {
+ getRoomConnections(roomName).forEach(function(connection) {
+ sendMessage(connection.connectionId, msg);
+ });
+ });
+}
+
+function appendPadText(pad, txt) {
+ txt = model.cleanText(txt);
+ var oldFullText = pad.text();
+ _applyChangesetToPad(pad, Changeset.makeSplice(oldFullText,
+ oldFullText.length-1, 0, txt));
+}
+
+function setPadText(pad, txt) {
+ txt = model.cleanText(txt);
+ var oldFullText = pad.text();
+ // replace text except for the existing final (virtual) newline
+ _applyChangesetToPad(pad, Changeset.makeSplice(oldFullText, 0,
+ oldFullText.length-1, txt));
+}
+
+function setPadAText(pad, atext) {
+ var oldFullText = pad.text();
+ var deletion = Changeset.makeSplice(oldFullText, 0, oldFullText.length-1, "");
+
+ var assem = Changeset.smartOpAssembler();
+ Changeset.appendATextToAssembler(atext, assem);
+ var charBank = atext.text.slice(0, -1);
+ var insertion = Changeset.checkRep(Changeset.pack(1, atext.text.length,
+ assem.toString(), charBank));
+
+ var cs = Changeset.compose(deletion, insertion, pad.pool());
+ Changeset.checkRep(cs);
+
+ _applyChangesetToPad(pad, cs);
+}
+
+function applyChangesetToPad(pad, changeset) {
+ Changeset.checkRep(changeset);
+
+ _applyChangesetToPad(pad, changeset);
+}
+
+function _applyChangesetToPad(pad, changeset) {
+ pad.appendRevision(changeset);
+ updatePadClients(pad);
+}
+
+function getHistoricalAuthorData(pad, author) {
+ var authorData = pad.getAuthorData(author);
+ if (authorData) {
+ var data = {};
+ if ((typeof authorData.colorId) == "number") {
+ data.colorId = authorData.colorId;
+ }
+ if (authorData.name) {
+ data.name = authorData.name;
+ }
+ else {
+ var uname = padusers.getNameForUserId(author);
+ if (uname) {
+ data.name = uname;
+ }
+ }
+ return data;
+ }
+ return null;
+}
+
+function buildHistoricalAuthorDataMapFromAText(pad, atext) {
+ var map = {};
+ pad.eachATextAuthor(atext, function(author, authorNum) {
+ var data = getHistoricalAuthorData(pad, author);
+ if (data) {
+ map[author] = data;
+ }
+ });
+ return map;
+}
+
+function buildHistoricalAuthorDataMapForPadHistory(pad) {
+ var map = {};
+ pad.pool().eachAttrib(function(key, value) {
+ if (key == 'author') {
+ var author = value;
+ var data = getHistoricalAuthorData(pad, author);
+ if (data) {
+ map[author] = data;
+ }
+ }
+ });
+ return map;
+}
+
+function getATextForWire(pad, optRev) {
+ var atext;
+ if ((optRev && ! isNaN(Number(optRev))) || (typeof optRev) == "number") {
+ atext = pad.getInternalRevisionAText(Number(optRev));
+ }
+ else {
+ atext = pad.atext();
+ }
+
+ var historicalAuthorData = buildHistoricalAuthorDataMapFromAText(pad, atext);
+
+ var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool());
+ var apool = attribsForWire.pool;
+ // mutate atext (translate attribs for wire):
+ atext.attribs = attribsForWire.translated;
+
+ return {atext:atext, apool:apool.toJsonable(),
+ historicalAuthorData:historicalAuthorData };
+}
+
+function getCollabClientVars(pad) {
+ // construct object that is made available on the client
+ // as collab_client_vars
+
+ var forWire = getATextForWire(pad);
+
+ return {
+ initialAttributedText: forWire.atext,
+ rev: pad.getHeadRevisionNumber(),
+ padId: pad.getLocalId(),
+ globalPadId: pad.getId(),
+ historicalAuthorData: forWire.historicalAuthorData,
+ apool: forWire.apool,
+ clientIp: request.clientAddr,
+ clientAgent: request.headers["User-Agent"]
+ };
+}
+
+function getNumConnections(pad) {
+ return _getPadConnections(pad).length;
+}
+
+function getConnectedUsers(pad) {
+ var users = [];
+ _getPadConnections(pad).forEach(function(connection) {
+ users.push(connection.data.userInfo);
+ });
+ return users;
+}
+
+
+function bootAllUsersFromPad(pad, reason) {
+ return bootUsersFromPad(pad, reason);
+}
+
+function bootUsersFromPad(pad, reason, userInfoFilter) {
+ var connections = _getPadConnections(pad);
+ var bootedUserInfos = [];
+ connections.forEach(function(connection) {
+ if ((! userInfoFilter) || userInfoFilter(connection.data.userInfo)) {
+ bootedUserInfos.push(connection.data.userInfo);
+ bootConnection(connection.connectionId);
+ }
+ });
+ return bootedUserInfos;
+}
+
+function dumpStorageToString(pad) {
+ var lines = [];
+ var errors = [];
+ var head = pad.getHeadRevisionNumber();
+ try {
+ for(var i=0;i<=head;i++) {
+ lines.push("changeset "+i+" "+Changeset.toBaseTen(pad.getRevisionChangeset(i)));
+ }
+ }
+ catch (e) {
+ errors.push("!!!!! Error in changeset "+i+": "+e.message);
+ }
+ for(var i=0;i<=head;i++) {
+ lines.push("author "+i+" "+pad.getRevisionAuthor(i));
+ }
+ for(var i=0;i<=head;i++) {
+ lines.push("time "+i+" "+pad.getRevisionDate(i));
+ }
+ var revisionSockets = _getPadRevisionSockets(pad);
+ for(var k in revisionSockets) lines.push("socket "+k+" "+revisionSockets[k]);
+ return errors.concat(lines).join('\n');
+}
+
+function _getPadIdForSocket(socketId) {
+ var connectionId = getSocketConnectionId(socketId);
+ if (connectionId) {
+ var connection = getConnection(connectionId);
+ if (connection) {
+ return _roomToPadId(connection.roomName);
+ }
+ }
+ return null;
+}
+
+function _getUserIdForSocket(socketId) {
+ var connectionId = getSocketConnectionId(socketId);
+ if (connectionId) {
+ var connection = getConnection(connectionId);
+ if (connection) {
+ return connection.data.userInfo.userId;
+ }
+ }
+ return null;
+}
+
+function _serverDebug(msg) { /* nothing */ }
+
+function _accessSocketPad(socketId, accessType, padFunc, dontRequirePad) {
+ return _accessCollabPad(_getPadIdForSocket(socketId), accessType,
+ padFunc, dontRequirePad);
+}
+
+function _accessConnectionPad(connection, accessType, padFunc, dontRequirePad) {
+ return _accessCollabPad(_roomToPadId(connection.roomName), accessType,
+ padFunc, dontRequirePad);
+}
+
+function _accessCollabPad(padId, accessType, padFunc, dontRequirePad) {
+ if (! padId) {
+ if (! dontRequirePad) {
+ _doWarn("Collab operation \""+accessType+"\" aborted because socket "+socketId+" has no pad.");
+ }
+ return;
+ }
+ else {
+ return _accessExistingPad(padId, accessType, function(pad) {
+ return padFunc(pad);
+ }, dontRequirePad);
+ }
+}
+
+function _accessExistingPad(padId, accessType, padFunc, dontRequireExist) {
+ return model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ if (! dontRequireExist) {
+ _doWarn("Collab operation \""+accessType+"\" aborted because pad "+padId+" doesn't exist.");
+ }
+ return;
+ }
+ else {
+ return padFunc(pad);
+ }
+ });
+}
+
+function _handlePadUserInfo(pad, userInfo) {
+ var author = userInfo.userId;
+ var colorId = Number(userInfo.colorId);
+ var name = userInfo.name;
+
+ if (! author) return;
+
+ // update map from author to that author's last known color and name
+ var data = {colorId: colorId};
+ if (name) data.name = name;
+ pad.setAuthorData(author, data);
+ padusers.notifyUserData(data);
+}
+
+function _sendUserInfoMessage(connectionId, type, userInfo) {
+ if (translateSpecialKey(userInfo.specialKey) != 'invisible') {
+ sendMessage(connectionId, {type: type, userInfo: userInfo });
+ }
+}
+
+
+function getRoomCallbacks(roomName) {
+ var callbacks = {};
+ callbacks.introduceUsers =
+ function (joiningConnection, existingConnection) {
+ // notify users of each other
+ _sendUserInfoMessage(existingConnection.connectionId,
+ "USER_NEWINFO",
+ joiningConnection.data.userInfo);
+ _sendUserInfoMessage(joiningConnection.connectionId,
+ "USER_NEWINFO",
+ existingConnection.data.userInfo);
+ };
+ callbacks.extroduceUsers =
+ function (leavingConnection, existingConnection) {
+ _sendUserInfoMessage(existingConnection.connectionId, "USER_LEAVE",
+ leavingConnection.data.userInfo);
+ };
+ callbacks.onAddConnection =
+ function (data) {
+ model.accessPadGlobal(_roomToPadId(roomName), function(pad) {
+ _handlePadUserInfo(pad, data.userInfo);
+ padevents.onUserJoin(pad, data.userInfo);
+ readonly_server.updateUserInfo(pad, data.userInfo);
+ });
+ };
+ callbacks.onRemoveConnection =
+ function (data) {
+ model.accessPadGlobal(_roomToPadId(roomName), function(pad) {
+ padevents.onUserLeave(pad, data.userInfo);
+ });
+ };
+ callbacks.handleConnect =
+ function (data) {
+ if (roomName.indexOf("padpage/") != 0) {
+ return null;
+ }
+ if (! (data.userInfo && data.userInfo.userId &&
+ _verifyUserId(data.userInfo.userId))) {
+ return null;
+ }
+ return data.userInfo;
+ };
+ callbacks.clientReady =
+ function(newConnection, data) {
+ var padId = _roomToPadId(newConnection.roomName);
+
+ if (data.stats) {
+ log.custom("padclientstats", {padId:padId, stats:data.stats});
+ }
+
+ var lastRev = data.lastRev;
+ var isReconnectOf = data.isReconnectOf;
+ var isCommitPending = !! data.isCommitPending;
+ var connectionId = newConnection.connectionId;
+
+ newConnection.data.lastRev = lastRev;
+ updateRoomConnectionData(connectionId, newConnection.data);
+
+ if (padutils.isProPadId(padId)) {
+ pro_padmeta.accessProPad(padId, function(propad) {
+ // tell client about pad title
+ sendMessage(connectionId, {type: "CLIENT_MESSAGE", payload: {
+ type: "padtitle", title: propad.getDisplayTitle() } });
+ sendMessage(connectionId, {type: "CLIENT_MESSAGE", payload: {
+ type: "padpassword", password: propad.getPassword() } });
+ });
+ }
+
+ _accessExistingPad(padId, "CLIENT_READY", function(pad) {
+ sendMessage(connectionId, {type: "CLIENT_MESSAGE", payload: {
+ type: "padoptions", options: pad.getPadOptionsObj() } });
+
+ updateClient(pad, connectionId);
+
+ });
+
+ if (isCommitPending) {
+ // tell client that if it hasn't received an ACCEPT_COMMIT by now, it isn't coming.
+ sendMessage(connectionId, {type:"NO_COMMIT_PENDING"});
+ }
+ };
+ callbacks.handleMessage = function(connection, msg) {
+ _handleCometMessage(connection, msg);
+ };
+ return callbacks;
+}
+
+var _specialKeys = [['x375b', 'invisible']];
+
+function translateSpecialKey(specialKey) {
+ // code -> name
+ for(var i=0;i<_specialKeys.length;i++) {
+ if (_specialKeys[i][0] == specialKey) {
+ return _specialKeys[i][1];
+ }
+ }
+ return null;
+}
+
+function getSpecialKey(name) {
+ // name -> code
+ for(var i=0;i<_specialKeys.length;i++) {
+ if (_specialKeys[i][1] == name) {
+ return _specialKeys[i][0];
+ }
+ }
+ return null;
+}
+
+function _updateDocumentConnectionUserInfo(pad, socketId, userInfo) {
+ var connectionId = getSocketConnectionId(socketId);
+ if (connectionId) {
+ var updatingConnection = getConnection(connectionId);
+ updatingConnection.data.userInfo = userInfo;
+ updateRoomConnectionData(connectionId, updatingConnection.data);
+ _getPadConnections(pad).forEach(function(connection) {
+ if (connection.socketId != updatingConnection.socketId) {
+ _sendUserInfoMessage(connection.connectionId,
+ "USER_NEWINFO", userInfo);
+ }
+ });
+
+ _handlePadUserInfo(pad, userInfo);
+ padevents.onUserInfoChange(pad, userInfo);
+ readonly_server.updateUserInfo(pad, userInfo);
+ }
+}
+
+function _handleCometMessage(connection, msg) {
+
+ var socketUserId = connection.data.userInfo.userId;
+ if (! (socketUserId && _verifyUserId(socketUserId))) {
+ // user has signed out or cleared cookies, no longer auth'ed
+ bootConnection(connection.connectionId, "unauth");
+ }
+
+ if (msg.type == "USER_CHANGES") {
+ try {
+ _accessConnectionPad(connection, "USER_CHANGES", function(pad) {
+ var baseRev = msg.baseRev;
+ var wireApool = (new AttribPool()).fromJsonable(msg.apool);
+ var changeset = msg.changeset;
+ if (changeset) {
+ _checkChangesetAndPool(changeset, wireApool);
+ changeset = pad.adoptChangesetAttribs(changeset, wireApool);
+ applyUserChanges(pad, baseRev, changeset, connection.socketId);
+ }
+ });
+ }
+ catch (e if e.easysync) {
+ _doWarn("Changeset error handling USER_CHANGES: "+e);
+ }
+ }
+ else if (msg.type == "USERINFO_UPDATE") {
+ _accessConnectionPad(connection, "USERINFO_UPDATE", function(pad) {
+ var userInfo = msg.userInfo;
+ // security check
+ if (userInfo.userId == connection.data.userInfo.userId) {
+ _updateDocumentConnectionUserInfo(pad,
+ connection.socketId, userInfo);
+ }
+ else {
+ // drop on the floor
+ }
+ });
+ }
+ else if (msg.type == "CLIENT_MESSAGE") {
+ _accessConnectionPad(connection, "CLIENT_MESSAGE", function(pad) {
+ var payload = msg.payload;
+ if (payload.authId &&
+ payload.authId != connection.data.userInfo.userId) {
+ // authId, if present, must actually be the sender's userId;
+ // here it wasn't
+ }
+ else {
+ getRoomConnections(connection.roomName).forEach(
+ function(conn) {
+ if (conn.socketId != connection.socketId) {
+ sendMessage(conn.connectionId,
+ {type: "CLIENT_MESSAGE", payload: payload});
+ }
+ });
+ padevents.onClientMessage(pad, connection.data.userInfo,
+ payload);
+ }
+ });
+ }
+}
+
+function _correctMarkersInPad(atext, apool) {
+ var text = atext.text;
+
+ // collect char positions of line markers (e.g. bullets) in new atext
+ // that aren't at the start of a line
+ var badMarkers = [];
+ var iter = Changeset.opIterator(atext.attribs);
+ var offset = 0;
+ while (iter.hasNext()) {
+ var op = iter.next();
+ var listValue = Changeset.opAttributeValue(op, 'list', apool);
+ if (listValue) {
+ for(var i=0;i<op.chars;i++) {
+ if (offset > 0 && text.charAt(offset-1) != '\n') {
+ badMarkers.push(offset);
+ }
+ offset++;
+ }
+ }
+ else {
+ offset += op.chars;
+ }
+ }
+
+ if (badMarkers.length == 0) {
+ return null;
+ }
+
+ // create changeset that removes these bad markers
+ offset = 0;
+ var builder = Changeset.builder(text.length);
+ badMarkers.forEach(function(pos) {
+ builder.keepText(text.substring(offset, pos));
+ builder.remove(1);
+ offset = pos+1;
+ });
+ return builder.toString();
+}
diff --git a/etherpad/src/etherpad/collab/collabroom_server.js b/etherpad/src/etherpad/collab/collabroom_server.js
new file mode 100644
index 0000000..ab1f844
--- /dev/null
+++ b/etherpad/src/etherpad/collab/collabroom_server.js
@@ -0,0 +1,359 @@
+/**
+ * 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.
+ */
+
+import("execution");
+import("comet");
+import("fastJSON");
+import("cache_utils.syncedWithCache");
+import("etherpad.collab.collab_server");
+import("etherpad.collab.readonly_server");
+import("etherpad.log");
+jimport("java.util.concurrent.ConcurrentSkipListMap");
+jimport("java.util.concurrent.CopyOnWriteArraySet");
+
+function onStartup() {
+ execution.initTaskThreadPool("collabroom_async", 1);
+}
+
+function _doWarn(str) {
+ log.warn(appjet.executionId+": "+str);
+}
+
+// deep-copies (recursively clones) an object (or value)
+function _deepCopy(obj) {
+ if ((typeof obj) != 'object' || !obj) {
+ return obj;
+ }
+ var o = {};
+ for(var k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ var v = obj[k];
+ if ((typeof v) == 'object' && v) {
+ o[k] = _deepCopy(v);
+ }
+ else {
+ o[k] = v;
+ }
+ }
+ }
+ return o;
+}
+
+// calls func inside a global lock on the cache
+function _withCache(func) {
+ return syncedWithCache("collabroom_server", function(cache) {
+ if (! cache.rooms) {
+ // roomName -> { connections: CopyOnWriteArraySet<connectionId>,
+ // type: <immutable type string> }
+ cache.rooms = new ConcurrentSkipListMap();
+ }
+ if (! cache.allConnections) {
+ // connectionId -> connection object
+ cache.allConnections = new ConcurrentSkipListMap();
+ }
+ return func(cache);
+ });
+}
+
+// accesses cache without lock
+function _getCache() {
+ return _withCache(function(cache) { return cache; });
+}
+
+// if roomType is null, will only update an existing connection
+// (otherwise will insert or update as appropriate)
+function _putConnection(connection, roomType) {
+ var roomName = connection.roomName;
+ var connectionId = connection.connectionId;
+ var socketId = connection.socketId;
+ var data = connection.data;
+
+ _withCache(function(cache) {
+ var rooms = cache.rooms;
+ if (! rooms.containsKey(roomName)) {
+ // connection refers to room that doesn't exist / is empty
+ if (roomType) {
+ rooms.put(roomName, {connections: new CopyOnWriteArraySet(),
+ type: roomType});
+ }
+ else {
+ return;
+ }
+ }
+ if (roomType) {
+ rooms.get(roomName).connections.add(connectionId);
+ cache.allConnections.put(connectionId, connection);
+ }
+ else {
+ cache.allConnections.replace(connectionId, connection);
+ }
+ });
+}
+
+function _removeConnection(connection) {
+ _withCache(function(cache) {
+ var rooms = cache.rooms;
+ var thisRoom = connection.roomName;
+ var thisConnectionId = connection.connectionId;
+ if (rooms.containsKey(thisRoom)) {
+ var roomConnections = rooms.get(thisRoom).connections;
+ roomConnections.remove(thisConnectionId);
+ if (roomConnections.isEmpty()) {
+ rooms.remove(thisRoom);
+ }
+ }
+ cache.allConnections.remove(thisConnectionId);
+ });
+}
+
+function _getConnection(connectionId) {
+ // return a copy of the connection object
+ return _deepCopy(_getCache().allConnections.get(connectionId) || null);
+}
+
+function _getConnections(roomName) {
+ var array = [];
+
+ var roomObj = _getCache().rooms.get(roomName);
+ if (roomObj) {
+ var roomConnections = roomObj.connections;
+ var iter = roomConnections.iterator();
+ while (iter.hasNext()) {
+ var cid = iter.next();
+ var conn = _getConnection(cid);
+ if (conn) {
+ array.push(conn);
+ }
+ }
+ }
+ return array;
+}
+
+function sendMessage(connectionId, msg) {
+ var connection = _getConnection(connectionId);
+ if (connection) {
+ _sendMessageToSocket(connection.socketId, msg);
+ if (! comet.isConnected(connection.socketId)) {
+ // defunct socket, disconnect (later)
+ execution.scheduleTask("collabroom_async",
+ "collabRoomDisconnectSocket",
+ 0, [connection.connectionId,
+ connection.socketId]);
+ }
+ }
+}
+
+function _sendMessageToSocket(socketId, msg) {
+ var msgString = fastJSON.stringify({type: "COLLABROOM", data: msg});
+ comet.sendMessage(socketId, msgString);
+}
+
+function disconnectDefunctSocket(connectionId, socketId) {
+ var connection = _getConnection(connectionId);
+ if (connection && connection.socketId == socketId) {
+ removeRoomConnection(connectionId);
+ }
+}
+
+function _bootSocket(socketId, reason) {
+ if (reason) {
+ _sendMessageToSocket(socketId,
+ {type: "DISCONNECT_REASON", reason: reason});
+ }
+ comet.disconnect(socketId);
+}
+
+function bootConnection(connectionId, reason) {
+ var connection = _getConnection(connectionId);
+ if (connection) {
+ _bootSocket(connection.socketId, reason);
+ removeRoomConnection(connectionId);
+ }
+}
+
+function getCallbacksForRoom(roomName, roomType) {
+ if (! roomType) {
+ var room = _getCache().rooms.get(roomName);
+ if (room) {
+ roomType = room.type;
+ }
+ }
+
+ var emptyCallbacks = {};
+ emptyCallbacks.introduceUsers =
+ function (joiningConnection, existingConnection) {};
+ emptyCallbacks.extroduceUsers =
+ function extroduceUsers(leavingConnection, existingConnection) {};
+ emptyCallbacks.onAddConnection = function (joiningData) {};
+ emptyCallbacks.onRemoveConnection = function (leavingData) {};
+ emptyCallbacks.handleConnect =
+ function(data) { return /*userInfo or */null; };
+ emptyCallbacks.clientReady = function(newConnection, data) {};
+ emptyCallbacks.handleMessage = function(connection, msg) {};
+
+ if (roomType == collab_server.PADPAGE_ROOMTYPE) {
+ return collab_server.getRoomCallbacks(roomName, emptyCallbacks);
+ }
+ else if (roomType == readonly_server.PADVIEW_ROOMTYPE) {
+ return readonly_server.getRoomCallbacks(roomName, emptyCallbacks);
+ }
+ else {
+ //java.lang.System.out.println("UNKNOWN ROOMTYPE: "+roomType);
+ return emptyCallbacks;
+ }
+}
+
+// roomName must be globally unique, just within roomType;
+// data must have a userInfo.userId
+function addRoomConnection(roomName, roomType,
+ connectionId, socketId, data) {
+ var callbacks = getCallbacksForRoom(roomName, roomType);
+
+ comet.setAttribute(socketId, "connectionId", connectionId);
+
+ bootConnection(connectionId, "userdup");
+ var joiningConnection = {roomName:roomName,
+ connectionId:connectionId, socketId:socketId,
+ data:data};
+ _putConnection(joiningConnection, roomType);
+ var connections = _getConnections(roomName);
+ var joiningUser = data.userInfo.userId;
+
+ connections.forEach(function(connection) {
+ if (connection.socketId != socketId) {
+ var user = connection.data.userInfo.userId;
+ if (user == joiningUser) {
+ bootConnection(connection.connectionId, "userdup");
+ }
+ else {
+ callbacks.introduceUsers(joiningConnection, connection);
+ }
+ }
+ });
+
+ callbacks.onAddConnection(data);
+
+ return joiningConnection;
+}
+
+function removeRoomConnection(connectionId) {
+ var leavingConnection = _getConnection(connectionId);
+ if (leavingConnection) {
+ var roomName = leavingConnection.roomName;
+ var callbacks = getCallbacksForRoom(roomName);
+
+ _removeConnection(leavingConnection);
+
+ _getConnections(roomName).forEach(function (connection) {
+ callbacks.extroduceUsers(leavingConnection, connection);
+ });
+
+ callbacks.onRemoveConnection(leavingConnection.data);
+ }
+}
+
+function getConnection(connectionId) {
+ return _getConnection(connectionId);
+}
+
+function updateRoomConnectionData(connectionId, data) {
+ var connection = _getConnection(connectionId);
+ if (connection) {
+ connection.data = data;
+ _putConnection(connection);
+ }
+}
+
+function getRoomConnections(roomName) {
+ return _getConnections(roomName);
+}
+
+function getAllRoomsOfType(roomType) {
+ var rooms = _getCache().rooms;
+ var roomsIter = rooms.entrySet().iterator();
+ var array = [];
+ while (roomsIter.hasNext()) {
+ var entry = roomsIter.next();
+ var roomName = entry.getKey();
+ var roomStruct = entry.getValue();
+ if (roomStruct.type == roomType) {
+ array.push(roomName);
+ }
+ }
+ return array;
+}
+
+function getSocketConnectionId(socketId) {
+ var result = comet.getAttribute(socketId, "connectionId");
+ return result && String(result);
+}
+
+function handleComet(cometOp, cometId, msg) {
+ var cometEvent = cometOp;
+
+ function requireTruthy(x, id) {
+ if (!x) {
+ _doWarn("Collab operation rejected due to missing value, case "+id);
+ if (messageSocketId) {
+ comet.disconnect(messageSocketId);
+ }
+ response.stop();
+ }
+ return x;
+ }
+
+ if (cometEvent != "disconnect" && cometEvent != "message") {
+ response.stop();
+ }
+
+ var messageSocketId = requireTruthy(cometId, 2);
+ var messageConnectionId = getSocketConnectionId(messageSocketId);
+
+ if (cometEvent == "disconnect") {
+ if (messageConnectionId) {
+ removeRoomConnection(messageConnectionId);
+ }
+ }
+ else if (cometEvent == "message") {
+ if (msg.type == "CLIENT_READY") {
+ var roomType = requireTruthy(msg.roomType, 4);
+ var roomName = requireTruthy(msg.roomName, 11);
+
+ var socketId = messageSocketId;
+ var connectionId = messageSocketId;
+ var clientReadyData = requireTruthy(msg.data, 12);
+
+ var callbacks = getCallbacksForRoom(roomName, roomType);
+ var userInfo =
+ requireTruthy(callbacks.handleConnect(clientReadyData), 13);
+
+ var newConnection = addRoomConnection(roomName, roomType,
+ connectionId, socketId,
+ {userInfo: userInfo});
+
+ callbacks.clientReady(newConnection, clientReadyData);
+ }
+ else {
+ if (messageConnectionId) {
+ var connection = getConnection(messageConnectionId);
+ if (connection) {
+ var callbacks = getCallbacksForRoom(connection.roomName);
+ callbacks.handleMessage(connection, msg);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/collab/genimg.js b/etherpad/src/etherpad/collab/genimg.js
new file mode 100644
index 0000000..04d1b3b
--- /dev/null
+++ b/etherpad/src/etherpad/collab/genimg.js
@@ -0,0 +1,55 @@
+/**
+ * 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.
+ */
+
+import("sync");
+import("image");
+import("blob");
+
+//jimport("java.lang.System.out.println");
+
+function _cache() {
+ sync.callsyncIfTrue(appjet.cache,
+ function() { return ! appjet.cache["etherpad-genimg"]; },
+ function() { appjet.cache["etherpad-genimg"] = { paths: {}}; });
+ return appjet.cache["etherpad-genimg"];
+}
+
+function renderPath(path) {
+ if (_cache().paths[path]) {
+ //println("CACHE HIT");
+ }
+ else {
+ //println("CACHE MISS");
+ var regexResult = null;
+ var img = null;
+ if ((regexResult =
+ /solid\/([0-9]+)x([0-9]+)\/([0-9a-fA-F]{6})\.gif/.exec(path))) {
+ var width = Number(regexResult[1]);
+ var height = Number(regexResult[2]);
+ var color = regexResult[3];
+ img = image.solidColorImageBlob(width, height, color);
+ }
+ else {
+ // our "broken image" image, red and partly transparent
+ img = image.pixelsToImageBlob(2, 2, [0x00000000, 0xffff0000,
+ 0xffff0000, 0x00000000], true, "gif");
+ }
+ _cache().paths[path] = img;
+ }
+
+ blob.serveBlob(_cache().paths[path]);
+ return true;
+}
diff --git a/etherpad/src/etherpad/collab/json_sans_eval.js b/etherpad/src/etherpad/collab/json_sans_eval.js
new file mode 100644
index 0000000..6cbd497
--- /dev/null
+++ b/etherpad/src/etherpad/collab/json_sans_eval.js
@@ -0,0 +1,178 @@
+// Copyright (C) 2008 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.
+
+/**
+ * Parses a string of well-formed JSON text.
+ *
+ * If the input is not well-formed, then behavior is undefined, but it is
+ * deterministic and is guaranteed not to modify any object other than its
+ * return value.
+ *
+ * This does not use `eval` so is less likely to have obscure security bugs than
+ * json2.js.
+ * It is optimized for speed, so is much faster than json_parse.js.
+ *
+ * This library should be used whenever security is a concern (when JSON may
+ * come from an untrusted source), speed is a concern, and erroring on malformed
+ * JSON is *not* a concern.
+ *
+ * Pros Cons
+ * +-----------------------+-----------------------+
+ * json_sans_eval.js | Fast, secure | Not validating |
+ * +-----------------------+-----------------------+
+ * json_parse.js | Validating, secure | Slow |
+ * +-----------------------+-----------------------+
+ * json2.js | Fast, some validation | Potentially insecure |
+ * +-----------------------+-----------------------+
+ *
+ * json2.js is very fast, but potentially insecure since it calls `eval` to
+ * parse JSON data, so an attacker might be able to supply strange JS that
+ * looks like JSON, but that executes arbitrary javascript.
+ * If you do have to use json2.js with untrusted data, make sure you keep
+ * your version of json2.js up to date so that you get patches as they're
+ * released.
+ *
+ * @param {string} json per RFC 4627
+ * @return {Object|Array}
+ * @author Mike Samuel <mikesamuel@gmail.com>
+ */
+var jsonParse = (function () {
+ var number
+ = '(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)';
+ var oneChar = '(?:[^\\0-\\x08\\x0a-\\x1f\"\\\\]'
+ + '|\\\\(?:[\"/\\\\bfnrt]|u[0-9A-Fa-f]{4}|x7c))';
+ var string = '(?:\"' + oneChar + '*\")';
+
+ // Will match a value in a well-formed JSON file.
+ // If the input is not well-formed, may match strangely, but not in an unsafe
+ // way.
+ // Since this only matches value tokens, it does not match whitespace, colons,
+ // or commas.
+ var jsonToken = new RegExp(
+ '(?:false|true|null|[\\{\\}\\[\\]]'
+ + '|' + number
+ + '|' + string
+ + ')', 'g');
+
+ // Matches escape sequences in a string literal
+ var escapeSequence = new RegExp('\\\\(?:([^ux]|x7c)|u(.{4}))', 'g');
+
+ // Decodes escape sequences in object literals
+ var escapes = {
+ '"': '"',
+ '/': '/',
+ '\\': '\\',
+ 'b': '\b',
+ 'f': '\f',
+ 'n': '\n',
+ 'r': '\r',
+ 't': '\t',
+ 'x7c': '|'
+ };
+ function unescapeOne(_, ch, hex) {
+ return ch ? escapes[ch] : String.fromCharCode(parseInt(hex, 16));
+ }
+
+ // A non-falsy value that coerces to the empty string when used as a key.
+ var EMPTY_STRING = new String('');
+ var SLASH = '\\';
+
+ // Constructor to use based on an open token.
+ var firstTokenCtors = { '{': Object, '[': Array };
+
+ return function (json) {
+ // Split into tokens
+ var toks = json.match(jsonToken);
+ // Construct the object to return
+ var result;
+ var tok = toks[0];
+ if ('{' === tok) {
+ result = {};
+ } else if ('[' === tok) {
+ result = [];
+ } else {
+ throw new Error(tok);
+ }
+
+ // If undefined, the key in an object key/value record to use for the next
+ // value parsed.
+ var key;
+ // Loop over remaining tokens maintaining a stack of uncompleted objects and
+ // arrays.
+ var stack = [result];
+ for (var i = 1, n = toks.length; i < n; ++i) {
+ tok = toks[i];
+
+ var cont;
+ switch (tok.charCodeAt(0)) {
+ default: // sign or digit
+ cont = stack[0];
+ cont[key || cont.length] = +(tok);
+ key = void 0;
+ break;
+ case 0x22: // '"'
+ tok = tok.substring(1, tok.length - 1);
+ if (tok.indexOf(SLASH) !== -1) {
+ tok = tok.replace(escapeSequence, unescapeOne);
+ }
+ cont = stack[0];
+ if (!key) {
+ if (cont instanceof Array) {
+ key = cont.length;
+ } else {
+ key = tok || EMPTY_STRING; // Use as key for next value seen.
+ break;
+ }
+ }
+ cont[key] = tok;
+ key = void 0;
+ break;
+ case 0x5b: // '['
+ cont = stack[0];
+ stack.unshift(cont[key || cont.length] = []);
+ key = void 0;
+ break;
+ case 0x5d: // ']'
+ stack.shift();
+ break;
+ case 0x66: // 'f'
+ cont = stack[0];
+ cont[key || cont.length] = false;
+ key = void 0;
+ break;
+ case 0x6e: // 'n'
+ cont = stack[0];
+ cont[key || cont.length] = null;
+ key = void 0;
+ break;
+ case 0x74: // 't'
+ cont = stack[0];
+ cont[key || cont.length] = true;
+ key = void 0;
+ break;
+ case 0x7b: // '{'
+ cont = stack[0];
+ stack.unshift(cont[key || cont.length] = {});
+ key = void 0;
+ break;
+ case 0x7d: // '}'
+ stack.shift();
+ break;
+ }
+ }
+ // Fail if we've got an uncompleted object.
+ if (stack.length) { throw new Error(); }
+ return result;
+ };
+})();
diff --git a/etherpad/src/etherpad/collab/readonly_server.js b/etherpad/src/etherpad/collab/readonly_server.js
new file mode 100644
index 0000000..e367f04
--- /dev/null
+++ b/etherpad/src/etherpad/collab/readonly_server.js
@@ -0,0 +1,174 @@
+/**
+ * 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.
+ */
+
+import("comet");
+import("ejs");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("etherpad.log");
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padevents");
+import("etherpad.pro.pro_padmeta");
+import("fastJSON");
+import("fileutils.readFile");
+import("jsutils.eachProperty");
+import("etherpad.collab.server_utils.*");
+import("etherpad.collab.collabroom_server");
+
+jimport("java.util.concurrent.ConcurrentHashMap");
+
+jimport("java.lang.System.out.println");
+
+var PADVIEW_ROOMTYPE = 'padview';
+
+var _serverDebug = println;//function(x) {};
+
+// "view id" is either a padId or an ro.id
+function _viewIdToRoom(padId) {
+ return "padview/"+padId;
+}
+
+function _roomToViewId(roomName) {
+ return roomName.substring(roomName.indexOf("/")+1);
+}
+
+function getRoomCallbacks(roomName, emptyCallbacks) {
+ var callbacks = emptyCallbacks;
+
+ var viewId = _roomToViewId(roomName);
+
+ callbacks.handleConnect = function(data) {
+ if (data.userInfo && data.userInfo.userId) {
+ return data.userInfo;
+ }
+ return null;
+ };
+ callbacks.clientReady =
+ function(newConnection, data) {
+ newConnection.data.lastRev = data.lastRev;
+ collabroom_server.updateRoomConnectionData(newConnection.connectionId,
+ newConnection.data);
+ };
+
+ return callbacks;
+}
+
+function updatePadClients(pad) {
+ var padId = pad.getId();
+ var roId = padIdToReadonly(padId);
+
+ function update(connection) {
+ updateClient(pad, connection.connectionId);
+ }
+
+ collabroom_server.getRoomConnections(_viewIdToRoom(padId)).forEach(update);
+ collabroom_server.getRoomConnections(_viewIdToRoom(roId)).forEach(update);
+}
+
+// Get arrays of text lines and attribute lines for a revision
+// of a pad.
+function _getPadLines(pad, revNum) {
+ var atext;
+ if (revNum >= 0) {
+ atext = pad.getInternalRevisionAText(revNum);
+ } else {
+ atext = Changeset.makeAText("\n");
+ }
+
+ var result = {};
+ result.textlines = Changeset.splitTextLines(atext.text);
+ result.alines = Changeset.splitAttributionLines(atext.attribs,
+ atext.text);
+ return result;
+}
+
+function updateClient(pad, connectionId) {
+ var conn = collabroom_server.getConnection(connectionId);
+ if (! conn) {
+ return;
+ }
+ var lastRev = conn.data.lastRev;
+ while (lastRev < pad.getHeadRevisionNumber()) {
+ var r = ++lastRev;
+ var author = pad.getRevisionAuthor(r);
+ var lines = _getPadLines(pad, r-1);
+ var wirePool = new AttribPool();
+ var forwards = pad.getRevisionChangeset(r);
+ var backwards = Changeset.inverse(forwards, lines.textlines,
+ lines.alines, pad.pool());
+ var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.pool(),
+ wirePool);
+ var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.pool(),
+ wirePool);
+
+ function revTime(r) {
+ var date = pad.getRevisionDate(r);
+ var s = Math.floor((+date)/1000);
+ //java.lang.System.out.println("time "+r+": "+s);
+ return s;
+ }
+
+ var msg = {type:"NEW_CHANGES", newRev:r,
+ changeset: forwards2,
+ changesetBack: backwards2,
+ apool: wirePool.toJsonable(),
+ author: author,
+ timeDelta: revTime(r) - revTime(r-1) };
+ collabroom_server.sendMessage(connectionId, msg);
+ }
+ conn.data.lastRev = pad.getHeadRevisionNumber();
+ collabroom_server.updateRoomConnectionData(connectionId, conn.data);
+}
+
+function sendMessageToPadConnections(pad, msg) {
+ var padId = pad.getId();
+ var roId = padIdToReadonly(padId);
+
+ function update(connection) {
+ collabroom_server.sendMessage(connection.connectionId, msg);
+ }
+
+ collabroom_server.getRoomConnections(_viewIdToRoom(padId)).forEach(update);
+ collabroom_server.getRoomConnections(_viewIdToRoom(roId)).forEach(update);
+}
+
+function updateUserInfo(pad, userInfo) {
+ var msg = { type:"NEW_AUTHORDATA",
+ author: userInfo.userId,
+ data: {} };
+ var hasData = false;
+ if ((typeof (userInfo.colorId)) == "number") {
+ msg.data.colorId = userInfo.colorId;
+ hasData = true;
+ }
+ if (userInfo.name) {
+ msg.data.name = userInfo.name;
+ hasData = true;
+ }
+ if (hasData) {
+ sendMessageToPadConnections(pad, msg);
+ }
+}
+
+function broadcastNewRevision(pad, revObj) {
+ var msg = { type:"NEW_SAVEDREV",
+ savedRev: revObj };
+
+ delete revObj.ip; // we try not to share info like IP addresses on slider
+
+ sendMessageToPadConnections(pad, msg);
+}
diff --git a/etherpad/src/etherpad/collab/server_utils.js b/etherpad/src/etherpad/collab/server_utils.js
new file mode 100644
index 0000000..ece3aea
--- /dev/null
+++ b/etherpad/src/etherpad/collab/server_utils.js
@@ -0,0 +1,204 @@
+/**
+ * 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.
+ */
+
+import("comet");
+import("ejs");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("etherpad.log");
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padevents");
+import("etherpad.pro.pro_padmeta");
+import("fastJSON");
+import("fileutils.readFile");
+import("jsutils.eachProperty");
+
+jimport("java.util.Random");
+jimport("java.lang.System");
+
+import("etherpad.collab.collab_server");
+// importClass(java.util.Random);
+// importClass(java.lang.System);
+
+var _serverDebug = function() {};
+var _dmesg = function() { System.out.println(arguments[0]+""); };
+
+/// Begin readonly/padId conversion code
+/// TODO: refactor into new file?
+var _baseRandomNumber = 0x123123; // keep this number seekrit
+
+function _map(array, func) {
+ for(var i=0; i<array.length; i++) {
+ array[i] = func(array[i]);
+ }
+ return array;
+}
+
+function parseUrlId(readOnlyIdOrLocalPadId) {
+ var localPadId;
+ var viewId;
+ var isReadOnly;
+ var roPadId;
+ var globalPadId;
+ if(isReadOnlyId(readOnlyIdOrLocalPadId)) {
+ isReadOnly = true;
+ globalPadId = readonlyToPadId(readOnlyIdOrLocalPadId);
+ localPadId = padutils.globalToLocalId(globalPadId);
+ var globalPadIdCheck = padutils.getGlobalPadId(localPadId);
+ if (globalPadId != globalPadIdCheck) {
+ // domain doesn't match
+ response.forbid();
+ }
+ roPadId = readOnlyIdOrLocalPadId;
+ viewId = roPadId;
+ }
+ else {
+ isReadOnly = false;
+ localPadId = readOnlyIdOrLocalPadId;
+ globalPadId = padutils.getGlobalPadId(localPadId);
+ viewId = globalPadId;
+ roPadId = padIdToReadonly(globalPadId);
+ }
+
+ return {localPadId:localPadId, viewId:viewId, isReadOnly:isReadOnly,
+ roPadId:roPadId, globalPadId:globalPadId};
+}
+
+function isReadOnlyId(str) {
+ return str.indexOf("ro.") == 0;
+}
+
+/*
+ for now, we just make it 'hard to guess'
+ TODO: make it impossible to find read/write page through hash
+*/
+function readonlyToPadId (readOnlyHash) {
+
+ // readOnly hashes must start with 'ro-'
+ if(!isReadOnlyId(readOnlyHash)) return null;
+ else {
+ readOnlyHash = readOnlyHash.substring(3, readOnlyHash.length);
+ }
+
+ // convert string to series of numbers between 1 and 64
+ var result = _strToArray(readOnlyHash);
+
+ var sum = result.pop();
+ // using a secret seed to util.random, transform each number using + and %
+ var seed = _baseRandomNumber + sum;
+ var rand = new Random(seed);
+
+ _map(result, function(elem) {
+ return ((64 + elem - rand.nextInt(64)) % 64);
+ });
+
+ // convert array of numbers back to a string
+ return _arrayToStr(result);
+}
+
+/*
+ Temporary code. see comment at readonlyToPadId.
+*/
+function padIdToReadonly (padid) {
+ var result = _strToArray(padid);
+ var sum = 0;
+
+ if(padid.length > 1) {
+ for(var i=0; i<result.length; i++) {
+ sum = (sum + result[i] + 1) % 64;
+ }
+ } else {
+ sum = 64;
+ }
+
+ var seed = _baseRandomNumber + sum;
+ var rand = new Random(seed);
+
+ _map(result, function(elem) {
+ var randnum = rand.nextInt(64);
+ return ((elem + randnum) % 64);
+ });
+
+ result.push(sum);
+ return "ro." + _arrayToStr(result);
+}
+
+// little reversable string encoding function
+// 0-9 are the numbers 0-9
+// 10-35 are the uppercase letters A-Z
+// 36-61 are the lowercase letters a-z
+// 62 are all other characters
+function _strToArray(str) {
+ var result = new Array(str.length);
+ for(var i=0; i<str.length; i++) {
+ result[i] = str.charCodeAt(i);
+
+ if (_between(result[i], '0'.charCodeAt(0), '9'.charCodeAt(0))) {
+ result[i] -= '0'.charCodeAt(0);
+ }
+ else if(_between(result[i], 'A'.charCodeAt(0), 'Z'.charCodeAt(0))) {
+ result[i] -= 'A'.charCodeAt(0); // A becomes 0
+ result[i] += 10; // A becomes 10
+ }
+ else if(_between(result[i], 'a'.charCodeAt(0), 'z'.charCodeAt(0))) {
+ result[i] -= 'a'.charCodeAt(0); // a becomes 0
+ result[i] += 36; // a becomes 36
+ } else if(result[i] == '$'.charCodeAt(0)) {
+ result[i] = 62;
+ } else {
+ result[i] = 63; // if not alphanumeric or $, we default to 63
+ }
+ }
+ return result;
+}
+
+function _arrayToStr(array) {
+ var result = "";
+ for(var i=0; i<array.length; i++) {
+ if(_between(array[i], 0, 9)) {
+ result += String.fromCharCode(array[i] + '0'.charCodeAt(0));
+ }
+ else if(_between(array[i], 10, 35)) {
+ result += String.fromCharCode(array[i] - 10 + 'A'.charCodeAt(0));
+ }
+ else if(_between(array[i], 36, 61)) {
+ result += String.fromCharCode(array[i] - 36 + 'a'.charCodeAt(0));
+ }
+ else if(array[i] == 62) {
+ result += "$";
+ } else {
+ result += "-";
+ }
+ }
+ return result;
+}
+
+function _between(charcode, start, end) {
+ return charcode >= start && charcode <= end;
+}
+
+/* a short little testing function, converts back and forth */
+// function _testEncrypt(str) {
+// var encrypted = padIdToReadonly(str);
+// var decrypted = readonlyToPadId(encrypted);
+// _dmesg(str + " " + encrypted + " " + decrypted);
+// if(decrypted != str) {
+// _dmesg("ERROR: " + str + " and " + decrypted + " do not match");
+// }
+// }
+
+// _testEncrypt("testing$");
diff --git a/etherpad/src/etherpad/control/aboutcontrol.js b/etherpad/src/etherpad/control/aboutcontrol.js
new file mode 100644
index 0000000..9d77142
--- /dev/null
+++ b/etherpad/src/etherpad/control/aboutcontrol.js
@@ -0,0 +1,263 @@
+/**
+ * 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.
+ */
+
+import("email.sendEmail");
+import("funhtml.*", "stringutils.*");
+import("netutils");
+import("execution");
+
+import("etherpad.utils.*");
+import("etherpad.log");
+import("etherpad.globals.*");
+import("etherpad.quotas");
+import("etherpad.sessions.getSession");
+import("etherpad.store.eepnet_trial");
+import("etherpad.store.checkout");
+import("etherpad.store.eepnet_checkout");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+function render_product() {
+ if (request.params.from) { response.redirect(request.path); }
+ renderFramed("about/product_body.ejs");
+}
+
+function render_faq() {
+ renderFramed("about/faq_body.ejs", {
+ LI: LI,
+ H2: H2,
+ A: A,
+ html: html
+ });
+}
+
+function render_pne_faq() {
+ renderFramed("about/pne-faq.ejs");
+}
+
+function render_company() {
+ renderFramed("about/company_body.ejs");
+}
+
+function render_contact() {
+ renderFramed("about/contact_body.ejs");
+}
+
+function render_privacy() {
+ renderFramed("about/privacy_body.ejs");
+}
+
+function render_tos() {
+ renderFramed("about/tos_body.ejs");
+}
+
+function render_testimonials() {
+ renderFramed("about/testimonials.ejs");
+}
+
+function render_appjet() {
+ response.redirect("/ep/blog/posts/etherpad-and-appjet");
+// renderFramed("about/appjet_body.ejs");
+}
+
+function render_screencast() {
+ if (request.params.from) { response.redirect(request.path); }
+ var screencastUrl;
+// if (isProduction()) {
+ screencastUrl = encodeURIComponent("http://etherpad.s3.amazonaws.com/epscreencast800x600.flv");
+// } else {
+// screencastUrl = encodeURIComponent("/static/flv/epscreencast800x600.flv");
+// }
+ renderFramed("about/screencast_body.ejs", {screencastUrl: screencastUrl});
+}
+
+function render_forums() {
+ renderFramed("about/forums_body.ejs");
+}
+
+function render_blog() {
+ renderFramed("about/blog_body.ejs");
+}
+
+function render_really_real_time() {
+ renderFramed("about/simultaneously.ejs");
+}
+
+function render_simultaneously() {
+ renderFramed("about/simultaneously.ejs");
+}
+
+//----------------------------------------------------------------
+// pricing
+//----------------------------------------------------------------
+
+function render_pricing() {
+ renderFramed("about/pricing.ejs", {
+ trialDays: eepnet_trial.getTrialDays(),
+ costPerUser: checkout.dollars(eepnet_checkout.COST_PER_USER)
+ });
+}
+
+function render_pricing_free() {
+ renderFramed("about/pricing_free.ejs", {
+ maxUsersPerPad: quotas.getMaxSimultaneousPadEditors()
+ });
+}
+
+function render_pricing_eepnet() {
+ renderFramed("about/pricing_eepnet.ejs", {
+ trialDays: eepnet_trial.getTrialDays(),
+ costPerUser: checkout.dollars(eepnet_checkout.COST_PER_USER)
+ });
+}
+
+function render_pricing_pro() {
+ renderFramed("about/pricing_pro.ejs", {});
+}
+
+function render_eepnet_pricing_contact_post() {
+ response.setContentType("text/plain; charset=utf-8");
+ var data = {};
+ var fields = ['firstName', 'lastName', 'email', 'orgName',
+ 'jobTitle', 'phone', 'estUsers', 'industry'];
+
+ if (!getSession().pricingContactData) {
+ getSession().pricingContactData = {};
+ }
+
+ function err(m) {
+ response.write(m);
+ response.stop();
+ }
+
+ fields.forEach(function(f) {
+ getSession().pricingContactData[f] = request.params[f];
+ });
+
+ fields.forEach(function(f) {
+ data[f] = request.params[f];
+ if (!(data[f] && (data[f].length > 0))) {
+ err("All fields are required.");
+ }
+ });
+
+ if (!isValidEmail(data.email)) {
+ err("Error: Invalid Email");
+ }
+
+ // log this data to a file
+ fields.ip = request.clientAddr;
+ fields.sessionReferer = getSession().initialReferer;
+ log.custom("eepnet_pricing_inquiry", fields);
+
+ // submit web2lead
+ var ref = getSession().initialReferer;
+ var googleQuery = extractGoogleQuery(ref);
+ var wlparams = {
+ oid: "00D80000000b7ey",
+ first_name: data.firstName,
+ last_name: data.lastName,
+ email: data.email,
+ company: data.orgName,
+ title: data.jobTitle,
+ phone: data.phone,
+ '00N80000003FYtG': data.estUsers,
+ '00N80000003FYto': ref,
+ '00N80000003FYuI': googleQuery,
+ lead_source: 'EEPNET Pricing Inquiry',
+ industry: data.industry,
+ retURL: 'http://'+request.host+'/ep/store/salesforce-web2lead-ok'
+ };
+
+ var result = netutils.urlPost(
+ "http://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8",
+ wlparams, {});
+
+ // now send an email sales notification
+ var hostname = ipToHostname(request.clientAddr) || "unknown";
+ var subject = 'EEPNET Pricing Inquiry: '+data.email+' / '+hostname;
+ var body = [
+ "", "This is an automated email.", "",
+ data.firstName+" "+data.lastName+" ("+data.orgName+") has inquired about EEPNET pricing.",
+ "",
+ "This record has automatically been added to SalesForce. See the salesforce lead page for more details.",
+ "", "Session Referer: "+ref, ""
+ ].join("\n");
+ var toAddr = 'sales@pad.spline.inf.fu-berlin.de';
+ if (isTestEmail(data.email)) {
+ toAddr = 'blackhole@appjet.com';
+ }
+ sendEmail(toAddr, 'sales@pad.spline.inf.fu-berlin.de', subject, {}, body);
+
+ // all done!
+ response.write("OK");
+}
+
+function render_pricing_interest_signup() {
+ response.setContentType('text/plain; charset=utf-8');
+
+ var email = request.params.email;
+ var interestedNet = request.params.interested_net;
+ var interestedHosted = request.params.interested_hosted;
+
+ if (!isValidEmail(email)) {
+ response.write("Error: Invalid Email");
+ response.stop();
+ }
+
+ log.custom("pricing_interest",
+ {email: email,
+ net: interestedNet,
+ hosted: interestedHosted});
+
+ response.write('OK');
+}
+
+function render_pricing_eepnet_users() {
+ renderFramed('about/pricing_eepnet_users.ejs', {});
+}
+
+function render_pricing_eepnet_support() {
+ renderFramed('about/pricing_eepnet_support.ejs', {});
+}
+
+
+//------------------------------------------------------------
+// survey
+
+function render_survey() {
+ var id = request.params.id;
+ log.custom("pro-user-survey", { surveyProAccountId: (id || "unknown") });
+ response.redirect("http://www.surveymonkey.com/s.aspx?sm=yT3ALP0pb_2fP_2bHtcfzvpkXQ_3d_3d");
+}
+
+
+//------------------------------------------------------------
+
+import("etherpad.billing.billing");
+
+function render_testbillingnotify() {
+ var ret = billing.handlePaypalNotification();
+ if (ret.status == 'completion') {
+ // do something with purchase ret.purchaseInfo
+ } else if (ret.status != 'redundant') {
+ java.lang.System.out.println("Whoa error: "+ret.toSource());
+ }
+ response.write("ok");
+}
+
diff --git a/etherpad/src/etherpad/control/admin/pluginmanager.js b/etherpad/src/etherpad/control/admin/pluginmanager.js
new file mode 100644
index 0000000..c4bee5b
--- /dev/null
+++ b/etherpad/src/etherpad/control/admin/pluginmanager.js
@@ -0,0 +1,71 @@
+/**
+ * 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");
+import("etherpad.pad.padutils");
+
+
+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.loadPlugins(1);
+ 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(),
+ });
+
+
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth")
+
+ renderHtml("admin/pluginmanager.ejs",
+ {
+ prefs: prefs,
+ 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/etherpad/src/etherpad/control/admincontrol.js b/etherpad/src/etherpad/control/admincontrol.js
new file mode 100644
index 0000000..ec48824
--- /dev/null
+++ b/etherpad/src/etherpad/control/admincontrol.js
@@ -0,0 +1,1482 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("netutils");
+import("funhtml.*");
+import("stringutils.{html,sprintf,startsWith,md5}");
+import("jsutils.*");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("varz");
+import("comet");
+import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
+
+import("etherpad.billing.team_billing");
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.licensing");
+import("etherpad.sessions.getSession");
+import("etherpad.sessions");
+import("etherpad.statistics.statistics");
+import("etherpad.log");
+import("etherpad.admin.shell");
+import("etherpad.usage_stats.usage_stats");
+import("etherpad.control.blogcontrol");
+import("etherpad.control.pro_beta_control");
+import("etherpad.control.statscontrol");
+import("etherpad.statistics.exceptions");
+import("etherpad.store.checkout");
+
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.dbwriter");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.domains");
+import("etherpad.admin.plugins");
+import("etherpad.control.admin.pluginmanager");
+
+jimport("java.lang.System.out.println");
+
+jimport("net.appjet.oui.cometlatencies");
+jimport("net.appjet.oui.appstats");
+
+
+//----------------------------------------------------------------
+
+function _isAuthorizedAdmin() {
+ if (!isProduction()) {
+ return true;
+ }
+ return (getSession().adminAuth === true);
+}
+
+var _mainLinks = [
+ ['exceptions', 'Exceptions Monitor'],
+ ['usagestats/', 'Usage Stats'],
+ ['padinspector', 'Pad Inspector'],
+ ['dashboard', 'Dashboard'],
+ ['eepnet-licenses', 'EEPNET Licenses'],
+ ['config', 'appjet.config'],
+ ['shell', 'Shell'],
+ ['timings', 'timing data'],
+ ['broadcast-message', 'Pad Broadcast'],
+// ['analytics', 'Google Analytics'],
+ ['varz', 'varz'],
+ ['genlicense', 'Manually generate a license key'],
+ ['flows', 'Flows (warning: slow)'],
+ ['diagnostics', 'Pad Connection Diagnostics'],
+ ['cachebrowser', 'Cache Browser'],
+ ['pne-tracker', 'PNE Tracking Stats'],
+ ['reload-blog-db', 'Reload blog DB'],
+ ['pro-domain-accounts', 'Pro Domain Accounts'],
+ ['beta-valve', 'Beta Valve'],
+ ['reset-subscription', "Reset Subscription"],
+ ['pluginmanager/', "Plugin manager"]
+];
+
+function onRequest(name) {
+ if (name == "auth") {
+ return;
+ }
+ if (!_isAuthorizedAdmin()) {
+ getSession().cont = request.path;
+ response.redirect('/ep/admin/auth');
+ }
+
+ var disp = new Dispatcher();
+
+ disp.addLocations(plugins.callHook("handleAdminPath"));
+
+ disp.addLocations([
+ [PrefixMatcher('/ep/admin/pluginmanager/'), forward(pluginmanager)],
+ [PrefixMatcher('/ep/admin/usagestats/'), forward(statscontrol)]
+ ]);
+
+ return disp.dispatch();
+}
+
+function _commonHead() {
+ return HEAD(STYLE(
+ "html {font-family:Verdana,Helvetica,sans-serif;}",
+ "body {padding: 2em;}"
+ ));
+}
+
+//----------------------------------------------------------------
+
+function render_auth() {
+ var cont = getSession().cont;
+ if (getSession().message) {
+ response.write(DIV(P(B(getSession().message))));
+ delete getSession().message;
+ }
+ if (request.method == "GET") {
+ response.write(FORM({method: "POST", action: request.path},
+ P("Are you an admin?"),
+ LABEL("Password:"),
+ INPUT({type: "password", name: "password", value: ""}),
+ INPUT({type: "submit", value: "submit"})
+ ));
+ }
+ if (request.method == "POST") {
+ var pass = request.params.password;
+ if (pass === appjet.config['etherpad.adminPass']) {
+ getSession().adminAuth = true;
+ if (cont) {
+ response.redirect(cont);
+ } else {
+ response.redirect("/ep/admin/main");
+ }
+ } else {
+ getSession().message = "Bad Password.";
+ response.redirect(request.path);
+ }
+ }
+}
+
+function render_main() {
+ var div = DIV();
+
+ div.push(A({href: "/"}, html("&laquo;"), " home"));
+ div.push(H1("Admin"));
+
+ function addMenuItem(l) {
+ div.push(DIV(A({href: l[0]}, l[1])));
+ }
+
+ plugins.callHook("adminMenu").forEach(addMenuItem);
+ _mainLinks.forEach(addMenuItem);
+
+ if (sessions.isAnEtherpadAdmin()) {
+ div.push(P(A({href: "/ep/admin/setadminmode?v=false"},
+ "Exit Admin Mode")));
+ }
+ else {
+ div.push(P(A({href: "/ep/admin/setadminmode?v=true"},
+ "Enter Admin Mode")));
+ }
+ response.write(HTML(_commonHead(), BODY(div)));
+}
+
+//----------------------------------------------------------------
+
+function render_config() {
+
+ vars = [];
+ eachProperty(appjet.config, function(k,v) {
+ vars.push(k);
+ });
+
+ vars.sort();
+
+ response.setContentType('text/plain; charset=utf-8');
+ vars.forEach(function(v) {
+ response.write("appjet.config."+v+" = "+appjet.config[v]+"\n");
+ });
+}
+
+//----------------------------------------------------------------
+
+function render_test() {
+ response.setContentType("text/plain");
+ response.write(Packages.net.appjet.common.util.ExpiringMapping + "\n");
+ var m = new Packages.net.appjet.common.util.ExpiringMapping(10 * 1000);
+ response.write(m.toString() + "\n");
+ m.get("test");
+ return;
+ response.write(m.toString());
+}
+
+function render_dashboard() {
+ var body = BODY();
+ body.push(A({href: '/ep/admin/'}, html("&laquo; Admin")));
+ body.push(H1({style: "border-bottom: 1px solid black;"}, "Dashboard"));
+
+ /*
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "License"));
+ var license = licensing.getLicense();
+ body.push(P(TT(" Licensed To (name): "+license.personName)));
+ body.push(P(TT(" Licensed To (organization): "+license.organizationName)));
+ body.push(P(TT(" Software Edition: "+license.editionName)));
+ var quota = ((license.userQuota > 0) ? license.userQuota : 'unlimited');
+ body.push(P(TT(" User Quota: "+quota)));
+ var expires = (license.expiresDate ? (license.expiresDate.toString()) : 'never');
+ body.push(P(TT(" Expires: "+expires)));
+ */
+
+ /*
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Active User Quota"));
+
+ var activeUserCount = licensing.getActiveUserCount();
+ var activeUserQuota = licensing.getActiveUserQuota();
+ var activeUserWindowStart = licensing.getActiveUserWindowStart();
+
+ body.push(P(TT(" Since ", B(activeUserWindowStart.toString()), ", ",
+ "you have used ", B(activeUserCount), " of ", B(activeUserQuota),
+ " active users.")));
+*/
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Uptime"));
+ body.push(P({style: "margin-left: 25px;"}, "Server running for "+renderServerUptime()+"."))
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Response codes"));
+ body.push(renderResponseCodes());
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Comet Connections"));
+ body.push(renderPadConnections());
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Comet Stats"));
+ body.push(renderCometStats());
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Recurring revenue, monthly"));
+ body.push(renderRevenueStats());
+
+ response.write(HTML(_commonHead(), body));
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderPadConnections() {
+ var d = DIV();
+ var lastCount = cometlatencies.lastCount();
+
+ if (lastCount.isDefined()) {
+ var countMap = {};
+ Array.prototype.map.call(lastCount.get().elements().collect().toArray().unbox(
+ java.lang.Class.forName("java.lang.Object")),
+ function(x) {
+ countMap[x._1()] = x._2();
+ });
+ var totalConnected = 0;
+ var ul = UL();
+ eachProperty(countMap, function(k,v) {
+ ul.push(LI(k+": "+v));
+ if (/^\d+$/.test(v)) {
+ totalConnected += Number(v);
+ }
+ });
+ ul.push(LI(B("Total: ", totalConnected)));
+ d.push(ul);
+ } else {
+ d.push("Still collecting data... check back in a minute.");
+ }
+ return d;
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderCometStats() {
+ var d = DIV();
+ var lastStats = cometlatencies.lastStats();
+ var lastCount = cometlatencies.lastCount();
+
+
+ if (lastStats.isDefined()) {
+ d.push(P("Realtime transport latency percentiles (microseconds):"));
+ var ul = UL();
+ lastStats.map(scalaF1(function(s) {
+ ['50', '90', '95', '99', 'max'].forEach(function(id) {
+ var fn = id;
+ if (id != "max") {
+ fn = ("p"+fn);
+ id = id+"%";
+ }
+ ul.push(LI(id, ": <", s[fn](), html("&micro;"), "s"));
+ });
+ }));
+ d.push(ul);
+ } else {
+ d.push(P("Still collecting data... check back in a minutes."));
+ }
+
+ /* ["p50", "p90", "p95", "p99", "max"].forEach(function(id) {
+ ul.push(LI(B(
+
+ return DIV(P(sprintf("50%% %d\t90%% %d\t95%% %d\t99%% %d\tmax %d",
+ s.p50(), s.p90(), s.p95(), s.p99(), s.max())),
+ P(sprintf("%d total messages", s.count())));
+ }})).get();*/
+
+
+ return d;
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderResponseCodes() {
+ var statusCodeFrequencyNames = ["minute", "hour", "day", "week"];
+ var data = { };
+ var statusCodes = appstats.stati();
+ for (var i = 0; i < statusCodes.length; ++i) {
+ var name = statusCodeFrequencyNames[i];
+ var map = statusCodes[i];
+ map.foreach(scalaF1(function(pair) {
+ if (! (pair._1() in data)) data[pair._1()] = {};
+ var scmap = data[pair._1()];
+ scmap[name] = pair._2().count();
+ }));
+ };
+ var stats = TABLE({id: "responsecodes-table", style: "margin-left: 25px;",
+ border: 1, cellspacing: 0, cellpadding: 4},
+ TR.apply(TR, statusCodeFrequencyNames.map(function(name) {
+ return TH({colspan: 2}, "Last", html("&nbsp;"), name);
+ })));
+ var sortedStati = [];
+ eachProperty(data, function(k) {
+ sortedStati.push(k);
+ });
+ sortedStati.sort();
+ sortedStati.forEach(function(k, i) { // k is status code.
+ var row = TR();
+ statusCodeFrequencyNames.forEach(function(name) {
+ row.push(TD({style: 'width: 2em;'}, data[k][name] ? k+":" : ""));
+ row.push(TD(data[k][name] ? data[k][name] : ""));
+ });
+ stats.push(row);
+ });
+ return stats;
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderServerUptime() {
+ var labels = ["seconds", "minutes", "hours", "days"];
+ var ratios = [60, 60, 24];
+ var time = appjet.uptime / 1000;
+ var pos = 0;
+ while (pos < ratios.length && time / ratios[pos] > 1.1) {
+ time = time / ratios[pos];
+ pos++;
+ }
+ return sprintf("%.1f %s", time, labels[pos]);
+}
+
+function renderRevenueStats() {
+ var subs = team_billing.getAllSubscriptions();
+ var total = 0;
+ var totalUsers = 0;
+ subs.forEach(function(sub) {
+ var users = team_billing.getMaxUsers(sub.customer);
+ var cost = team_billing.calculateSubscriptionCost(users, sub.coupon);
+ if (cost > 0) {
+ totalUsers += users;
+ total += cost;
+ }
+ });
+ return "US $"+checkout.dollars(total)+", from "+subs.length+" domains and "+totalUsers+" users.";
+}
+
+//----------------------------------------------------------------
+// Broadcasting Messages
+//----------------------------------------------------------------
+
+function render_broadcast_message_get() {
+ var body = BODY(FORM({action: request.path, method: 'post'},
+ H3('Broadcast Message to All Active Pad Clients:'),
+ TEXTAREA({name: 'msgtext', style: 'width: 100%; height: 100px;'}),
+ H3('JavaScript code to be eval()ed on client (optional, be careful!): '),
+ TEXTAREA({name: 'jscode', style: 'width: 100%; height: 100px;'}),
+ INPUT({type: 'submit', value: 'Broadcast Now'})));
+ response.write(HTML(body));
+}
+
+function render_broadcast_message_post() {
+ var msgText = request.params.msgtext;
+ var jsCode = request.params.jscode;
+ if (!(msgText || jsCode)) {
+ response.write("No mesage text or jscode specified.");
+ response.stop();
+ return;
+ }
+ collab_server.broadcastServerMessage({
+ type: 'NOTICE',
+ text: msgText,
+ js: jsCode
+ });
+ response.write(HTML(BODY(P("OK"), P(A({href: request.path}, "back")))));
+}
+
+function render_shell() {
+ shell.handleRequest();
+}
+
+//----------------------------------------------------------------
+// pad inspector
+//----------------------------------------------------------------
+
+function _getPadUrl(globalPadId) {
+ var superdomain = pro_utils.getRequestSuperdomain();
+ var domain;
+ if (padutils.isProPadId(globalPadId)) {
+ var domainId = padutils.getDomainId(globalPadId);
+ domain = domains.getDomainRecord(domainId).subDomain +
+ '.' + superdomain;
+ }
+ else {
+ domain = superdomain;
+ }
+ var localId = padutils.globalToLocalId(globalPadId);
+ return "http://"+httpHost(domain)+"/"+localId;
+}
+
+function render_padinspector_get() {
+ var padId = request.params.padId;
+ if (!padId) {
+ response.write(FORM({action: request.path, method: 'get', style: 'border: 1px solid #ccc; background-color: #eee; padding: .2em 1em;'},
+ P("Pad Lookup: ",
+ INPUT({name: 'padId', value: '<enter pad id>'}),
+ INPUT({type: 'submit'}))));
+
+ // show recently active pads; the number of them may vary; lots of
+ // activity in a pad will push others off the list
+ response.write(H3("Recently Active Pads:"));
+ var recentlyActiveTable = TABLE({cellspacing: 0, cellpadding: 6, border: 1,
+ style: 'font-family: monospace;'});
+ var recentPads = activepads.getActivePads();
+ recentPads.forEach(function (info) {
+ var time = info.timestamp; // number
+ var pid = info.padId;
+ model.accessPadGlobal(pid, function(pad) {
+ if (pad.exists()) {
+ var numRevisions = pad.getHeadRevisionNumber();
+ var connected = collab_server.getNumConnections(pad);
+ recentlyActiveTable.push(
+ TR(TD(B(pid)),
+ TD({style: 'font-style: italic;'}, timeAgo(time)),
+ TD(connected+" connected"),
+ TD(numRevisions+" revisions"),
+ TD(A({href: qpath({padId: pid, revtext: "HEAD"})}, "HEAD")),
+ TD(A({href: qpath({padId: pid})}, "inspect")),
+ TD(A({href: qpath({padId: pid, snoop: 1})}, "snoop"))
+ ));
+ }
+ }, "r");
+ });
+ response.write(recentlyActiveTable);
+ response.stop();
+ }
+ if (startsWith(padId, '/')) {
+ padId = padId.substr(1);
+ }
+ if (request.params.snoop) {
+ sessions.setIsAnEtherpadAdmin(true);
+ response.redirect(_getPadUrl(padId));
+ }
+ if (request.params.setsupportstimeslider) {
+ var v = (String(request.params.setsupportstimeslider).toLowerCase() ==
+ 'true');
+ model.accessPadGlobal(padId, function(pad) {
+ pad.setSupportsTimeSlider(v);
+ });
+ response.write("on pad "+padId+": setSupportsTimeSlider("+v+")");
+ response.stop();
+ }
+ model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ response.write("Pad not found: /"+padId);
+ }
+ else {
+ var headRev = pad.getHeadRevisionNumber();
+ var div = DIV({style: 'font-family: monospace;'});
+
+ if (request.params.revtext) {
+ var i;
+ if (request.params.revtext == "HEAD") {
+ i = headRev;
+ } else {
+ i = Number(request.params.revtext);
+ }
+ var infoObj = {};
+ div.push(H2(A({href: request.path}, "PadInspector"),
+ ' > ', A({href: request.path+'?padId='+padId}, "/"+padId),
+ ' > ', "Revision ", i, "/", headRev,
+ SPAN({style: 'color: #949;'}, ' [ ', pad.getRevisionDate(i).toString(), ' ] ')));
+ div.push(H3("Browse Revisions: ",
+ ((i > 0) ? A({id: 'previous', href: qpath({revtext: (i-1)})}, '<< previous') : ''),
+ ' ',
+ ((i < pad.getHeadRevisionNumber()) ? A({id: 'next', href: qpath({revtext:(i+1)})}, 'next >>') : '')),
+ DIV({style: 'padding: 1em; border: 1px solid #ccc;'},
+ pad.getRevisionText(i, infoObj)));
+ if (infoObj.badLastChar) {
+ div.push(P("Bad last character of text (not newline): "+infoObj.badLastChar));
+ }
+ } else if (request.params.dumpstorage) {
+ div.push(P(collab_server.dumpStorageToString(pad)));
+ } else if (request.params.showlatest) {
+ div.push(P(pad.text()));
+ } else {
+ div.push(H2(A({href: request.path}, "PadInspector"), ' > ', "/"+padId));
+ // no action
+ div.push(P(A({href: qpath({revtext: 'HEAD'})}, 'HEAD='+headRev)));
+ div.push(P(A({href: qpath({dumpstorage: 1})}, 'dumpstorage')));
+ var supportsTimeSlider = pad.getSupportsTimeSlider();
+ if (supportsTimeSlider) {
+ div.push(P(A({href: qpath({setsupportstimeslider: 'false'})}, 'hide slider')));
+ }
+ else {
+ div.push(P(A({href: qpath({setsupportstimeslider: 'true'})}, 'show slider')));
+ }
+ }
+ }
+
+ var script = SCRIPT({type: 'text/javascript'}, html([
+ '$(document).keydown(function(e) {',
+ ' var h = undefined;',
+ ' if (e.keyCode == 37) { h = $("#previous").attr("href"); }',
+ ' if (e.keyCode == 39) { h = $("#next").attr("href"); }',
+ ' if (h) { window.location.href = h; }',
+ '});'
+ ].join('\n')));
+
+ response.write(HTML(
+ HEAD(SCRIPT({type: 'text/javascript', src: '/static/js/jquery-1.3.2.js?'+(+(new Date))})),
+ BODY(div, script)));
+ }, "r");
+}
+
+function render_analytics() {
+ response.redirect("https://www.google.com/analytics/reporting/?reset=1&id=12611622");
+}
+
+//----------------------------------------------------------------
+// eepnet license display
+//----------------------------------------------------------------
+
+function render_eepnet_licenses() {
+ var data = sqlobj.selectMulti('eepnet_signups', {}, {orderBy: 'date'});
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 2});
+ var cols = ['date','email','orgName','firstName','lastName', 'jobTitle','phone','estUsers'];
+ data.forEach(function(x) {
+ var tr = TR();
+ cols.forEach(function(colname) {
+ tr.push(TD(x[colname]));
+ });
+ t.push(tr);
+ });
+ response.write(HTML(BODY({style: 'font-family: monospace;'}, t)));
+}
+
+//----------------------------------------------------------------
+// pad integrity
+//----------------------------------------------------------------
+
+/*function render_changesettest_get() {
+ var nums = [0, 1, 2, 3, 0xfffffff, 0x02345678, 4];
+ var str = Changeset.numberArrayToString(nums);
+ var result = Changeset.numberArrayFromString(str);
+ var resultArray = result[0];
+ var remainingString = result[1];
+ var bad = false;
+ if (remainingString) {
+ response.write(P("remaining string length is: "+remainingString.length));
+ bad = true;
+ }
+ if (nums.length != resultArray.length) {
+ response.write(P("length mismatch: "+nums.length+" / "+resultArray.length));
+ bad = true;
+ }
+ response.write(P(nums[2]));
+ for(var i=0;i<nums.length;i++) {
+ var a = nums[i];
+ var b = resultArray[i];
+ if (a !== b) {
+ response.write(P("mismatch at element "+i+": "+a+" / "+b));
+ bad = true;
+ }
+ }
+ if (! bad) {
+ response.write("SUCCESS");
+ }
+}*/
+
+/////////
+
+function render_appendtest() {
+ var padId = request.params.padId;
+ var mode = request.params.mode;
+ var text = request.params.text;
+
+ model.accessPadGlobal(padId, function(pad) {
+ if (mode == "append") {
+ collab_server.appendPadText(pad, text);
+ }
+ else if (mode == "replace") {
+ collab_server.setPadText(pad, text);
+ }
+ });
+}
+
+//function render_flushall() {
+// dbwriter.writeAllToDB(null, true);
+// response.write("OK");
+//}
+
+//function render_flushpad() {
+// var padId = request.params.padId;
+// model.accessPadGlobal(padId, function(pad) {
+// dbwriter.writePad(pad, true);
+// });
+// response.write("OK");
+//}
+
+/*function render_foo() {
+ locking.doWithPadLock("CAT", function() {
+ sqlbase.createJSONTable("STUFF");
+ sqlbase.putJSON("STUFF", "dogs", {very:"bad"});
+ response.write(sqlbase.getJSON("STUFF", "dogs")); // {very:"bad"}
+ response.write(',');
+ response.write(sqlbase.getJSON("STUFF", "cats")); // undefined
+ response.write("<br/>");
+
+ sqlbase.createStringArrayTable("SEQUENCES");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 0, "1");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 1, "1");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 2, "2");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 3, "3");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 4, "5");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 30, "number30");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 29, "number29");
+ sqlbase.deleteStringArrayElement("SEQUENCES", "fibo", 29);
+ sqlbase.putConsecutiveStringArrayElements("SEQUENCES", "fibo", 19, [19,20,21,22]);
+ var a = [];
+ for(var i=0;i<31;i++) {
+ a.push(sqlbase.getStringArrayElement("SEQUENCES", "fibo", i));
+ }
+ response.write(a.join(',')); // 1,1,2,3,5,,, ... 19,20,21,22, ... ,,,number30
+ });
+}*/
+
+function render_timings() {
+ var timer = Packages.net.appjet.ajstdlib.timer;
+ var opnames = timer.getOpNames();
+
+ response.write(P(A({href: '/ep/admin/timingsreset'}, "reset all")));
+
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 3, style: 'font-family: monospace;'});
+ t.push(TR(TH("operation"),
+ TH("sample_count"),
+ TH("total_ms"),
+ TH("avg_ms")));
+
+ function r(x) {
+ return sprintf("%09.2f", x);
+ }
+ var rows = [];
+ for (var i = 0; i < opnames.length; i++) {
+ var stats = timer.getStats(opnames[i]);
+ rows.push([String(opnames[i]),
+ Math.floor(stats[0]),
+ stats[1],
+ stats[2]]);
+ }
+
+ var si = Number(request.params.sb || 0);
+
+ rows.sort(function(a,b) { return cmp(b[si],a[si]); });
+
+ rows.forEach(function(row) {
+ t.push(TR(TD(row[0]),
+ TD(row[1]),
+ TD(r(row[2])),
+ TD(r(row[3]))));
+ });
+
+ response.write(t);
+}
+
+function render_timingsreset() {
+ Packages.net.appjet.ajstdlib.timer.reset();
+ response.redirect('/ep/admin/timings');
+}
+
+// function render_jsontest() {
+// response.setContentType('text/plain; charset=utf-8');
+
+// var a = [];
+// a[0] = 5;
+// a[1] = 6;
+// a[9] = 8;
+// a['foo'] = "should appear";
+
+// jtest(a);
+
+// var obj1 = {
+// a: 1,
+// b: 2,
+// q: [true,true,,,,,,false,false,,,,{},{a:{a:{a:{a:{a:{a:[[{a:{a:false}}]]}}}}}}],
+// c: "foo",
+// d: {
+// nested: { obj: 'yo' },
+// bar: "baz"
+// },
+// e: 3.6,
+// 1: "numeric value",
+// 2: "anohter numeric value",
+// 2.46: "decimal numeric value",
+// foo: 3.212312310,
+// bar: 0.234242e-10,
+// baz: null,
+// ar: [{}, '1', [], [[[[]]]]],
+// n1: null,
+// n2: undefined,
+// n3: false,
+// n4: "null",
+// n5: "undefined"
+// };
+
+// jtest(obj1);
+
+// var obj2 = {
+// t1: 1232738532270
+// };
+
+// jtest(obj2);
+
+// // a javascript object plus numeric ids
+// var obj3 = {};
+// obj3["foo"] = "bar";
+// obj3[1] = "aaron";
+// obj3[2] = "iba";
+
+// jtest(obj3);
+
+// function jtest(x) {
+// response.write('----------------------------------------------------------------\n\n');
+
+// var str1 = JSON.stringify(x);
+// var str2 = fastJSON.stringify(x);
+
+// var str1_ = JSON.stringify(JSON.parse(str1));
+// var str2_ = fastJSON.stringify(fastJSON.parse(str2));
+
+// response.write([str1,str2].join('\n') + '\n\n');
+// response.write([str1_,str2_].join('\n') + '\n\n');
+// }
+// }
+
+function render_varz() {
+ var varzes = varz.getSnapshot();
+ response.setContentType('text/plain; charset=utf-8');
+ for (var k in varzes) {
+ response.write(k+': '+varzes[k]+'\n');
+ }
+}
+
+function render_extest() {
+ throw new Error("foo");
+}
+
+
+function _diagnosticRecordToHtml(obj) {
+ function valToHtml(o, noborder) {
+ if (typeof (o) != 'object') {
+ return String(o);
+ }
+ var t = TABLE((noborder ? {} : {style: "border-left: 1px solid black; border-top: 1px solid black;"}));
+ if (typeof (o.length) != 'number') {
+ eachProperty(o, function(k, v) {
+ var tr = TR();
+ tr.push(TD({valign: "top", align: "right"}, B(k)));
+ tr.push(TD(valToHtml(v)));
+ t.push(tr);
+ });
+ } else {
+ if (o.length == 0) return "(empty array)";
+ for (var i = 0; i < o.length; ++i) {
+ var tr = TR();
+ tr.push(TD({valign: "top", align: "right"}, B(i)));
+ tr.push(TD(valToHtml(o[i])));
+ t.push(tr);
+ }
+ }
+ return t;
+ }
+ return valToHtml(obj, true);
+}
+
+function render_diagnostics() {
+ var start = Number(request.params.start || 0);
+ var count = Number(request.params.count || 100);
+ var diagnostic_entries = sqlbase.getAllJSON("PAD_DIAGNOSTIC", start, count);
+ var expandArray = request.params.expand || [];
+
+ if (typeof (expandArray) == 'string') expandArray = [expandArray];
+ var expand = {};
+ for (var i = 0; i < expandArray.length; ++i) {
+ expand[expandArray[i]] = true;
+ }
+
+ function makeLink(text, expand, collapse, start0, count0) {
+ start0 = (typeof(start0) == "number" ? start0 : start);
+ count0 = count0 || count;
+ collapse = collapse || [];
+ expand = expand || [];
+
+ var collapseObj = {};
+ for (var i = 0; i < collapse.length; ++i) {
+ collapseObj[collapse[i]] = true;
+ }
+ var expandString =
+ expandArray.concat(expand).filter(function(x) { return ! collapseObj[x] }).map(function(x) { return "expand="+encodeURIComponent(x) }).join("&");
+
+ var url = request.path + "?start="+start0+"&count="+count0+"&"+expandString+(expand.length == 1 ? "#"+md5(expand[0]) : "");
+
+ return A({href: url}, text);
+ }
+
+ var t = TABLE({border: 1, cellpadding: 2, style: "font-family: monospace;"});
+ diagnostic_entries.forEach(function(ent) {
+ var tr = TR()
+ tr.push(TD({valign: "top", align: "right"}, (new Date(Number(ent.id.split("-")[0]))).toString()));
+ tr.push(TD({valign: "top", align: "right"}, ent.id));
+ if (expand[ent.id]) {
+ tr.push(TD(A({name: md5(ent.id)}, makeLink("(collapse)", false, [ent.id])), BR(),
+ _diagnosticRecordToHtml(ent.value)));
+ } else {
+ tr.push(TD(A({name: md5(ent.id)}, makeLink(_diagnosticRecordToHtml({padId: ent.value.padId, disconnectedMessage: ent.value.disconnectedMessage}), [ent.id]))));
+ }
+ t.push(tr);
+ });
+
+ var body = BODY();
+ body.push(P("Showing entries ", start, "-", start+diagnostic_entries.length, ". ",
+ (start > 0 ? makeLink("Show previous "+count+".", [], [], start-count) : ""),
+ (diagnostic_entries.length == count ? makeLink("Show next "+count+".", [], [], start+count) : "")));
+ body.push(t);
+
+ response.write(HTML(body));
+}
+
+//----------------------------------------------------------------
+import("etherpad.billing.billing");
+
+function render_testbillingdirect() {
+ var invoiceId = billing.createInvoice();
+ var ret = billing.directPurchase(invoiceId, 0, 'EEPNET', 500, 'DISCOUNT', {
+ cardType: "Visa",
+ cardNumber: "4501251685453214",
+ cardExpiration: "042019",
+ cardCvv: "123",
+ nameSalutation: "Dr.",
+ nameFirst: "John",
+ nameMiddle: "D",
+ nameLast: "Zamfirescu",
+ nameSuffix: "none",
+ addressStreet: "531 Main St. Apt. 1227",
+ addressStreet2: "",
+ addressCity: "New York",
+ addressState: "NY",
+ addressCountry: "US",
+ addressZip: "10044"
+ }, "https://"+request.host+"/ep/about/testbillingnotify");
+ if (ret.status == 'success') {
+ response.write(P("Success! Invoice id: "+ret.purchaseInfo.invoiceId+" for "+ret.purchaseInfo.cost));
+ } else {
+ response.write(P("Failure: "+ret.toSource()))
+ }
+}
+
+function render_testbillingrecurring() {
+ var invoiceId = billing.createInvoice();
+ var ret = billing.directPurchase(invoiceId, 0, 'EEPNET', 1, 'DISCOUNT', {
+ cardType: "Visa",
+ cardNumber: "4501251685453214",
+ cardExpiration: "042019",
+ cardCvv: "123",
+ nameSalutation: "Dr.",
+ nameFirst: "John",
+ nameMiddle: "D",
+ nameLast: "Zamfirescu",
+ nameSuffix: "none",
+ addressStreet: "531 Main St. Apt. 1227",
+ addressStreet2: "",
+ addressCity: "New York",
+ addressState: "NY",
+ addressCountry: "US",
+ addressZip: "10044"
+ }, "https://"+request.host+"/ep/about/testbillingnotify", true);
+ if (ret.status == 'success') {
+ var transactionId = billing.getTransaction(ret.purchaseInfo.transactionId).txnId;
+ var purchaseId = ret.purchaseInfo.purchaseId;
+ response.write(P("Direct billing successful. PayPal transaction id: ", transactionId));
+
+ invoiceId = billing.createInvoice();
+ ret = billing.asyncRecurringPurchase(
+ invoiceId, purchaseId, transactionId, 500,
+ "https://"+request.host+"/ep/about/testbillingnotify");
+ if (ret.status == 'success') {
+ response.write(P("Woot! Recurrent billing successful! ", ret.purchaseInfo.invoiceId, " for ", ret.purchaseInfo.cost));
+ } else {
+ response.write(P("Failure: "+ret.toSource()));
+ }
+ } else {
+ response.write("Direct billing failure: "+ret.toSource());
+ }
+}
+
+function render_testbillingexpress() {
+ var urlPrefix = "http://"+request.host+request.path;
+ var session = sessions.getSession();
+ var notifyUrl = "http://"+request.host+"/ep/about/testbillingnotify";
+
+ switch (request.params.step) {
+ case '0':
+ response.write(P("You'll be charged $400 for EEPNET. Click the link below to go to paypal."));
+ response.write(A({href: urlPrefix+"?step=1"}, "Link"));
+ break;
+ case '1':
+ var ret = billing.beginExpressPurchase(1, 'EEPNET', 400, 'DISCOUNT', urlPrefix+"?step=2", urlPrefix+"?step=0", notifyUrl);
+ if (ret.status != 'success') {
+ response.write("Error: "+ret.debug.toSource());
+ response.stop();
+ }
+ session.purchaseInfo = ret.purchaseInfo;
+ response.redirect(paypalPurchaseUrl(ret.purchaseInfo.token));
+ break;
+ case '2':
+ var ret = billing.continueExpressPurchase(session.purchaseInfo);
+ if (! ret.status == 'success') {
+ response.write("Error: "+ret.debug.toSource());
+ response.stop();
+ }
+ session.payerInfo = ret.payerInfo;
+
+ response.write(P("You approved the transaction. Click 'confirm' to confirm."));
+ response.write(A({href: urlPrefix+"?step=3"}, "Confirm"));
+ break;
+ case '3':
+ var ret = billing.completeExpressPurchase(session.purchaseInfo, session.payerInfo, notifyUrl);
+ if (ret.status == 'failure') {
+ response.write("Error: "+ret.debug.toSource());
+ response.stop();
+ }
+ if (ret.status == 'pending') {
+ response.write("Your charge is pending. You will be notified by email when your payment clears. Your invoice number is "+session.purchaseInfo.invoiceId);
+ response.stop();
+ }
+
+ response.write(P("Purchase completed: invoice # is "+session.purchaseInfo.invoiceId+" for "+session.purchaseInfo.cost));
+ break;
+ default:
+ response.redirect(request.path+"?step=0");
+ }
+}
+
+//----------------------------------------------------------------
+
+function render_genlicense_get() {
+
+ var t = TABLE({border: 1});
+ function ti(id, label) {
+ t.push(TR(TD({align: "right"}, LABEL({htmlFor: id}, label+":")),
+ TD(INPUT({id: id, name: id, type: 'text', size: 40}))));
+ }
+
+ ti("name", "Name of Licensee");
+ ti("org", "Name of Organization");
+ ti("userQuota", "User Quota");
+
+ t.push(TR(TD({align: "right"}, LABEL("Software Edtition:")),
+ TD( SELECT({name: "edition"},
+ OPTION({value: licensing.getEditionId('PRIVATE_NETWORK_EVALUATION')},
+ "Private Network EVALUATION"),
+ OPTION({value: licensing.getEditionId('PRIVATE_NETWORK')},
+ "Private Network")))));
+
+ ti("expdays", "Number of days until expiration\n(leave blank if never expires)");
+
+ t.push(TR(TD({colspan: 2}, INPUT({type: "submit"}))));
+
+ var f = FORM({action: request.path, method: "post"});
+ f.push(t);
+
+ response.write(HTML(BODY(f)));
+}
+
+function render_genlicense_post() {
+ var name = request.params.name;
+ var org = request.params.org;
+ var editionId = +request.params.edition;
+ var editionName = licensing.getEditionName(editionId);
+ var userQuota = +request.params.userQuota;
+
+ var expiresTime = null;
+ if (request.params.expdays) {
+ expiresTime = +(new Date) + 1000*60*60*24*(+request.params.expdays);
+ }
+
+ var licenseKey = licensing.generateNewKey(
+ name,
+ org,
+ expiresTime,
+ editionId,
+ userQuota
+ );
+
+ // verify
+ if (!licensing.isValidKey(licenseKey)) {
+ throw Error("License key I just created is not valid: "+licenseKey);
+ }
+
+ // TODO: write to database??
+ //
+
+ // display
+ var licenseInfo = licensing.decodeLicenseInfoFromKey(licenseKey);
+ var t = TABLE({border: 1});
+ function line(k, v) {
+ t.push(TR(TD({align: "right"}, k+":"),
+ TD(v)));
+ }
+
+ var key = licenseKey.split(":")[2];
+ if ((key.length % 2) != 0) {
+ key = key + "+";
+ }
+ var keyLine1 = key.substr(0, key.length/2);
+ var keyLine2 = key.substr(key.length/2, key.length);
+
+ line("Name", licenseInfo.personName);
+ line("Organization", licenseInfo.organizationName);
+ line("Key", P(keyLine1, BR(), keyLine2));
+ line("Software Edition", licenseInfo.editionName);
+ line("User Quota", licenseInfo.userQuota);
+ line("Expires", (+licenseInfo.expiresDate > 0) ? licenseInfo.expiresDate.toString() : "(never)");
+
+ response.write(HTML(BODY(t)));
+}
+
+//----------------------------------------------------------------
+
+import("etherpad.metrics.metrics");
+
+function render_flows() {
+ if (request.params.imgId && getSession()[request.params.imgId]) {
+ var arr = getSession()[request.params.imgId];
+ metrics[arr[0]](arr[1], Array.prototype.slice.call(arr[2]));
+ response.stop();
+ }
+
+ function drawHistogram(name, h) {
+ var imgKey = Math.round(Math.random()*1e12);
+ print(IMG({src: request.path+"?imgId="+imgKey}));
+ getSession()[imgKey] = ["respondWithPieChart", name, h];
+ }
+
+ var body = BODY();
+ function print() {
+ for (var i = 0; i < arguments.length; ++i) {
+ body.push(arguments[i]);
+ }
+ }
+
+ var [startDate, endDate] = [7, 1].map(function(a) { return new Date(Date.now() - 86400*1000*a); });
+
+ var allFlows = metrics.getFlows(startDate, endDate);
+
+/*
+ print(P("All flows:"));
+
+ eachProperty(allFlows, function(k, flows) {
+ print(P(k, html(" &raquo; ")));
+ flows.forEach(function(flow) {
+ print(P(flow.toString()));
+ });
+ });
+ response.write(HTML(body));
+ return;
+*/
+
+ print(P("Parsing logs from: "+startDate+" through "+endDate));
+
+ var fs =
+ [metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/about/pricing-eepnet', '/ep/store/eepnet-eval-signup'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/about/pricing-free'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/about/pricing-eepod'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/store/eepnet-eval-signup'], true),
+ metrics.getFunnel(startDate, endDate, ['/', '(pad)']),
+ metrics.getFunnel(startDate, endDate, ['/', '/ep/pad/newpad'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/screencast', '(pad)'])];
+
+ function vcnt(i, i2) {
+ return fs[i].visitorCounts[i2];
+ }
+ function pct(f) {
+ return ""+Math.round(f*10000)/100+"%"
+ }
+ function cntAndPct(i, i2) {
+ if (i2 === undefined) { i2 = 1; }
+ return ""+vcnt(i, i2)+" ("+pct(vcnt(i, i2)/vcnt(i, i2-1))+")";
+ }
+ print(P("Of ", vcnt(0, 0), " visitors to the pricing page, ",
+ cntAndPct(0), " of them viewed eepnet, (", cntAndPct(0, 2), " of those downloaded), ",
+ cntAndPct(1), " of them viewed free, and ",
+ cntAndPct(2), " of them viewed eepod. ",
+ cntAndPct(3), " of them clicked on the eval signup link straight up."
+ ),
+ P("Of ", vcnt(4, 0), " visitors to the home page, ",
+ cntAndPct(4), " of them went to a pad page in the same flow; ",
+ cntAndPct(5), " of them clicked the new pad button immediately."),
+ P("Of ", vcnt(6, 0), " vistitors to the screencast page, ",
+ cntAndPct(6), " of them visisted a pad page in the same flow."));
+
+ var origins = metrics.getOrigins(startDate, endDate, true);
+ print(P("Flow first origins: "));
+ drawHistogram("first origins", origins.flowFirsts);
+
+ var firstHits = metrics.getOrigins(startDate, endDate, false, true);
+ var padFirstHits = 0;
+ var nonPadFirstHits = 0;
+ print(P("First paths hit: "));
+ drawHistogram("first paths", firstHits.flowFirsts);
+ firstHits.flowFirsts.filter(function(x) {
+ if (x.value != '/' && ! startsWith(x.value, "/ep/")) {
+ padFirstHits += x.count;
+ return false;
+ }
+ nonPadFirstHits += x.count;
+ return true;
+ });
+ print(P("Some pad page: "+padFirstHits),
+ P("Non-pad page: "+nonPadFirstHits));
+
+ var exitsFromHomepage = metrics.getExits(startDate, endDate, '/', true);
+ print(P("Exits from homepage: "));
+ drawHistogram("exits", exitsFromHomepage.histogram)
+
+ response.write(HTML(body));
+}
+
+//----------------------------------------------------------------
+
+import("etherpad.pad.pad_migrations");
+
+function render_padmigrations() {
+ var residue = (request.params.r || 0);
+ var modulus = (request.params.m || 1);
+ var name = (request.params.n || (residue+"%"+modulus));
+ pad_migrations.runBackgroundMigration(residue, modulus, name);
+ response.write("done");
+ return true;
+}
+
+// TODO: add ability to delete entries?
+// TODO: show sizes?
+function render_cachebrowser() {
+ var path = request.params.path;
+ if (path && path.charAt(0) == ',') {
+ path = path.substr(1);
+ }
+ var pathArg = (path || "");
+ var c = appjet.cache;
+ if (path) {
+ path.split(",").forEach(function(part) {
+ c = c[part];
+ });
+ }
+
+ var d = DIV({style: 'font-family: monospace; text-decoration: none;'});
+
+ d.push(H3("appjet.cache --> "+pathArg.split(",").join(" --> ")));
+
+ var t = TABLE({border: 1});
+ keys(c).sort().forEach(function(k) {
+ var v = c[k];
+ if (v && (typeof(v) == 'object') && (!v.getDate)) {
+ t.push(TR(TD(A({style: 'text-decoration: none;',
+ href: request.path+"?path="+pathArg+","+k}, k))));
+ } else {
+ t.push(TR(TD(k), TD(v)));
+ }
+ });
+
+ d.push(t);
+ response.write(d);
+}
+
+function render_pne_tracker_get() {
+ var data = sqlobj.selectMulti('pne_tracking_data', {}, {});
+ data.sort(function(x, y) { return cmp(y.date, x.date); });
+
+ var t = TABLE();
+
+ var headrow = TR();
+ ['date', 'remote host', 'keyHash', 'name', 'value'].forEach(function(x) {
+ headrow.push(TH({align: "left", style: "padding: 0 6px;"}, x));
+ });
+ t.push(headrow);
+
+ data.forEach(function(d) {
+ var tr = TR();
+
+ tr.push(TD(d.date.toString().split(' ').slice(0,5).join('-')));
+
+ if (d.remoteIp) {
+ tr.push(TD(netutils.getHostnameFromIp(d.remoteIp) || d.remoteIp));
+ } else {
+ tr.push(TD("-"));
+ }
+
+ if (d.keyHash) {
+ tr.push(TD(A({href: '/ep/admin/pne-tracker-lookup-keyhash?hash='+d.keyHash}, d.keyHash)));
+ } else {
+ tr.push(TD("-"));
+ }
+
+ tr.push(TD(d.name));
+ tr.push(TD(d.value));
+
+ t.push(tr);
+ });
+
+ response.write(HTML(HEAD(html("<style>td { border-bottom: 1px solid #ddd; border-right: 1px solid #ddd; padding: 0 6px; } \n tr:hover { background: #ffc; }</style>"),
+ BODY({style: "font-family: monospace; font-size: 12px;"}, t))));
+}
+
+function render_pne_tracker_lookup_keyhash_get() {
+ var hash = request.params.hash;
+ // brute force it
+ var allLicenses = sqlobj.selectMulti('eepnet_signups', {}, {});
+ var record = null;
+ var i = 0;
+ while (i < allLicenses.length && record == null) {
+ var d = allLicenses[i];
+ if (md5(d.licenseKey).substr(0, 16) == hash) {
+ record = d;
+ }
+ i++;
+ }
+ if (!record) {
+ response.write("Not found. Perhaps this was a test download from local development, or a paid customer whose licenses we don't currently look through on this page.");
+ } else {
+ var kl = keys(record).sort();
+ var t = TABLE();
+ kl.forEach(function(k) {
+ t.push(TR(TH({align: "right"}, k+":"),
+ TD({style: "padding-left: 1em;"}, record[k])));
+ });
+ response.write(HTML(BODY(DIV({style: "font-family: monospace;"},
+ DIV(H1("Trial Signup Record:")), t))));
+ }
+}
+
+function render_reload_blog_db_get() {
+ var d = DIV();
+ if (request.params.ok) {
+ d.push(DIV(P("OK")));
+ }
+ d.push(FORM({method: "post", action: request.path},
+ INPUT({type: "submit", value: "Reload Blog DB Now"})));
+ response.write(HTML(BODY(d)));
+}
+
+function render_reload_blog_db_post() {
+ blogcontrol.reloadBlogDb();
+ response.redirect(request.path+"?ok=1");
+}
+
+function render_pro_domain_accounts() {
+ var accounts = sqlobj.selectMulti('pro_accounts', {}, {});
+ var domains = sqlobj.selectMulti('pro_domains', {}, {});
+
+ // build domain map
+ var domainMap = {};
+ domains.forEach(function(d) { domainMap[d.id] = d; });
+ accounts.sort(function(a,b) { return cmp(b.lastLoginDate, a.lastLoginDate); });
+
+ var b = BODY({style: "font-family: monospace;"});
+ b.push(accounts.length + " pro accounts.");
+ var t = TABLE({border: 1});
+ t.push(TR(TH("email"),
+ TH("domain"),
+ TH("lastLogin")));
+ accounts.forEach(function(u) {
+ t.push(TR(TD(u.email),
+ TD(domainMap[u.domainId].subDomain+"."+request.domain),
+ TD(u.lastLoginDate)));
+ });
+
+ b.push(t);
+
+ response.write(HTML(b));
+}
+
+
+function render_beta_valve_get() {
+ var d = DIV(
+ P("Beta Valve Status: ",
+ (pro_beta_control.isValveOpen() ?
+ SPAN({style: "color: green;"}, B("OPEN")) :
+ SPAN({style: "color: red;"}, B("CLOSED")))),
+ P(FORM({action: '/ep/admin/beta-valve-toggle', method: "post"},
+ BUTTON({type: "submit"}, "Toggle"))));
+
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 4, style: "font-family: monospace;"});
+ var signupList = sqlobj.selectMulti('pro_beta_signups', {}, {});
+ signupList.sort(function(a, b) {
+ return cmp(b.signupDate, a.signupDate);
+ });
+
+ d.push(HR());
+
+ if (getSession().betaAdminMessage) {
+ d.push(DIV({style: "border: 1px solid #ccc; padding: 1em; background: #eee;"},
+ getSession().betaAdminMessage));
+ delete getSession().betaAdminMessage;
+ }
+
+ d.push(P(signupList.length + " beta signups"));
+
+ d.push(FORM({action: '/ep/admin/beta-invite-multisend', method: 'post'},
+ P("Send ", INPUT({type: 'text', name: 'count', size: 3}), " invites."),
+ INPUT({type: "submit"})));
+
+ t.push(TR(TH("id"), TH("email"), TH("signupDate"),
+ TH("activationDate"), TH("activationCode"), TH(' ')));
+
+ signupList.forEach(function(s) {
+ var tr = TR();
+ tr.push(TD(s.id),
+ TD(s.email),
+ TD(s.signupDate),
+ TD(s.isActivated ? s.activationDate : "-"),
+ TD(s.activationCode));
+ if (!s.activationCode) {
+ tr.push(TD(FORM({action: '/ep/admin/beta-invite-send', method: 'post'},
+ INPUT({type: 'hidden', name: 'id', value: s.id}),
+ INPUT({type: 'submit', value: "Send Invite"}))));
+ } else {
+ tr.push(TD(' '));
+ }
+ t.push(tr);
+ });
+ d.push(t);
+ response.write(d);
+}
+
+function render_beta_valve_toggle_post() {
+ pro_beta_control.toggleValve();
+ response.redirect('/ep/admin/beta-valve');
+}
+
+function render_beta_invite_send_post() {
+ var id = request.params.id;
+ pro_beta_control.sendInvite(id);
+ response.redirect('/ep/admin/beta-valve');
+}
+
+function render_beta_invite_multisend_post() {
+ var count = request.params.count;
+ var signupList = sqlobj.selectMulti('pro_beta_signups', {}, {});
+ signupList.sort(function(a, b) {
+ return cmp(a.signupDate, b.signupDate);
+ });
+ var sent = 0;
+ for (var i = 0; ((i < signupList.length) && (sent < count)); i++) {
+ var record = signupList[i];
+ if (!record.activationCode) {
+ pro_beta_control.sendInvite(record.id);
+ sent++;
+ }
+ }
+ getSession().betaAdminMessage = (sent+" invites sent.");
+ response.redirect('/ep/admin/beta-valve');
+}
+
+function render_usagestats() {
+ response.redirect("/ep/admin/usagestats/");
+}
+
+function render_exceptions() {
+ exceptions.render();
+}
+
+function render_setadminmode() {
+ sessions.setIsAnEtherpadAdmin(
+ String(request.params.v).toLowerCase() == "true");
+ response.redirect("/ep/admin/");
+}
+
+// --------------------------------------------------------------
+// billing-related
+// --------------------------------------------------------------
+
+// some of these functions are only used from selenium tests, and so have no UI.
+
+function render_setdomainpaidthrough() {
+ var domainName = request.params.domain;
+ var when = new Date(Number(request.params.paidthrough));
+ if (! domainName || ! when) {
+ response.write("fail");
+ response.stop();
+ }
+ var domain = domains.getDomainRecordFromSubdomain(domainName);
+ var domainId = domain.id;
+
+ var subscription = team_billing.getSubscriptionForCustomer(domainId);
+ if (subscription) {
+ billing.updatePurchase(subscription.id, {paidThrough: when});
+ team_billing.domainCacheClear(domainId);
+ response.write("OK");
+ } else {
+ response.write("fail");
+ }
+}
+
+function render_runsubscriptions() {
+ team_billing.processAllSubscriptions();
+ response.write("OK");
+}
+
+function render_reset_subscription() {
+ var body = BODY();
+ if (request.isGet) {
+ body.push(FORM({method: "POST"},
+ "Subdomain: ", INPUT({type: "text", name: "subdomain"}), BUTTON({name: "clear"}, "Go")));
+ } else if (request.isPost) {
+ if (! request.params.confirm) {
+ var domain = domains.getDomainRecordFromSubdomain(request.params.subdomain);
+ var admins = pro_accounts.listAllDomainAdmins(domain.id);
+ body.push(P("Domain ", domain.subDomain, ".", request.domain, "; admins:"));
+ var p = UL();
+ admins.forEach(function(admin) {
+ p.push(LI(admin.fullName, " <", admin.email, ">"));
+ });
+ body.push(p);
+ var subscription = team_billing.getSubscriptionForCustomer(domain.id);
+ if (subscription) {
+ body.push(P("Subscription is currently ", subscription.status, ", and paid through: ", checkout.formatDate(subscription.paidThrough), "."))
+ body.push(FORM({method: "POST"},
+ INPUT({type: "hidden", name: "subdomain", value: request.params.subdomain}),
+ "Are you sure? ", BUTTON({name: "confirm", value: "yes"}, "YES")));
+ } else {
+ body.push(P("No current subscription"));
+ }
+ } else {
+ var domain = domains.getDomainRecordFromSubdomain(request.params.subdomain);
+ sqlcommon.inTransaction(function() {
+ team_billing.resetMaxUsers(domain.id);
+ sqlobj.deleteRows('billing_purchase', {customer: domain.id, type: 'subscription'});
+ team_billing.domainCacheClear(domain.id);
+ team_billing.clearRecurringBillingInfo(domain.id);
+ });
+ body.push("Done!")
+ }
+ }
+ body.push(A({href: request.path}, html("&laquo; back")));
+ response.write(HTML(body));
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/blogcontrol.js b/etherpad/src/etherpad/control/blogcontrol.js
new file mode 100644
index 0000000..9ec485d
--- /dev/null
+++ b/etherpad/src/etherpad/control/blogcontrol.js
@@ -0,0 +1,199 @@
+/**
+ * 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.
+ */
+
+//blogcontrol
+
+import("jsutils.*");
+import("atomfeed");
+import("funhtml.*");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.quotas");
+
+//----------------------------------------------------------------
+// bloghelpers
+//----------------------------------------------------------------
+bloghelpers = {};
+bloghelpers.disqusDeveloper = function() {
+ if (isProduction()) {
+ return '';
+ }
+ return [
+ '<script type="text/javascript">',
+ ' var disqus_developer = 1;',
+ '</script>'
+ ].join('\n');
+};
+
+bloghelpers.feedburnerUrl = function() {
+ var name = isProduction() ? "TheEtherPadBlog" : "TheEtherPadBlogDev";
+ return "http://feeds.feedburner.com/"+name;
+};
+
+bloghelpers.feedLink = function() {
+ return [
+ '<link rel="alternate"',
+ ' title="EtherPad Blog Feed"',
+ ' href="', bloghelpers.feedburnerUrl(), '"',
+ ' type="application/rss+xml" />'
+ ].join('');
+};
+
+bloghelpers.dfmt = function(d) {
+ return d.toString().split(' ').slice(0,3).join(' ');
+};
+
+bloghelpers.feedbuttonHtml = function() {
+ var aProps = {
+ href: bloghelpers.feedburnerUrl(),
+ rel: "alternate",
+ type: "application/rss+xml"
+ };
+
+ return SPAN(A(aProps,
+ IMG({src: "http://www.feedburner.com/fb/images/pub/feed-icon32x32.png",
+ alt: "EtherPad Blog Feed",
+ style: "vertical-align:middle; border:0;"}))).toHTML();
+};
+
+bloghelpers.getMaxUsersPerPad = function() {
+ return quotas.getMaxSimultaneousPadEditors()
+};
+
+//----------------------------------------------------------------
+// posts "database"
+//----------------------------------------------------------------
+
+function _wrapPost(p) {
+ var wp = {};
+ keys(p).forEach(function(k) { wp[k] = p[k]; });
+ wp.url = function() {
+ return "http://"+request.host+"/ep/blog/posts/"+p.id;
+ };
+ wp.renderContent = function() {
+ return renderTemplateAsString("blog/posts/"+p.id+".ejs",
+ {post: wp, bloghelpers: bloghelpers});
+ };
+ return wp;
+}
+
+function _addPost(id, title, author, published, updated) {
+ if (!appjet.cache.blogDB) {
+ appjet.cache.blogDB = {
+ posts: [],
+ postMap: {}
+ };
+ }
+ var p = {id: id, title: title, author: author, published: published, updated: updated};
+ appjet.cache.blogDB.posts.push(p);
+ appjet.cache.blogDB.postMap[p.id] = p;
+}
+
+function _getPostById(id) {
+ var p = appjet.cache.blogDB.postMap[id];
+ if (!p) { return null; }
+ return _wrapPost(p);
+}
+
+function _getAllPosts() {
+ return [];
+}
+
+function _sortBlogDB() {
+ appjet.cache.blogDB.posts.sort(function(a,b) { return cmp(b.published, a.published); });
+}
+
+//----------------------------------------------------------------
+// Posts
+//----------------------------------------------------------------
+
+function _initBlogDB() {
+ return;
+}
+
+function reloadBlogDb() {
+ delete appjet.cache.blogDB;
+ _initBlogDB();
+}
+
+function onStartup() {
+ reloadBlogDb();
+}
+
+//----------------------------------------------------------------
+// onRequest
+//----------------------------------------------------------------
+function onRequest(name) {
+ // nothing yet.
+}
+
+//----------------------------------------------------------------
+// main
+//----------------------------------------------------------------
+function render_main() {
+ renderFramed('blog/blog_main_body.ejs',
+ {posts: _getAllPosts(), bloghelpers: bloghelpers});
+}
+
+//----------------------------------------------------------------
+// render_feed
+//----------------------------------------------------------------
+function render_feed() {
+ var lastModified = new Date(); // TODO: most recent of all entries modified
+
+ var entries = [];
+ _getAllPosts().forEach(function(post) {
+ entries.push({
+ title: post.title,
+ author: post.author,
+ published: post.published,
+ updated: post.updated,
+ href: post.url(),
+ content: post.renderContent()
+ });
+ });
+
+ response.setContentType("application/atom+xml; charset=utf-8");
+
+ response.write(atomfeed.renderFeed(
+ "The EtherPad Blog", new Date(), entries,
+ "http://"+request.host+"/ep/blog/"));
+}
+
+//----------------------------------------------------------------
+// render_post
+//----------------------------------------------------------------
+function render_post(name) {
+ var p = _getPostById(name);
+ if (!p) {
+ return false;
+ }
+ renderFramed('blog/blog_post_body.ejs', {
+ post: p, bloghelpers: bloghelpers,
+ posts: _getAllPosts()
+ });
+ return true;
+}
+
+//----------------------------------------------------------------
+// render_new_from_etherpad()
+//----------------------------------------------------------------
+
+function render_new_from_etherpad() {
+ return "";
+}
+
diff --git a/etherpad/src/etherpad/control/connection_diagnostics_control.js b/etherpad/src/etherpad/control/connection_diagnostics_control.js
new file mode 100644
index 0000000..aaa1bb3
--- /dev/null
+++ b/etherpad/src/etherpad/control/connection_diagnostics_control.js
@@ -0,0 +1,87 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.*");
+import("etherpad.helpers.*");
+
+//----------------------------------------------------------------
+// Connection diagnostics
+//----------------------------------------------------------------
+
+/*
+function _getDiagnosticsCollection() {
+ var db = storage.getRoot("connection_diagnostics");
+ if (!db.diagnostics) {
+ db.diagnostics = new StorableCollection();
+ }
+ return db.diagnostics;
+}
+*/
+
+function render_main_get() {
+ /*
+ var diagnostics = _getDiagnosticsCollection();
+
+ var data = new StorableObject({
+ ip: request.clientAddr,
+ userAgent: request.headers['User-Agent']
+ });
+
+ diagnostics.add(data);
+
+ helpers.addClientVars({
+ diagnosticStorableId: data.id
+ });
+*/
+ renderFramed("main/connection_diagnostics_body.ejs");
+}
+
+function render_submitdata_post() {
+ response.setContentType('text/plain; charset=utf-8');
+ /*
+ var id = request.params.diagnosticStorableId;
+ var storedData = storage.getStorable(id);
+ if (!storedData) {
+ response.write("Error retreiving diagnostics record.");
+ response.stop();
+ }
+ var diagnosticData = JSON.parse(request.params.dataJson);
+ eachProperty(diagnosticData, function(k,v) {
+ storedData[k] = v;
+ });
+*/
+ response.write("OK");
+}
+
+function render_submitemail_post() {
+ response.setContentType('text/plain; charset=utf-8');
+ /*
+ var id = request.params.diagnosticStorableId;
+ var data = storage.getStorable(id);
+ if (!data) {
+ response.write("Error retreiving diagnostics record.");
+ response.stop();
+ }
+ var email = request.params.email;
+ if (!isValidEmail(email)) {
+ response.write("Invalid email address.");
+ response.stop();
+ }
+ data.email = email;
+*/
+ response.write("OK");
+}
+
diff --git a/etherpad/src/etherpad/control/global_pro_account_control.js b/etherpad/src/etherpad/control/global_pro_account_control.js
new file mode 100644
index 0000000..65d2124
--- /dev/null
+++ b/etherpad/src/etherpad/control/global_pro_account_control.js
@@ -0,0 +1,143 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("stringutils");
+import("stringutils.*");
+import("email.sendEmail");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_utils");
+
+jimport("java.lang.System.out.println");
+
+function onRequest() {
+ if (!getSession().oldFormData) {
+ getSession().oldFormData = {};
+ }
+ return false; // not handled yet.
+}
+
+function _errorDiv() {
+ var m = getSession().proAccountControlError;
+ delete getSession().proAccountControlError;
+ if (m) {
+ return DIV({className: "error"}, m);
+ }
+ return "";
+}
+
+function _redirectError(m) {
+ getSession().proAccountControlError = m;
+ response.redirect(request.path);
+}
+
+
+function render_main_get() {
+ response.redirect('/ep/pro-account/sign-in');
+}
+
+function render_sign_in_get() {
+ renderFramed('pro-account/sign-in.ejs', {
+ oldData: getSession().oldFormData,
+ errorDiv: _errorDiv
+ });
+}
+
+
+function render_sign_in_post() {
+ var email = trim(request.params.email);
+ var password = request.params.password;
+ var subDomain = request.params.subDomain;
+
+ subDomain = subDomain.toLowerCase();
+
+ getSession().oldFormData.email = email;
+ getSession().oldFormData.subDomain = subDomain;
+
+ var domainRecord = domains.getDomainRecordFromSubdomain(subDomain);
+ if (!domainRecord) {
+ _redirectError("Site address not found: "+subDomain+"."+request.host);
+ }
+
+ var instantSigninKey = stringutils.randomString(20);
+ syncedWithCache('global_signin_passwords', function(c) {
+ c[instantSigninKey] = {
+ email: email,
+ password: password
+ };
+ });
+
+ response.redirect(
+ "https://"+subDomain+"."+httpsHost(request.host)+
+ "/ep/account/sign-in?instantSigninKey="+instantSigninKey);
+}
+
+function render_recover_get() {
+ renderFramed('pro-account/recover.ejs', {
+ oldData: getSession().oldFormData,
+ errorDiv: _errorDiv
+ });
+}
+
+function render_recover_post() {
+
+ function _recoverLink(accountRecord, domainRecord) {
+ var host = (domainRecord.subDomain + "." + httpsHost(request.host));
+ return (
+ "https://"+host+"/ep/account/forgot-password?instantSubmit=1&email="+
+ encodeURIComponent(accountRecord.email));
+ }
+
+ var email = trim(request.params.email);
+
+ // lookup all domains associated with this email
+ var accountList = pro_accounts.getAllAccountsWithEmail(email);
+ println("account records matching ["+email+"]: "+accountList.length);
+
+ var domainList = [];
+ for (var i = 0; i < accountList.length; i++) {
+ domainList[i] = domains.getDomainRecord(accountList[i].domainId);
+ }
+
+ if (accountList.length == 0) {
+ _redirectError("No accounts were found associated with the email address \""+email+"\".");
+ }
+ if (accountList.length == 1) {
+ response.redirect(_recoverLink(accountList[0], domainList[0]));
+ }
+ if (accountList.length > 1) {
+ var fromAddr = '"EtherPad" <noreply@pad.spline.inf.fu-berlin.de>';
+ var subj = "EtherPad: account information";
+ var body = renderTemplateAsString(
+ 'pro/account/global-multi-domain-recover-email.ejs', {
+ accountList: accountList,
+ domainList: domainList,
+ recoverLink: _recoverLink,
+ email: email
+ }
+ );
+ sendEmail(email, fromAddr, subj, {}, body);
+ pro_utils.renderFramedMessage("Instructions have been sent to "+email+".");
+ }
+}
+
+
diff --git a/etherpad/src/etherpad/control/historycontrol.js b/etherpad/src/etherpad/control/historycontrol.js
new file mode 100644
index 0000000..a78cfad
--- /dev/null
+++ b/etherpad/src/etherpad/control/historycontrol.js
@@ -0,0 +1,226 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("etherpad.utils.render404");
+import("etherpad.pad.model");
+import("etherpad.collab.collab_server");
+import("etherpad.collab.ace.easysync2.*");
+import("jsutils.eachProperty");
+
+function _urlCache() {
+ if (!appjet.cache.historyUrlCache) {
+ appjet.cache.historyUrlCache = {};
+ }
+ return appjet.cache.historyUrlCache;
+}
+
+function _replyWithJSONAndCache(obj) {
+ obj.apiversion = _VERSION;
+ var output = fastJSON.stringify(obj);
+ _urlCache()[request.path] = output;
+ response.write(output);
+ response.stop();
+}
+
+function _replyWithJSON(obj) {
+ obj.apiversion = _VERSION;
+ response.write(fastJSON.stringify(obj));
+ response.stop();
+}
+
+function _error(msg, num) {
+ _replyWithJSON({error: String(msg), errornum: num});
+}
+
+var _VERSION = 1;
+
+var _ERROR_REVISION_NUMBER_TOO_LARGE = 14;
+
+function _do_text(padId, r) {
+ if (! padId) render404();
+ model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ render404();
+ }
+ if (r > pad.getHeadRevisionNumber()) {
+ _error("Revision number too large", _ERROR_REVISION_NUMBER_TOO_LARGE);
+ }
+ var text = pad.getInternalRevisionText(r);
+ text = _censorText(text);
+ _replyWithJSONAndCache({ text: text });
+ });
+}
+
+function _do_stat(padId) {
+ var obj = {};
+ if (! padId) {
+ obj.exists = false;
+ }
+ else {
+ model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ obj.exists = false;
+ }
+ else {
+ obj.exists = true;
+ obj.latestRev = pad.getHeadRevisionNumber();
+ }
+ });
+ }
+ _replyWithJSON(obj);
+}
+
+function _censorText(text) {
+ // may not change length of text
+ return text.replace(/(http:\/\/pad.spline.inf.fu-berlin.de\/)(\w+)/g, function(url, u1, u2) {
+ return u1 + u2.replace(/\w/g, '-');
+ });
+}
+
+function _do_changes(padId, first, last) {
+ if (! padId) render404();
+
+ var charPool = [];
+ var changeList = [];
+
+ function charPoolText(txt) {
+ charPool.push(txt);
+ return _encodeVarInt(txt.length);
+ }
+
+ model.accessPadGlobal(padId, function(pad) {
+
+ if (first > pad.getHeadRevisionNumber() || last > pad.getHeadRevisionNumber()) {
+ _error("Revision number too large", _ERROR_REVISION_NUMBER_TOO_LARGE);
+ }
+
+ var curAText = Changeset.makeAText("\n");
+ if (first > 0) {
+ curAText = pad.getInternalRevisionAText(first - 1);
+ }
+ curAText.text = _censorText(curAText.text);
+ var lastTimestamp = null;
+ for(var r=first;r<=last;r++) {
+ var binRev = [];
+ var timestamp = +pad.getRevisionDate(r);
+ binRev.push(_encodeTimeStamp(timestamp, lastTimestamp));
+ lastTimestamp = timestamp;
+ binRev.push(_encodeVarInt(1)); // fake author
+
+ var c = pad.getRevisionChangeset(r);
+ var splices = Changeset.toSplices(c);
+ splices.forEach(function (splice) {
+ var startChar = splice[0];
+ var endChar = splice[1];
+ var newText = splice[2];
+ oldText = curAText.text.substring(startChar, endChar);
+
+ if (oldText.length == 0) {
+ binRev.push('+');
+ binRev.push(_encodeVarInt(startChar));
+ binRev.push(charPoolText(newText));
+ }
+ else if (newText.length == 0) {
+ binRev.push('-');
+ binRev.push(_encodeVarInt(startChar));
+ binRev.push(charPoolText(oldText));
+ }
+ else {
+ binRev.push('*');
+ binRev.push(_encodeVarInt(startChar));
+ binRev.push(charPoolText(oldText));
+ binRev.push(charPoolText(newText));
+ }
+ });
+ changeList.push(binRev.join(''));
+
+ curAText = Changeset.applyToAText(c, curAText, pad.pool());
+ }
+
+ _replyWithJSONAndCache({charPool: charPool.join(''), changes: changeList.join(',')});
+
+ });
+}
+
+function render_history(padOpaqueRef, rest) {
+ if (_urlCache()[request.path]) {
+ response.write(_urlCache()[request.path]);
+ response.stop();
+ return true;
+ }
+ var padId;
+ if (padOpaqueRef == "CSi1xgbFXl" || padOpaqueRef == "13sentences") {
+ // made-up, hard-coded opaque ref, should be a table for these
+ padId = "jbg5HwzUX8";
+ }
+ else if (padOpaqueRef == "dO1j7Zf34z" || padOpaqueRef == "foundervisa") {
+ // made-up, hard-coded opaque ref, should be a table for these
+ padId = "3hS7kQyDXG";
+ }
+ else {
+ padId = null;
+ }
+ var regexResult;
+ if ((regexResult = /^stat$/.exec(rest))) {
+ _do_stat(padId);
+ }
+ else if ((regexResult = /^text\/(\d+)$/.exec(rest))) {
+ var r = Number(regexResult[1]);
+ _do_text(padId, r);
+ }
+ else if ((regexResult = /^changes\/(\d+)-(\d+)$/.exec(rest))) {
+ _do_changes(padId, Number(regexResult[1]), Number(regexResult[2]));
+ }
+ else {
+ return false;
+ }
+}
+
+function _encodeVarInt(num) {
+ var n = +num;
+ if (isNaN(n)) {
+ throw new Error("Can't encode non-number "+num);
+ }
+ var chars = [];
+ var done = false;
+ while (! done) {
+ if (n < 32) done = true;
+ var nd = (n % 32);
+ if (chars.length > 0) {
+ // non-first, will become non-last digit
+ nd = (nd | 32);
+ }
+ chars.push(_BASE64_DIGITS[nd]);
+ n = Math.floor(n / 32)
+ }
+ return chars.reverse().join('');
+}
+var _BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._";
+
+function _encodeTimeStamp(tMillis, baseMillis) {
+ var t = Math.floor(tMillis/1000);
+ var base = Math.floor(baseMillis/1000);
+ var absolute = ["+", t];
+ var resultPair = absolute;
+ if (((typeof base) == "number") && base <= t) {
+ var relative = ["", t - base];
+ if (relative[1] < absolute[1]) {
+ resultPair = relative;
+ }
+ }
+ return resultPair[0] + _encodeVarInt(resultPair[1]);
+}
diff --git a/etherpad/src/etherpad/control/loadtestcontrol.js b/etherpad/src/etherpad/control/loadtestcontrol.js
new file mode 100644
index 0000000..2a4e3f7
--- /dev/null
+++ b/etherpad/src/etherpad/control/loadtestcontrol.js
@@ -0,0 +1,93 @@
+/**
+ * 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.
+ */
+
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.dbwriter");
+import("etherpad.pad.activepads");
+import("etherpad.control.pad.pad_control");
+import("etherpad.collab.collab_server");
+
+// NOTE: we need to talk before enabling this again, for potential security vulnerabilities.
+var LOADTEST_ENABLED = false;
+
+function onRequest() {
+ if (!LOADTEST_ENABLED) {
+ response.forbid();
+ }
+}
+
+function render_createpad() {
+ var padId = request.params.padId;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ if (! pad.exists()) {
+ pad.create(pad_control.getDefaultPadText());
+ }
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_readpad() {
+ var padId = request.params.padId;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ /* nothing */
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_appendtopad() {
+ var padId = request.params.padId;
+ var text = request.params.text;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ collab_server.appendPadText(pad, text);
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_flushpad() {
+ var padId = request.params.padId;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ dbwriter.writePadNow(pad, true);
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_setpadtext() {
+ var padId = request.params.padId;
+ var text = request.params.text;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ collab_server.setPadText(pad, text);
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/maincontrol.js b/etherpad/src/etherpad/control/maincontrol.js
new file mode 100644
index 0000000..261ddaf
--- /dev/null
+++ b/etherpad/src/etherpad/control/maincontrol.js
@@ -0,0 +1,54 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("funhtml.*");
+import("stringutils.toHTML");
+
+import("etherpad.globals.*");
+import("etherpad.helpers.*");
+import("etherpad.licensing");
+import("etherpad.log");
+import("etherpad.utils.*");
+
+import("etherpad.control.blogcontrol");
+
+import("etherpad.pad.model");
+import("etherpad.collab.collab_server");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+function render_main() {
+ if (request.path == '/ep/') {
+ response.redirect('/');
+ }
+ renderFramed('main/home.ejs', {
+ newFromEtherpad: blogcontrol.render_new_from_etherpad()
+ });
+ return true;
+}
+
+function render_support() {
+ renderFramed("main/support_body.ejs");
+}
+
+function render_changelog_get() {
+ renderFramed("main/changelog.ejs");
+}
+
+
diff --git a/etherpad/src/etherpad/control/pad/pad_changeset_control.js b/etherpad/src/etherpad/control/pad/pad_changeset_control.js
new file mode 100644
index 0000000..5af7ed0
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_changeset_control.js
@@ -0,0 +1,280 @@
+/**
+ * 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.
+ */
+
+import("etherpad.helpers");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.utils.*");
+import("fastJSON");
+import("etherpad.collab.server_utils.*");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("cache_utils.syncedWithCache");
+import("etherpad.log");
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+import("stringutils");
+import("stringutils.sprintf");
+
+var _JSON_CACHE_SIZE = 10000;
+
+// to clear: appjet.cache.pad_changeset_control.jsoncache.map.clear()
+function _getJSONCache() {
+ return syncedWithCache('pad_changeset_control.jsoncache', function(cache) {
+ if (! cache.map) {
+ cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE);
+ }
+ return cache.map;
+ });
+}
+
+var _profiler = {
+ t: 0,
+ laps: [],
+ active: false,
+ start: function() {
+ _profiler.t = +new Date;
+ _profiler.laps = [];
+ //_profiler.active = true;
+ },
+ lap: function(name) {
+ if (! _profiler.active) return;
+ var t2 = +new Date;
+ _profiler.laps.push([name, t2 - _profiler.t]);
+ },
+ dump: function(info) {
+ if (! _profiler.active) return;
+ function padright(s, len) {
+ s = String(s);
+ return s + new Array(Math.max(0,len-s.length+1)).join(' ');
+ }
+ var str = padright(info,20)+": ";
+ _profiler.laps.forEach(function(e) {
+ str += padright(e.join(':'), 8);
+ });
+ java.lang.System.out.println(str);
+ },
+ stop: function() {
+ _profiler.active = false;
+ }
+};
+
+function onRequest() {
+ _profiler.start();
+
+ var parts = request.path.split('/');
+ // TODO(kroo): create a mapping between padId and read-only id
+ var urlId = parts[4];
+ var padId = parseUrlId(urlId).localPadId;
+ // var revisionId = parts[5];
+
+ padutils.accessPadLocal(padId, function(pad) {
+ if (! pad.exists() && pad.getSupportsTimeSlider()) {
+ response.forbid();
+ }
+ }, 'r');
+
+ // use the query string to specify start and end revision numbers
+ var startRev = parseInt(request.params["s"]);
+ var endRev = startRev + 100 * parseInt(request.params["g"]);
+ var granularity = parseInt(request.params["g"]);
+
+ _profiler.lap('A');
+ var changesetsJson =
+ getCacheableChangesetInfoJSON(padId, startRev, endRev, granularity);
+ _profiler.lap('X');
+
+ //TODO(kroo): set content-type to javascript
+ response.write(changesetsJson);
+ _profiler.lap('J');
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+
+ _profiler.lap('Z');
+ _profiler.dump(startRev+'/'+granularity+'/'+endRev);
+ _profiler.stop();
+
+ return true;
+}
+
+function getCacheableChangesetInfoJSON(padId, startNum, endNum, granularity) {
+ padutils.accessPadLocal(padId, function(pad) {
+ var lastRev = pad.getHeadRevisionNumber();
+ if (endNum > lastRev+1) {
+ endNum = lastRev+1;
+ }
+ endNum = Math.floor(endNum / granularity)*granularity;
+ }, 'r');
+
+ var cacheKey = "C/"+startNum+"/"+endNum+"/"+granularity+"/"+
+ padutils.getGlobalPadId(padId);
+
+ var cache = _getJSONCache();
+
+ var cachedJson = cache.get(cacheKey);
+ if (cachedJson) {
+ cache.touch(cacheKey);
+ //java.lang.System.out.println("HIT! "+cacheKey);
+ return cachedJson;
+ }
+ else {
+ var result = getChangesetInfo(padId, startNum, endNum, granularity);
+ var json = fastJSON.stringify(result);
+ cache.put(cacheKey, json);
+ //java.lang.System.out.println("MISS! "+cacheKey);
+ return json;
+ }
+}
+
+// uses changesets whose numbers are between startRev (inclusive)
+// and endRev (exclusive); 0 <= startNum < endNum
+function getChangesetInfo(padId, startNum, endNum, granularity) {
+ var forwardsChangesets = [];
+ var backwardsChangesets = [];
+ var timeDeltas = [];
+ var apool = new AttribPool();
+
+ var callId = stringutils.randomString(10);
+
+ log.custom("getchangesetinfo", {event: "start", callId:callId,
+ padId:padId, startNum:startNum,
+ endNum:endNum, granularity:granularity});
+
+ // This function may take a while and avoids holding a lock on the pad.
+ // Though the pad may change during execution of this function,
+ // after we retrieve the HEAD revision number, all other accesses
+ // are unaffected by new revisions being added to the pad.
+
+ var lastRev;
+ padutils.accessPadLocal(padId, function(pad) {
+ lastRev = pad.getHeadRevisionNumber();
+ }, 'r');
+
+ if (endNum > lastRev+1) {
+ endNum = lastRev+1;
+ }
+ endNum = Math.floor(endNum / granularity)*granularity;
+
+ var lines;
+ padutils.accessPadLocal(padId, function(pad) {
+ lines = _getPadLines(pad, startNum-1);
+ }, 'r');
+ _profiler.lap('L');
+
+ var compositeStart = startNum;
+ while (compositeStart < endNum) {
+ var whileBodyResult = padutils.accessPadLocal(padId, function(pad) {
+ _profiler.lap('c0');
+ if (compositeStart + granularity > endNum) {
+ return "break";
+ }
+ var compositeEnd = compositeStart + granularity;
+ var forwards = _composePadChangesets(pad, compositeStart, compositeEnd);
+ _profiler.lap('c1');
+ var backwards = Changeset.inverse(forwards, lines.textlines,
+ lines.alines, pad.pool());
+
+ _profiler.lap('c2');
+ Changeset.mutateAttributionLines(forwards, lines.alines, pad.pool());
+ _profiler.lap('c3');
+ Changeset.mutateTextLines(forwards, lines.textlines);
+ _profiler.lap('c4');
+
+ var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.pool(), apool);
+ _profiler.lap('c5');
+ var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.pool(), apool);
+ _profiler.lap('c6');
+ function revTime(r) {
+ var date = pad.getRevisionDate(r);
+ var s = Math.floor((+date)/1000);
+ //java.lang.System.out.println("time "+r+": "+s);
+ return s;
+ }
+
+ var t1, t2;
+ if (compositeStart == 0) {
+ t1 = revTime(0);
+ }
+ else {
+ t1 = revTime(compositeStart - 1);
+ }
+ t2 = revTime(compositeEnd - 1);
+ timeDeltas.push(t2 - t1);
+
+ _profiler.lap('c7');
+ forwardsChangesets.push(forwards2);
+ backwardsChangesets.push(backwards2);
+
+ compositeStart += granularity;
+ }, 'r');
+ if (whileBodyResult == "break") {
+ break;
+ }
+ }
+
+ log.custom("getchangesetinfo", {event: "finish", callId:callId,
+ padId:padId, startNum:startNum,
+ endNum:endNum, granularity:granularity});
+
+ return { forwardsChangesets:forwardsChangesets,
+ backwardsChangesets:backwardsChangesets,
+ apool: apool.toJsonable(),
+ actualEndNum: endNum,
+ timeDeltas: timeDeltas };
+}
+
+// Compose a series of consecutive changesets from a pad.
+// precond: startNum < endNum
+function _composePadChangesets(pad, startNum, endNum) {
+ if (endNum - startNum > 1) {
+ var csFromPad = pad.getCoarseChangeset(startNum, endNum - startNum);
+ if (csFromPad) {
+ //java.lang.System.out.println("HIT! "+startNum+"-"+endNum);
+ return csFromPad;
+ }
+ else {
+ //java.lang.System.out.println("MISS! "+startNum+"-"+endNum);
+ }
+ //java.lang.System.out.println("composePadChangesets: "+startNum+','+endNum);
+ }
+ var changeset = pad.getRevisionChangeset(startNum);
+ for(var r=startNum+1; r<endNum; r++) {
+ var cs = pad.getRevisionChangeset(r);
+ changeset = Changeset.compose(changeset, cs, pad.pool());
+ }
+ return changeset;
+}
+
+// Get arrays of text lines and attribute lines for a revision
+// of a pad.
+function _getPadLines(pad, revNum) {
+ var atext;
+ _profiler.lap('PL0');
+ if (revNum >= 0) {
+ atext = pad.getInternalRevisionAText(revNum);
+ }
+ else {
+ atext = Changeset.makeAText("\n");
+ }
+ _profiler.lap('PL1');
+ var result = {};
+ result.textlines = Changeset.splitTextLines(atext.text);
+ _profiler.lap('PL2');
+ result.alines = Changeset.splitAttributionLines(atext.attribs,
+ atext.text);
+ _profiler.lap('PL3');
+ return result;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/pad/pad_control.js b/etherpad/src/etherpad/control/pad/pad_control.js
new file mode 100644
index 0000000..32ff8a3
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_control.js
@@ -0,0 +1,754 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("comet");
+import("email.sendEmail");
+import("fastJSON");
+import("jsutils.eachProperty");
+import("sqlbase.sqlbase");
+import("stringutils.{toHTML,md5}");
+import("stringutils");
+
+import("etherpad.collab.collab_server");
+import("etherpad.debug.dmesg");
+import("etherpad.globals.*");
+import("etherpad.helpers");
+import("etherpad.licensing");
+import("etherpad.quotas");
+import("etherpad.log");
+import("etherpad.log.{logRequest,logException}");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_config");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_quotas");
+
+import("etherpad.pad.revisions");
+import("etherpad.pad.chatarchive");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.control.pad.pad_view_control");
+import("etherpad.control.pad.pad_changeset_control");
+import("etherpad.control.pad.pad_importexport_control");
+import("etherpad.collab.readonly_server");
+
+import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
+
+jimport("java.lang.System.out.println");
+
+var DISABLE_PAD_CREATION = false;
+
+function onStartup() {
+ sqlbase.createJSONTable("PAD_DIAGNOSTIC");
+}
+
+function onRequest() {
+
+ // TODO: take a hard look at /ep/pad/FOO/BAR/ dispatching.
+ // Perhaps standardize on /ep/pad/<pad-id>/foo
+ if (request.path.indexOf('/ep/pad/auth/') == 0) {
+ if (request.isGet) {
+ return render_auth_get();
+ }
+ if (request.isPost) {
+ return render_auth_post();
+ }
+ }
+
+ if (pro_utils.isProDomainRequest()) {
+ pro_quotas.perRequestBillingCheck();
+ }
+
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [PrefixMatcher('/ep/pad/view/'), forward(pad_view_control)],
+ [PrefixMatcher('/ep/pad/changes/'), forward(pad_changeset_control)],
+ [PrefixMatcher('/ep/pad/impexp/'), forward(pad_importexport_control)],
+ [PrefixMatcher('/ep/pad/export/'), pad_importexport_control.renderExport]
+ ]);
+ return disp.dispatch();
+}
+
+//----------------------------------------------------------------
+// utils
+//----------------------------------------------------------------
+
+function getDefaultPadText() {
+ if (pro_utils.isProDomainRequest()) {
+ return pro_config.getConfig().defaultPadText;
+ }
+ return renderTemplateAsString("misc/pad_default.ejs", {padUrl: request.url.split("?", 1)[0]});
+}
+
+function assignName(pad, userId) {
+ if (padusers.isGuest(userId)) {
+ // use pad-specific name if possible
+ var userData = pad.getAuthorData(userId);
+ var nm = (userData && userData.name) || padusers.getUserName() || null;
+
+ // don't let name guest typed in last once we've assigned a name
+ // for this pad, so the user can change it
+ delete getSession().guestDisplayName;
+
+ return nm;
+ }
+ else {
+ return padusers.getUserName();
+ }
+}
+
+function assignColorId(pad, userId) {
+ // use pad-specific color if possible
+ var userData = pad.getAuthorData(userId);
+ if (userData && ('colorId' in userData)) {
+ return userData.colorId;
+ }
+
+ // assign random unique color
+ function r(n) {
+ return Math.floor(Math.random() * n);
+ }
+ var colorsUsed = {};
+ var users = collab_server.getConnectedUsers(pad);
+ var availableColors = [];
+ users.forEach(function(u) {
+ colorsUsed[u.colorId] = true;
+ });
+ for (var i = 0; i < COLOR_PALETTE.length; i++) {
+ if (!colorsUsed[i]) {
+ availableColors.push(i);
+ }
+ }
+ if (availableColors.length > 0) {
+ return availableColors[r(availableColors.length)];
+ } else {
+ return r(COLOR_PALETTE.length);
+ }
+}
+
+function _getPrivs() {
+ return {
+ maxRevisions: quotas.getMaxSavedRevisionsPerPad()
+ };
+}
+
+//----------------------------------------------------------------
+// linkfile (a file that users can save that redirects them to
+// a particular pad; auto-download)
+//----------------------------------------------------------------
+function render_linkfile() {
+ var padId = request.params.padId;
+
+ renderHtml("pad/pad_download_link.ejs", {
+ padId: padId
+ });
+
+ response.setHeader("Content-Disposition", "attachment; filename=\""+padId+".html\"");
+}
+
+//----------------------------------------------------------------
+// newpad
+//----------------------------------------------------------------
+
+function render_newpad() {
+ var session = getSession();
+ var padId;
+
+ if (pro_utils.isProDomainRequest()) {
+ padId = pro_pad_db.getNextPadId();
+ } else {
+ padId = randomUniquePadId();
+ }
+
+ session.instantCreate = padId;
+ response.redirect("/"+padId);
+}
+
+// Tokbox
+function render_newpad_xml_post() {
+ var localPadId;
+ if (pro_utils.isProDomainRequest()) {
+ localPadId = pro_pad_db.getNextPadId();
+ } else {
+ localPadId = randomUniquePadId();
+ }
+ // <RAFTER>
+ if (DISABLE_PAD_CREATION) {
+ if (! pro_utils.isProDomainRequest()) {
+ utils.render500();
+ return;
+ }
+ }
+ // </RAFTER>
+
+ padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) {
+ pad.create(getDefaultPadText());
+ }
+ });
+ response.setContentType('text/plain; charset=utf-8');
+ response.write([
+ '<newpad>',
+ '<url>http://'+request.host+'/'+localPadId+'</url>',
+ '</newpad>'
+ ].join('\n'));
+}
+
+//----------------------------------------------------------------
+// pad
+//----------------------------------------------------------------
+
+function _createIfNecessary(localPadId, pad) {
+ if (pad.exists()) {
+ delete getSession().instantCreate;
+ return;
+ }
+ // make sure localPadId is valid.
+ var validPadId = padutils.makeValidLocalPadId(localPadId);
+ if (localPadId != validPadId) {
+ response.redirect('/'+validPadId);
+ }
+ // <RAFTER>
+ if (DISABLE_PAD_CREATION) {
+ if (! pro_utils.isProDomainRequest()) {
+ response.redirect("/ep/pad/create?padId="+encodeURIComponent(localPadId));
+ return;
+ }
+ }
+ // </RAFTER>
+ // tokbox may use createImmediately
+ if (request.params.createImmediately || getSession().instantCreate == localPadId) {
+ pad.create(getDefaultPadText());
+ delete getSession().instantCreate;
+ return;
+ }
+ response.redirect("/ep/pad/create?padId="+encodeURIComponent(localPadId));
+}
+
+function _promptForMobileDevices(pad) {
+ // TODO: also work with blackbery and windows mobile and others
+ if (request.userAgent.isIPhone() && (!request.params.skipIphoneCheck)) {
+ renderHtml("pad/pad_iphone_body.ejs", {padId: pad.getLocalId()});
+ response.stop();
+ }
+}
+
+function _checkPadQuota(pad) {
+ var numConnectedUsers = collab_server.getNumConnections(pad);
+ var maxUsersPerPad = quotas.getMaxSimultaneousPadEditors(pad.getId());
+
+ if (numConnectedUsers >= maxUsersPerPad) {
+ log.info("rendered-padfull");
+ renderFramed('pad/padfull_body.ejs',
+ {maxUsersPerPad: maxUsersPerPad, padId: pad.getLocalId()});
+ response.stop();
+ }
+
+ if (pne_utils.isPNE()) {
+ if (!licensing.canSessionUserJoin()) {
+ renderFramed('pad/total_users_exceeded.ejs', {
+ userQuota: licensing.getActiveUserQuota(),
+ activeUserWindowHours: licensing.getActiveUserWindowHours()
+ });
+ response.stop();
+ }
+ }
+}
+
+function _checkIfDeleted(pad) {
+ // TODO: move to access control check on access?
+ if (pro_utils.isProDomainRequest()) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ if (propad.exists() && propad.isDeleted()) {
+ renderNoticeString("This pad has been deleted.");
+ response.stop();
+ }
+ });
+ }
+}
+
+function render_pad(localPadId) {
+ var proTitle = null, documentBarTitle, initialPassword = null;
+ var isPro = isProDomainRequest();
+ var userId = padusers.getUserId();
+
+ var opts = {};
+ var globalPadId;
+
+ if (isPro) {
+ pro_quotas.perRequestBillingCheck();
+ }
+
+ padutils.accessPadLocal(localPadId, function(pad) {
+ globalPadId = pad.getId();
+ request.cache.globalPadId = globalPadId;
+ _createIfNecessary(localPadId, pad);
+ _promptForMobileDevices(pad);
+ _checkPadQuota(pad);
+ _checkIfDeleted(pad);
+
+ if (request.params.inviteTo) {
+ getSession().nameGuess = request.params.inviteTo;
+ response.redirect('/'+localPadId);
+ }
+ var displayName;
+ if (request.params.displayName) { // tokbox
+ displayName = String(request.params.displayName);
+ }
+ else {
+ displayName = assignName(pad, userId);
+ }
+
+ if (isProDomainRequest()) {
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ proTitle = propad.getDisplayTitle();
+ initialPassword = propad.getPassword();
+ });
+ }
+ documentBarTitle = (proTitle || "Public Pad");
+
+ var specialKey = request.params.specialKey ||
+ (sessions.isAnEtherpadAdmin() ? collab_server.getSpecialKey('invisible') :
+ null);
+
+ helpers.addClientVars({
+ padId: localPadId,
+ globalPadId: globalPadId,
+ userAgent: request.headers["User-Agent"],
+ collab_client_vars: collab_server.getCollabClientVars(pad),
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ nameGuess: (getSession().nameGuess || null),
+ initialRevisionList: revisions.getRevisionList(pad),
+ serverTimestamp: +(new Date),
+ accountPrivs: _getPrivs(),
+ chatHistory: chatarchive.getRecentChatBlock(pad, 30),
+ numConnectedUsers: collab_server.getNumConnections(pad),
+ isProPad: isPro,
+ initialTitle: documentBarTitle,
+ initialPassword: initialPassword,
+ initialOptions: pad.getPadOptionsObj(),
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ userName: displayName,
+ userColor: assignColorId(pad, userId),
+ specialKey: specialKey,
+ specialKeyTranslation: collab_server.translateSpecialKey(specialKey),
+ });
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth") +
+ " "+(isPro ? "propad" : "nonpropad")+" "+
+ (isProUser ? "prouser" : "nonprouser");
+
+
+ renderHtml("pad/pad_body2.ejs",
+ {localPadId:localPadId,
+ pageTitle:toHTML(proTitle || localPadId),
+ initialTitle:toHTML(documentBarTitle),
+ bodyClass: bodyClass,
+ hasOffice: hasOffice(),
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ toHTML: toHTML,
+ prefs: prefs,
+ signinUrl: '/ep/account/sign-in?cont='+
+ encodeURIComponent(request.url),
+ fullSuperdomain: pro_utils.getFullSuperdomainHost()
+ });
+ return true;
+}
+
+function render_create_get() {
+ var padId = request.params.padId;
+ // <RAFTER>
+ var template = (DISABLE_PAD_CREATION && ! pro_utils.isProDomainRequest()) ?
+ "pad/create_body_rafter.ejs" :
+ "pad/create_body.ejs";
+ // </RAFTER>
+ renderFramed(template, {padId: padId,
+ fullSuperdomain: pro_utils.getFullSuperdomainHost()});
+}
+
+function render_create_post() {
+ var padId = request.params.padId;
+ getSession().instantCreate = padId;
+ response.redirect("/"+padId);
+}
+
+//----------------------------------------------------------------
+// saverevision
+//----------------------------------------------------------------
+
+function render_saverevision_post() {
+ var padId = request.params.padId;
+ var savedBy = request.params.savedBy;
+ var savedById = request.params.savedById;
+ var revNum = request.params.revNum;
+ var privs = _getPrivs();
+ padutils.accessPadLocal(padId, function(pad) {
+ if (! pad.exists()) { response.notFound(); }
+ var currentRevs = revisions.getRevisionList(pad);
+ if (currentRevs.length >= privs.maxRevisions) {
+ response.forbid();
+ }
+ var savedRev = revisions.saveNewRevision(pad, savedBy, savedById,
+ revNum);
+ readonly_server.broadcastNewRevision(pad, savedRev);
+ response.setContentType('text/x-json');
+ response.write(fastJSON.stringify(revisions.getRevisionList(pad)));
+ });
+}
+
+function render_saverevisionlabel_post() {
+ var userId = request.params.userId;
+ var padId = request.params.padId;
+ var revId = request.params.revId;
+ var newLabel = request.params.newLabel;
+ padutils.accessPadLocal(padId, function(pad) {
+ revisions.setLabel(pad, revId, userId, newLabel);
+ response.setContentType('text/x-json');
+ response.write(fastJSON.stringify(revisions.getRevisionList(pad)));
+ });
+}
+
+function render_getrevisionatext_get() {
+ var padId = request.params.padId;
+ var revId = request.params.revId;
+ var result = null;
+
+ var rev = padutils.accessPadLocal(padId, function(pad) {
+ var r = revisions.getStoredRevision(pad, revId);
+ var forWire = collab_server.getATextForWire(pad, r.revNum);
+ result = {atext:forWire.atext, apool:forWire.apool,
+ historicalAuthorData:forWire.historicalAuthorData};
+ return r;
+ }, "r");
+
+ response.setContentType('text/plain; charset=utf-8');
+ response.write(fastJSON.stringify(result));
+}
+
+//----------------------------------------------------------------
+// reconnect
+//----------------------------------------------------------------
+
+function _recordDiagnosticInfo(padId, diagnosticInfoJson) {
+
+ var diagnosticInfo = {};
+ try {
+ diagnosticInfo = fastJSON.parse(diagnosticInfoJson);
+ } catch (ex) {
+ log.warn("Error parsing diagnosticInfoJson: "+ex);
+ diagnosticInfo = {error: "error parsing JSON"};
+ }
+
+ // ignore userdups, unauth
+ if (diagnosticInfo.disconnectedMessage == "userdup" ||
+ diagnosticInfo.disconnectedMessage == "unauth") {
+ return;
+ }
+
+ var d = new Date();
+
+ diagnosticInfo.date = +d;
+ diagnosticInfo.strDate = String(d);
+ diagnosticInfo.clientAddr = request.clientAddr;
+ diagnosticInfo.padId = padId;
+ diagnosticInfo.headers = {};
+ eachProperty(request.headers, function(k,v) {
+ diagnosticInfo.headers[k] = v;
+ });
+
+ var uid = diagnosticInfo.uniqueId;
+
+ sqlbase.putJSON("PAD_DIAGNOSTIC", (diagnosticInfo.date)+"-"+uid, diagnosticInfo);
+
+}
+
+function recordMigratedDiagnosticInfo(objArray) {
+ objArray.forEach(function(obj) {
+ sqlbase.putJSON("PAD_DIAGNOSTIC", (obj.date)+"-"+obj.uniqueId, obj);
+ });
+}
+
+function render_reconnect() {
+ var localPadId = request.params.padId;
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ var userId = (padutils.getPrefsCookieUserId() || undefined);
+ var hasClientErrors = false;
+ var uniqueId;
+ try {
+ var obj = fastJSON.parse(request.params.diagnosticInfo);
+ uniqueId = obj.uniqueId;
+ errorMessage = obj.disconnectedMessage;
+ hasClientErrors = obj.collabDiagnosticInfo.errors.length > 0;
+ } catch (e) {
+ // guess it doesn't have errors.
+ }
+
+ log.custom("reconnect", {globalPadId: globalPadId, userId: userId,
+ uniqueId: uniqueId,
+ hasClientErrors: hasClientErrors,
+ errorMessage: errorMessage });
+
+ try {
+ _recordDiagnosticInfo(globalPadId, request.params.diagnosticInfo);
+ } catch (ex) {
+ log.warn("Error recording diagnostic info: "+ex+" / "+request.params.diagnosticInfo);
+ }
+
+ try {
+ _applyMissedChanges(localPadId, request.params.missedChanges);
+ } catch (ex) {
+ log.warn("Error applying missed changes: "+ex+" / "+request.params.missedChanges);
+ }
+
+ response.redirect('/'+localPadId);
+}
+
+/* posted asynchronously by the client as soon as reconnect dialogue appears. */
+function render_connection_diagnostic_info_post() {
+ var localPadId = request.params.padId;
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ var userId = (padutils.getPrefsCookieUserId() || undefined);
+ var hasClientErrors = false;
+ var uniqueId;
+ var errorMessage;
+ try {
+ var obj = fastJSON.parse(request.params.diagnosticInfo);
+ uniqueId = obj.uniqueId;
+ errorMessage = obj.disconnectedMessage;
+ hasClientErrors = obj.collabDiagnosticInfo.errors.length > 0;
+ } catch (e) {
+ // guess it doesn't have errors.
+ }
+ log.custom("disconnected_autopost", {globalPadId: globalPadId, userId: userId,
+ uniqueId: uniqueId,
+ hasClientErrors: hasClientErrors,
+ errorMessage: errorMessage});
+
+ try {
+ _recordDiagnosticInfo(globalPadId, request.params.diagnosticInfo);
+ } catch (ex) {
+ log.warn("Error recording diagnostic info: "+ex+" / "+request.params.diagnosticInfo);
+ }
+ response.setContentType('text/plain; charset=utf-8');
+ response.write("OK");
+}
+
+function _applyMissedChanges(localPadId, missedChangesJson) {
+ var missedChanges;
+ try {
+ missedChanges = fastJSON.parse(missedChangesJson);
+ } catch (ex) {
+ log.warn("Error parsing missedChangesJson: "+ex);
+ return;
+ }
+
+ padutils.accessPadLocal(localPadId, function(pad) {
+ if (pad.exists()) {
+ collab_server.applyMissedChanges(pad, missedChanges);
+ }
+ });
+}
+
+//----------------------------------------------------------------
+// feedback
+//----------------------------------------------------------------
+
+function render_feedback_post() {
+ var feedback = request.params.feedback;
+ var localPadId = request.params.padId;
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ var username = request.params.username;
+ var email = request.params.email;
+ var subject = 'EtherPad Feedback from '+request.clientAddr+' / '+globalPadId+' / '+username;
+
+ if (feedback.indexOf("@") > 0) {
+ subject = "@ "+subject;
+ }
+
+ feedback += "\n\n--\n";
+ feedback += ("User Agent: "+request.headers['User-Agent'] + "\n");
+ feedback += ("Session Referer: "+getSession().initialReferer + "\n");
+ feedback += ("Email: "+email+"\n");
+
+ // log feedback
+ var userId = padutils.getPrefsCookieUserId();
+ log.custom("feedback", {
+ globalPadId: globalPadId,
+ userId: userId,
+ email: email,
+ username: username,
+ feedback: request.params.feedback});
+
+ sendEmail(
+ 'feedback@pad.spline.inf.fu-berlin.de',
+ 'feedback@pad.spline.inf.fu-berlin.de',
+ subject,
+ {},
+ feedback
+ );
+ response.write("OK");
+}
+
+//----------------------------------------------------------------
+// emailinvite
+//----------------------------------------------------------------
+
+function render_emailinvite_post() {
+ var toEmails = String(request.params.toEmails).split(',');
+ var padId = String(request.params.padId);
+ var username = String(request.params.username);
+ var subject = String(request.params.subject);
+ var message = String(request.params.message);
+
+ log.custom("padinvite",
+ {toEmails: toEmails, padId: padId, username: username,
+ subject: subject, message: message});
+
+ var fromAddr = '"EtherPad" <noreply@pad.spline.inf.fu-berlin.de>';
+ // client enforces non-empty subject and message
+ var subj = '[EtherPad] '+subject;
+ var body = renderTemplateAsString('email/padinvite.ejs',
+ {body: message});
+ var headers = {};
+ var proAccount = getSessionProAccount();
+ if (proAccount) {
+ headers['Reply-To'] = proAccount.email;
+ }
+
+ response.setContentType('text/plain; charset=utf-8');
+ try {
+ sendEmail(toEmails, fromAddr, subj, headers, body);
+ response.write("OK");
+ } catch (e) {
+ logException(e);
+ response.setStatusCode(500);
+ response.write("Error");
+ }
+}
+
+//----------------------------------------------------------------
+// time-slider
+//----------------------------------------------------------------
+function render_slider() {
+ var parts = request.path.split('/');
+ var padOpaqueRef = parts[4];
+
+ helpers.addClientVars({padOpaqueRef:padOpaqueRef});
+
+ renderHtml("pad/padslider_body.ejs", {
+ // properties go here
+ });
+
+ return true;
+}
+
+//----------------------------------------------------------------
+// auth
+//----------------------------------------------------------------
+
+function render_auth_get() {
+ var parts = request.path.split('/');
+ var localPadId = parts[4];
+ var errDiv;
+ if (getSession().padPassErr) {
+ errDiv = DIV({style: "border: 1px solid #fcc; background: #ffeeee; padding: 1em; margin: 1em 0;"},
+ B(getSession().padPassErr));
+ delete getSession().padPassErr;
+ } else {
+ errDiv = DIV();
+ }
+ renderFramedHtml(function() {
+ return DIV({className: "fpcontent"},
+ DIV({style: "margin: 1em;"},
+ errDiv,
+ FORM({style: "border: 1px solid #ccc; padding: 1em; background: #fff6cc;",
+ action: request.path+'?'+request.query,
+ method: "post"},
+ LABEL(B("Please enter the password required to access this pad:")),
+ BR(), BR(),
+ INPUT({type: "text", name: "password"}), INPUT({type: "submit", value: "Submit"})
+ /*DIV(BR(), "Or ", A({href: '/ep/account/sign-in'}, "sign in"), ".")*/
+ )),
+ DIV({style: "padding: 0 1em;"},
+ P({style: "color: #444;"},
+ "If you have forgotten a pad's password, contact your site administrator.",
+ " Site administrators can recover lost pad text through the \"Admin\" tab.")
+ )
+ );
+ });
+ return true;
+}
+
+function render_auth_post() {
+ var parts = request.path.split('/');
+ var localPadId = parts[4];
+ var domainId = domains.getRequestDomainId();
+ if (!getSession().padPasswordAuth) {
+ getSession().padPasswordAuth = {};
+ }
+ var currentPassword = pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ return propad.getPassword();
+ });
+ if (request.params.password == currentPassword) {
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ getSession().padPasswordAuth[globalPadId] = true;
+ } else {
+ getSession().padPasswordAuth[globalPadId] = false;
+ getSession().padPassErr = "Incorrect password.";
+ }
+ var cont = request.params.cont;
+ if (!cont) {
+ cont = '/'+localPadId;
+ }
+ response.redirect(cont);
+}
+
+//----------------------------------------------------------------
+// chathistory
+//----------------------------------------------------------------
+
+function render_chathistory_get() {
+ var padId = request.params.padId;
+ var start = Number(request.params.start || 0);
+ var end = Number(request.params.end || 0);
+ var result = null;
+
+ var rev = padutils.accessPadLocal(padId, function(pad) {
+ result = chatarchive.getChatBlock(pad, start, end);
+ }, "r");
+
+ response.setContentType('text/plain; charset=utf-8');
+ response.write(fastJSON.stringify(result));
+}
+
diff --git a/etherpad/src/etherpad/control/pad/pad_importexport_control.js b/etherpad/src/etherpad/control/pad/pad_importexport_control.js
new file mode 100644
index 0000000..b7e5f4d
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_importexport_control.js
@@ -0,0 +1,319 @@
+/**
+ * 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.
+ */
+
+import("jsutils.arrayToSet");
+import("stringutils.{toHTML,md5}");
+import("stringutils");
+import("sync");
+import("varz");
+
+import("etherpad.control.pad.pad_view_control.getRevisionInfo");
+import("etherpad.helpers");
+import("etherpad.importexport.importexport");
+import("etherpad.log");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.importhtml");
+import("etherpad.pad.exporthtml");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.{render404,renderFramedError}");
+import("etherpad.collab.server_utils");
+
+function _log(obj) {
+ log.custom("import-export", obj);
+}
+
+//---------------------------------------
+// utilities
+//---------------------------------------
+
+function _getPadTextBytes(padId, revNum) {
+ if (revNum === undefined) {
+ return null;
+ }
+ return padutils.accessPadLocal(padId, function(pad) {
+ if (pad.exists()) {
+ var txt = exporthtml.getPadPlainText(pad, revNum);
+ return (new java.lang.String(txt)).getBytes("UTF-8");
+ } else {
+ return null;
+ }
+ }, 'r');
+}
+
+function _getPadHtmlBytes(padId, revNum, noDocType) {
+ if (revNum === undefined) {
+ return null;
+ }
+ var html = padutils.accessPadLocal(padId, function(pad) {
+ if (pad.exists()) {
+ return exporthtml.getPadHTMLDocument(pad, revNum, noDocType);
+ }
+ });
+ if (html) {
+ return (new java.lang.String(html)).getBytes("UTF-8");
+ } else {
+ return null;
+ }
+}
+
+function _getFileExtension(fileName, def) {
+ if (fileName.lastIndexOf('.') > 0) {
+ return fileName.substr(fileName.lastIndexOf('.')+1);
+ } else {
+ return def;
+ }
+}
+
+function _guessFileType(contentType, fileName) {
+ function _f(str) { return function() { return str; }}
+ var unchangedExtensions =
+ arrayToSet(['txt', 'htm', 'html', 'doc', 'docx', 'rtf', 'pdf', 'odt']);
+ var textExtensions =
+ arrayToSet(['js', 'scala', 'java', 'c', 'cpp', 'log', 'h', 'htm', 'html', 'css', 'php', 'xhtml',
+ 'dhtml', 'jsp', 'asp', 'sh', 'bat', 'pl', 'py']);
+ var contentTypes = {
+ 'text/plain': 'txt',
+ 'text/html': 'html',
+ 'application/msword': 'doc',
+ 'application/vnd.oasis.opendocument.text': 'odt',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
+ 'text/rtf': 'rtf',
+ 'application/pdf': 'pdf'
+ }
+
+ var ext = _getFileExtension(fileName);
+ if (ext) {
+ if (unchangedExtensions[ext]) {
+ return ext;
+ } else if (textExtensions[ext]) {
+ return 'txt';
+ }
+ }
+ if (contentType in contentTypes) {
+ return contentTypes[contentType]
+ }
+ // unknown type, nothing to return.
+ _log({type: "warning", error: "unknown-type", contentType: contentType, fileName: fileName});
+}
+
+function _noteExportFailure() {
+ varz.incrementInt("export-failed");
+}
+
+function _noteImportFailure() {
+ varz.incrementInt("import-failed");
+}
+
+//---------------------------------------
+// export
+//---------------------------------------
+
+// handles /ep/pad/export/*
+function renderExport() {
+ var parts = request.path.split('/');
+ var padId = server_utils.parseUrlId(parts[4]).localPadId;
+ var revisionId = parts[5];
+ var rev = null;
+ var format = request.params.format || 'txt';
+
+ if (! request.cache.skipAccess) {
+ _log({type: "request", direction: "export", format: format});
+ rev = getRevisionInfo(padId, revisionId);
+ if (! rev) {
+ render404();
+ }
+ request.cache.skipAccess = true;
+ }
+
+ var result = _exportToFormat(padId, revisionId, (rev || {}).revNum, format);
+ if (result === true) {
+ response.stop();
+ } else {
+ renderFramedError(result);
+ }
+ return true;
+}
+
+function _exportToFormat(padId, revisionId, revNum, format) {
+ var bytes = _doExportConversion(format,
+ function() { return _getPadTextBytes(padId, revNum); },
+ function(noDocType) { return _getPadHtmlBytes(padId, revNum, noDocType); });
+ if (! bytes) {
+ return "Unable to convert file for export... try a different format?"
+ } else if (typeof(bytes) == 'string') {
+ return bytes
+ } else {
+ response.setContentType(importexport.formats[format]);
+ response.setHeader("Content-Disposition", "attachment; filename=\""+padId+"-"+revisionId+"."+format+"\"");
+ response.writeBytes(bytes);
+ return true;
+ }
+}
+
+
+function _doExportConversion(format, getTextBytes, getHtmlBytes) {
+ if (! (format in importexport.formats)) {
+ return false;
+ }
+ var bytes;
+ var srcFormat;
+
+ if (format == 'txt') {
+ bytes = getTextBytes();
+ srcFormat = 'txt';
+ } else {
+ bytes = getHtmlBytes(format == 'doc' || format == 'odt');
+ srcFormat = 'html';
+ }
+ if (bytes == null) {
+ bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 0);
+ }
+
+ try {
+ var ret = importexport.convertFile(srcFormat, format, bytes);
+ if (typeof(ret) == 'string') {
+ _log({type: "error", error: "export-failed", format: format, message: ret});
+ _noteExportFailure();
+ return ret;
+ }
+ bytes = ret;
+ } catch (e) {
+ if (e.javaException instanceof org.mortbay.jetty.RetryRequest) {
+ throw e.javaException
+ }
+ if (e.javaException || e.rhinoException) {
+ net.appjet.oui.exceptionlog.apply(e.javaException || e.rhinoException);
+ }
+ bytes = null;
+ }
+ if (bytes == null || bytes.length == 0) {
+ _log({type: "error", error: "export-failed", format: format, message: ret});
+ _noteExportFailure();
+ return false;
+ }
+ return bytes;
+}
+
+//---------------------------------------
+// import
+//---------------------------------------
+
+function _getImportInfo(key) {
+ var session = getSession();
+ sync.callsyncIfTrue(session, function() { return ! ('importexport' in session) },
+ function() {
+ session.importexport = {};
+ });
+ var tokens = session.importexport;
+ sync.callsyncIfTrue(tokens, function() { return ! (key in tokens) },
+ function() {
+ tokens[key] = {};
+ });
+ return tokens[key];
+}
+
+function render_import() {
+ function _r(code) {
+ response.setContentType("text/html");
+ response.write("<html><body><script>try{parent.document.domain}catch(e){document.domain=document.domain}\n"+code+"</script></body></html>");
+ response.stop();
+ }
+
+ if (! request.isPost) {
+ response.stop();
+ }
+
+ var padId = decodeURIComponent(request.params.padId);
+ if (! padId) {
+ response.stop();
+ }
+
+ var file = request.files.file;
+ if (! file) {
+ _r('parent.pad.handleImportExportFrameCall("importFailed", "Please select a file to import.")');
+ }
+
+ var bytes = file.bytes;
+ var type = _guessFileType(file.contentType, file.filesystemName);
+
+ _log({type: "request", direction: "import", format: type});
+
+ if (! type) {
+ type = _getFileExtension(file.filesystemName, "no file extension found");
+ _r('parent.pad.handleImportExportFrameCall("importFailed", "'+importexport.errorUnsupported(type)+'")');
+ }
+
+ var token = md5(bytes);
+ var state = _getImportInfo(token);
+ state.bytes = bytes;
+ state.type = type;
+
+ _r("parent.pad.handleImportExportFrameCall('importSuccessful', '"+token+"')");
+}
+
+
+function render_import2() {
+ var token = request.params.token;
+
+ function _r(txt) {
+ response.write(txt);
+ response.stop();
+ }
+
+ if (! token) { _r("fail"); }
+
+ var state = _getImportInfo(token);
+ if (! state.type || ! state.bytes) { _r("fail"); }
+
+ var newBytes;
+ try {
+ newBytes = importexport.convertFile(state.type, "html", state.bytes);
+ } catch (e) {
+ if (e.javaException instanceof org.mortbay.jetty.RetryRequest) {
+ throw e.javaException;
+ }
+ net.appjet.oui.exceptionlog.apply(e);
+ throw e;
+ }
+
+ if (typeof(newBytes) == 'string') {
+ _log({type: "error", error: "import-failed", format: state.type, message: newBytes});
+ _noteImportFailure();
+ _r("msg:"+newBytes);
+ }
+
+ if (! newBytes || newBytes.length == 0) {
+ _r("fail");
+ }
+
+ var newHTML;
+ try {
+ newHTML = String(new java.lang.String(newBytes, "UTF-8"));
+ } catch (e) {
+ _r("fail");
+ }
+
+ if (! request.params.padId) { _r("fail"); }
+ padutils.accessPadLocal(request.params.padId, function(pad) {
+ if (! pad.exists()) {
+ _r("fail");
+ }
+ importhtml.setPadHTML(pad, newHTML);
+ });
+ _r("ok");
+}
diff --git a/etherpad/src/etherpad/control/pad/pad_view_control.js b/etherpad/src/etherpad/control/pad/pad_view_control.js
new file mode 100644
index 0000000..0606d2c
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_view_control.js
@@ -0,0 +1,287 @@
+/**
+ * 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.
+ */
+
+import("etherpad.helpers");
+import("etherpad.pad.model");
+import("etherpad.pad.padusers");
+import("etherpad.pad.padutils");
+import("etherpad.pad.exporthtml");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.utils.*");
+import("etherpad.pad.revisions");
+import("stringutils.toHTML");
+import("etherpad.collab.server_utils.*");
+import("etherpad.collab.collab_server.buildHistoricalAuthorDataMapForPadHistory");
+import("etherpad.collab.collab_server.getATextForWire");
+import("etherpad.control.pad.pad_changeset_control.getChangesetInfo");
+import("etherpad.globals");
+import("fastJSON");
+import("etherpad.collab.ace.easysync2.Changeset");
+import("etherpad.collab.ace.linestylefilter.linestylefilter");
+import("etherpad.collab.ace.domline.domline");
+
+//----------------------------------------------------------------
+// view (viewing a static revision of a pad)
+//----------------------------------------------------------------
+
+function onRequest() {
+ var parts = request.path.split('/');
+ // TODO(kroo): create a mapping between padId and read-only id
+ var readOnlyIdOrLocalPadId = parts[4];
+ var parseResult = parseUrlId(readOnlyIdOrLocalPadId);
+ var isReadOnly = parseResult.isReadOnly;
+ var viewId = parseResult.viewId;
+ var localPadId = parseResult.localPadId;
+ var globalPadId = parseResult.globalPadId;
+ var roPadId = parseResult.roPadId;
+ var revisionId = parts[5];
+
+ var rev = getRevisionInfo(localPadId, revisionId);
+ if (! rev) {
+ return false;
+ }
+
+ if (request.params.pt == 1) {
+ var padText = padutils.accessPadLocal(localPadId, function(pad) {
+ return pad.getRevisionText(rev.revNum);
+ }, 'r');
+
+ response.setContentType('text/plain; charset=utf-8');
+ response.write(padText);
+ } else {
+ var padContents, totalRevs, atextForWire, savedRevisions;
+ var supportsSlider;
+ padutils.accessPadLocal(localPadId, function(pad) {
+ padContents = [_getPadHTML(pad, rev.revNum),
+ pad.getRevisionText(rev.revNum)];
+ totalRevs = pad.getHeadRevisionNumber();
+ atextForWire = getATextForWire(pad, rev.revNum);
+ savedRevisions = revisions.getRevisionList(pad);
+ supportsSlider = pad.getSupportsTimeSlider();
+ }, 'r');
+
+ var _add = function(dict, anotherdict) {
+ for(var key in anotherdict) {
+ dict[key] = anotherdict[key];
+ }
+ return dict;
+ }
+
+ var getAdaptiveChangesetsArray = function(array, start, granularity) {
+ array = array || [];
+ start = start || 0;
+ granularity = granularity || Math.pow(10, Math.floor(Math.log(totalRevs+1) / Math.log(10)));
+ var changeset = _add(getChangesetInfo(localPadId, start, totalRevs+1, granularity), {
+ start: start,
+ granularity: Math.floor(granularity)
+ });
+ array.push(changeset);
+ if(changeset.actualEndNum != totalRevs+1 && granularity > 1)
+ getAdaptiveChangesetsArray(array, changeset.actualEndNum, Math.floor(granularity / 10));
+ return array;
+ }
+ var initialChangesets = [];
+ if (supportsSlider) {
+ initialChangesets = getAdaptiveChangesetsArray(
+ [
+ _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 1000)*1000, Math.floor(rev.revNum / 1000)*1000+1000, 100), {
+ start: Math.floor(rev.revNum / 1000)*1000,
+ granularity: 100
+ }),
+ _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 100)*100, Math.floor(rev.revNum / 100)*100+100, 10), {
+ start: Math.floor(rev.revNum / 100)*100,
+ granularity: 10
+ }),
+ _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 10)*10, Math.floor(rev.revNum / 10)*10+10, 1), {
+ start: Math.floor(rev.revNum / 10)*10,
+ granularity: 1
+ })]
+ );
+ }
+
+ var zpad = function(str, length) {
+ str = str+"";
+ while(str.length < length)
+ str = '0'+str;
+ return str;
+ };
+ var dateFormat = function(savedWhen) {
+ var date = new Date(savedWhen);
+ var month = zpad(date.getMonth()+1,2);
+ var day = zpad(date.getDate(),2);
+ var year = (date.getFullYear());
+ var hours = zpad(date.getHours(),2);
+ var minutes = zpad(date.getMinutes(),2);
+ var seconds = zpad(date.getSeconds(),2);
+ return ([month,'/',day,'/',year,' ',hours,':',minutes,':',seconds].join(""));
+ };
+
+ var proTitle = null;
+ var initialPassword = null;
+ if (isProDomainRequest()) {
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ proTitle = propad.getDisplayTitle();
+ initialPassword = propad.getPassword();
+ });
+ }
+ var documentBarTitle = (proTitle || "Public Pad");
+
+ var padHTML = padContents[0];
+ var padText = padContents[1];
+
+ var historicalAuthorData = padutils.accessPadLocal(localPadId, function(pad) {
+ return buildHistoricalAuthorDataMapForPadHistory(pad);
+ }, 'r');
+
+ helpers.addClientVars({
+ viewId: viewId,
+ initialPadContents: padText,
+ revNum: rev.revNum,
+ totalRevs: totalRevs,
+ initialChangesets: initialChangesets,
+ initialStyledContents: atextForWire,
+ savedRevisions: savedRevisions,
+ currentTime: rev.timestamp,
+ sliderEnabled: (!appjet.cache.killSlider) && request.params.slider != 0,
+ supportsSlider: supportsSlider,
+ historicalAuthorData: historicalAuthorData,
+ colorPalette: globals.COLOR_PALETTE,
+ padIdForUrl: readOnlyIdOrLocalPadId,
+ fullWidth: request.params.fullScreen == 1,
+ disableRightBar: request.params.sidebar == 0,
+ });
+
+ var userId = padusers.getUserId();
+ var isPro = isProDomainRequest();
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ var bodyClass = ["limwidth",
+ (isPro ? "propad" : "nonpropad"),
+ (isProUser ? "prouser" : "nonprouser")].join(" ");
+
+ renderHtml("pad/padview_body.ejs", {
+ bodyClass: bodyClass,
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: pro_accounts.getSessionProAccount(),
+ signinUrl: '/ep/account/sign-in?cont='+
+ encodeURIComponent(request.url),
+ padId: readOnlyIdOrLocalPadId,
+ padTitle: documentBarTitle,
+ rlabel: rev.label,
+ padHTML: padHTML,
+ padText: padText,
+ savedBy: rev.savedBy,
+ savedIp: rev.ip,
+ savedWhen: rev.timestamp,
+ toHTML: toHTML,
+ revisionId: revisionId,
+ dateFormat: dateFormat(rev.timestamp),
+ readOnly: isReadOnly,
+ roPadId: roPadId,
+ hasOffice: hasOffice()
+ });
+ }
+
+ return true;
+}
+
+function getRevisionInfo(localPadId, revisionId) {
+ var rev = padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) {
+ return null;
+ }
+ var r;
+ if (revisionId == "latest") {
+ // a "fake" revision for HEAD
+ var headRevNum = pad.getHeadRevisionNumber();
+ r = {
+ revNum: headRevNum,
+ label: "Latest text of pad "+localPadId,
+ savedBy: null,
+ savedIp: null,
+ timestamp: +pad.getRevisionDate(headRevNum)
+ };
+ } else if (revisionId == "autorecover") {
+ var revNum = _findLastGoodRevisionInPad(pad);
+ r = {
+ revNum: revNum,
+ label: "Auto-recovered text of pad "+localPadId,
+ savedBy: null,
+ savedIp: null,
+ timestamp: +pad.getRevisionDate(revNum)
+ };
+ } else if(revisionId.indexOf("rev.") === 0) {
+ var revNum = parseInt(revisionId.split(".")[1]);
+ var latest = pad.getHeadRevisionNumber();
+ if(revNum > latest)
+ revNum = latest;
+ r = {
+ revNum: revNum,
+ label: "Version " + revNum,
+ savedBy: null,
+ savedIp: null,
+ timestamp: +pad.getRevisionDate(revNum)
+ }
+
+ } else {
+ r = revisions.getStoredRevision(pad, revisionId);
+ }
+ if (!r) {
+ return null;
+ }
+ return r;
+ }, "r");
+ return rev;
+}
+
+function _findLastGoodRevisionInPad(pad) {
+ var revNum = pad.getHeadRevisionNumber();
+ function valueOrNullOnError(f) {
+ try { return f(); } catch (e) { return null; }
+ }
+ function isAcceptable(strOrNull) {
+ return (strOrNull && strOrNull.length > 20);
+ }
+ while (revNum > 0 &&
+ ! isAcceptable(valueOrNullOnError(function() { return pad.getRevisionText(revNum); }))) {
+ revNum--;
+ }
+ return revNum;
+}
+
+function _getPadHTML(pad, revNum) {
+ var atext = pad.getInternalRevisionAText(revNum);
+ var textlines = Changeset.splitTextLines(atext.text);
+ var alines = Changeset.splitAttributionLines(atext.attribs,
+ atext.text);
+
+ var pieces = [];
+ var apool = pad.pool();
+ for(var i=0;i<textlines.length;i++) {
+ var line = textlines[i];
+ var aline = alines[i];
+ var emptyLine = (line == '\n');
+ var domInfo = domline.createDomLine(! emptyLine, true);
+ linestylefilter.populateDomLine(line, aline, apool, domInfo);
+ domInfo.prepareForAdd();
+ var node = domInfo.node;
+ pieces.push('<div class="', node.className, '">',
+ node.innerHTML, '</div>\n');
+ }
+ return pieces.join('');
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/pne_manual_control.js b/etherpad/src/etherpad/control/pne_manual_control.js
new file mode 100644
index 0000000..0dd65f8
--- /dev/null
+++ b/etherpad/src/etherpad/control/pne_manual_control.js
@@ -0,0 +1,75 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+
+function onRequest() {
+ var p = request.path.split('/')[3];
+ if (!p) {
+ p = "main";
+ }
+ if (_getTitle(p)) {
+ _renderManualPage(p);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function _getTitle(t) {
+ var titles = {
+ 'main': " ",
+ 'installation-guide': "Installation Guide",
+ 'upgrade-guide': "Upgrade Guide",
+ 'configuration-guide': "Configuration Guide",
+ 'troubleshooting': "Troubleshooting",
+ 'faq': "FAQ",
+ 'changelog': "ChangeLog"
+ };
+ return titles[t];
+}
+
+function _renderTopnav(p) {
+ var d = DIV({className: "pne-manual-topnav"});
+ if (p != "main") {
+ d.push(A({href: '/ep/pne-manual/'}, "PNE Manual"),
+ " > ",
+ _getTitle(p));
+ }
+ return d;
+}
+
+function _renderManualPage(p, data) {
+ data = (data || {});
+ data.pneVersion = PNE_RELEASE_VERSION;
+
+ function getContent() {
+ return renderTemplateAsString('pne-manual/'+p+'.ejs', data);
+ }
+ renderFramed('pne-manual/manual-template.ejs', {
+ getContent: getContent,
+ renderTopnav: function() { return _renderTopnav(p); },
+ title: _getTitle(p),
+ id: p,
+ });
+ return true;
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pne_tracker_control.js b/etherpad/src/etherpad/control/pne_tracker_control.js
new file mode 100644
index 0000000..ee36645
--- /dev/null
+++ b/etherpad/src/etherpad/control/pne_tracker_control.js
@@ -0,0 +1,48 @@
+/**
+ * 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.
+ */
+
+import("image");
+import("blob");
+import("sqlbase.sqlobj");
+import("jsutils.*");
+
+function render_t() {
+ var data = {
+ date: new Date(),
+ remoteIp: request.clientAddr
+ };
+ if (request.params.k) {
+ data.keyHash = request.params.k;
+ }
+ var found = false;
+ eachProperty(request.params, function(name, value) {
+ if (name != "k") {
+ data.name = name;
+ data.value = value;
+ found = true;
+ }
+ });
+ if (found) {
+ sqlobj.insert('pne_tracking_data', data);
+ }
+
+ // serve a 1x1 white image
+ if (!appjet.cache.pneTrackingImage) {
+ appjet.cache.pneTrackingImage = image.solidColorImageBlob(1, 1, "ffffff");
+ }
+ blob.serveBlob(appjet.cache.pneTrackingImage);
+}
+
diff --git a/etherpad/src/etherpad/control/pro/account_control.js b/etherpad/src/etherpad/control/pro/account_control.js
new file mode 100644
index 0000000..031dbe6
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/account_control.js
@@ -0,0 +1,369 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("stringutils.*");
+import("funhtml.*");
+import("email.sendEmail");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.helpers");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_account_auto_signin");
+import("etherpad.pro.pro_config");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.collab.collab_server");
+
+function onRequest() {
+ if (!getSession().tempFormData) {
+ getSession().tempFormData = {};
+ }
+
+ return false; // path not handled here
+}
+
+//--------------------------------------------------------------------------------
+// helpers
+//--------------------------------------------------------------------------------
+
+function _redirOnError(m, clearQuery) {
+ if (m) {
+ getSession().accountFormError = m;
+
+ var dest = request.url;
+ if (clearQuery) {
+ dest = request.path;
+ }
+ response.redirect(dest);
+ }
+}
+
+function setSigninNotice(m) {
+ getSession().accountSigninNotice = m;
+}
+
+function setSessionError(m) {
+ getSession().accountFormError = m;
+}
+
+function _topDiv(id, name) {
+ var m = getSession()[name];
+ if (m) {
+ delete getSession()[name];
+ return DIV({id: id}, m);
+ } else {
+ return '';
+ }
+}
+
+function _messageDiv() { return _topDiv('account-message', 'accountMessage'); }
+function _errorDiv() { return _topDiv('account-error', 'accountFormError'); }
+function _signinNoticeDiv() { return _topDiv('signin-notice', 'accountSigninNotice'); }
+
+function _renderTemplate(name, data) {
+ data.messageDiv = _messageDiv;
+ data.errorDiv = _errorDiv;
+ data.signinNotice = _signinNoticeDiv;
+ data.tempFormData = getSession().tempFormData;
+ renderFramed('pro/account/'+name+'.ejs', data);
+}
+
+//----------------------------------------------------------------
+// /ep/account/
+//----------------------------------------------------------------
+
+function render_main_get() {
+ _renderTemplate('my-account', {
+ account: getSessionProAccount(),
+ changePass: getSession().changePass
+ });
+}
+
+function render_update_info_get() {
+ response.redirect('/ep/account/');
+}
+
+function render_update_info_post() {
+ var fullName = request.params.fullName;
+ var email = trim(request.params.email);
+
+ getSession().tempFormData.email = email;
+ getSession().tempFormData.fullName = fullName;
+
+ _redirOnError(pro_accounts.validateEmail(email));
+ _redirOnError(pro_accounts.validateFullName(fullName));
+
+ pro_accounts.setEmail(getSessionProAccount(), email);
+ pro_accounts.setFullName(getSessionProAccount(), fullName);
+
+ getSession().accountMessage = "Info updated.";
+ response.redirect('/ep/account/');
+}
+
+function render_update_password_get() {
+ response.redirect('/ep/account/');
+}
+
+function render_update_password_post() {
+ var password = request.params.password;
+ var passwordConfirm = request.params.passwordConfirm;
+
+ if (password != passwordConfirm) { _redirOnError('Passwords did not match.'); }
+
+ _redirOnError(pro_accounts.validatePassword(password));
+
+ pro_accounts.setPassword(getSessionProAccount(), password);
+
+ if (getSession().changePass) {
+ delete getSession().changePass;
+ response.redirect('/');
+ }
+
+ getSession().accountMessage = "Password updated.";
+ response.redirect('/ep/account/');
+}
+
+//--------------------------------------------------------------------------------
+// signin/signout
+//--------------------------------------------------------------------------------
+
+function render_sign_in_get() {
+ if (request.params.uid && request.params.tp) {
+ var m = pro_accounts.authenticateTempSignIn(Number(request.params.uid), request.params.tp);
+ if (m) {
+ getSession().accountFormError = m;
+ response.redirect('/ep/account/');
+ }
+ }
+ if (request.params.instantSigninKey) {
+ _attemptInstantSignin(request.params.instantSigninKey);
+ }
+ if (getSession().recentlySignedOut && getSession().accountFormError) {
+ delete getSession().accountFormError;
+ delete getSession().recentlySignedOut;
+ }
+ // Note: must check isAccountSignedIn before calling checkAutoSignin()!
+ if (pro_accounts.isAccountSignedIn()) {
+ _redirectToPostSigninDestination();
+ }
+ pro_account_auto_signin.checkAutoSignin();
+ var domainRecord = domains.getRequestDomainRecord();
+ var showGuestBox = false;
+ if (request.params.guest && request.params.padId) {
+ showGuestBox = true;
+ }
+ _renderTemplate('signin', {
+ domain: pro_utils.getFullProDomain(),
+ siteName: toHTML(pro_config.getConfig().siteName),
+ email: getSession().tempFormData.email || "",
+ password: getSession().tempFormData.password || "",
+ rememberMe: getSession().tempFormData.rememberMe || false,
+ showGuestBox: showGuestBox,
+ localPadId: request.params.padId
+ });
+}
+
+function _attemptInstantSignin(key) {
+ // See src/etherpad/control/global_pro_account_control.js
+ var email = null;
+ var password = null;
+ syncedWithCache('global_signin_passwords', function(c) {
+ if (c[key]) {
+ email = c[key].email;
+ password = c[key].password;
+ }
+ delete c[key];
+ });
+ getSession().tempFormData.email = email;
+ _redirOnError(pro_accounts.authenticateSignIn(email, password), true);
+}
+
+function render_sign_in_post() {
+ var email = trim(request.params.email);
+ var password = request.params.password;
+
+ getSession().tempFormData.email = email;
+ getSession().tempFormData.rememberMe = request.params.rememberMe;
+
+ _redirOnError(pro_accounts.authenticateSignIn(email, password));
+ pro_account_auto_signin.setAutoSigninCookie(request.params.rememberMe);
+ _redirectToPostSigninDestination();
+}
+
+function render_guest_sign_in_get() {
+ var localPadId = request.params.padId;
+ var domainId = domains.getRequestDomainId();
+ var globalPadId = padutils.makeGlobalId(domainId, localPadId);
+ var userId = padusers.getUserId();
+
+ pro_account_auto_signin.checkAutoSignin();
+ pad_security.clearKnockStatus(userId, globalPadId);
+
+ _renderTemplate('signin-guest', {
+ localPadId: localPadId,
+ errorMessage: getSession().guestAccessError,
+ siteName: toHTML(pro_config.getConfig().siteName),
+ guestName: padusers.getUserName() || ""
+ });
+}
+
+function render_guest_sign_in_post() {
+ function _err(m) {
+ if (m) {
+ getSession().guestAccessError = m;
+ response.redirect(request.url);
+ }
+ }
+ var displayName = request.params.guestDisplayName;
+ var localPadId = request.params.localPadId;
+ if (!(displayName && displayName.length > 0)) {
+ _err("Please enter a display name");
+ }
+ getSession().guestDisplayName = displayName;
+ response.redirect('/ep/account/guest-knock?padId='+encodeURIComponent(localPadId)+
+ "&guestDisplayName="+encodeURIComponent(displayName));
+}
+
+function render_guest_knock_get() {
+ var localPadId = request.params.padId;
+ helpers.addClientVars({
+ localPadId: localPadId,
+ guestDisplayName: request.params.guestDisplayName,
+ padUrl: "http://"+httpHost(request.host)+"/"+localPadId
+ });
+ _renderTemplate('guest-knock', {});
+}
+
+function render_guest_knock_post() {
+ var localPadId = request.params.padId;
+ var displayName = request.params.guestDisplayName;
+ var domainId = domains.getRequestDomainId();
+ var globalPadId = padutils.makeGlobalId(domainId, localPadId);
+ var userId = padusers.getUserId();
+
+ response.setContentType("text/plain; charset=utf-8");
+ // has the knock already been answsered?
+ var currentAnswer = pad_security.getKnockAnswer(userId, globalPadId);
+ if (currentAnswer) {
+ response.write(currentAnswer);
+ } else {
+ collab_server.guestKnock(globalPadId, userId, displayName);
+ response.write("wait");
+ }
+}
+
+function _redirectToPostSigninDestination() {
+ var cont = request.params.cont;
+ if (!cont) { cont = '/'; }
+ response.redirect(cont);
+}
+
+function render_sign_out() {
+ pro_account_auto_signin.setAutoSigninCookie(false);
+ pro_accounts.signOut();
+ delete getSession().padPasswordAuth;
+ getSession().recentlySignedOut = true;
+ response.redirect("/");
+}
+
+//--------------------------------------------------------------------------------
+// create-admin-account (eepnet only)
+//--------------------------------------------------------------------------------
+
+function render_create_admin_account_get() {
+ if (pro_accounts.doesAdminExist()) {
+ renderFramedError("An admin account already exists on this domain.");
+ response.stop();
+ }
+ _renderTemplate('create-admin-account', {});
+}
+
+function render_create_admin_account_post() {
+ var email = trim(request.params.email);
+ var password = request.params.password;
+ var passwordConfirm = request.params.passwordConfirm;
+ var fullName = request.params.fullName;
+
+ getSession().tempFormData.email = email;
+ getSession().tempFormData.fullName = fullName;
+
+ if (password != passwordConfirm) { _redirOnError('Passwords did not match.'); }
+
+ _redirOnError(pro_accounts.validateEmail(email));
+ _redirOnError(pro_accounts.validateFullName(fullName));
+ _redirOnError(pro_accounts.validatePassword(password));
+
+ pro_accounts.createNewAccount(null, fullName, email, password, true);
+
+ var u = pro_accounts.getAccountByEmail(email, null);
+
+ // TODO: should we send a welcome email here?
+ //pro_accounts.sendWelcomeEmail(u);
+
+ _redirOnError(pro_accounts.authenticateSignIn(email, password));
+
+ response.redirect("/");
+}
+
+
+//--------------------------------------------------------------------------------
+// forgot password
+//--------------------------------------------------------------------------------
+
+function render_forgot_password_get() {
+ if (request.params.instantSubmit && request.params.email) {
+ render_forgot_password_post();
+ } else {
+ _renderTemplate('forgot-password', {
+ email: getSession().tempFormData.email || ""
+ });
+ }
+}
+
+function render_forgot_password_post() {
+ var email = trim(request.params.email);
+
+ getSession().tempFormData.email = email;
+
+ var u = pro_accounts.getAccountByEmail(email, null);
+ if (!u) {
+ _redirOnError("Account not found: "+email);
+ }
+
+ var tempPass = stringutils.randomString(10);
+ pro_accounts.setTempPassword(u, tempPass);
+
+ var subj = "EtherPad: Request to reset your password on "+request.domain;
+ var body = renderTemplateAsString('pro/account/forgot-password-email.ejs', {
+ account: u,
+ recoverUrl: pro_accounts.getTempSigninUrl(u, tempPass)
+ });
+ var fromAddr = pro_utils.getEmailFromAddr();
+ sendEmail(u.email, fromAddr, subj, {}, body);
+
+ getSession().accountMessage = "An email has been sent to "+u.email+" with instructions to reset the password.";
+ response.redirect(request.path);
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/account_manager_control.js b/etherpad/src/etherpad/control/pro/admin/account_manager_control.js
new file mode 100644
index 0000000..8f93b2e
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/account_manager_control.js
@@ -0,0 +1,260 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("stringutils");
+import("stringutils.*");
+import("email.sendEmail");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+
+import("etherpad.control.pro.admin.pro_admin_control");
+
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_config");
+import("etherpad.pro.domains");
+import("etherpad.billing.team_billing");
+
+jimport("java.lang.System.out.println");
+
+function _err(m) {
+ if (m) {
+ getSession().accountManagerError = m;
+ response.redirect(request.path);
+ }
+}
+
+function _renderTopDiv(mid, htmlId) {
+ var m = getSession()[mid];
+ if (m) {
+ delete getSession()[mid];
+ return DIV({id: htmlId}, m);
+ } else {
+ return '';
+ }
+}
+
+function _errorDiv() { return _renderTopDiv('accountManagerError', 'error-message'); }
+function _messageDiv() { return _renderTopDiv('accountManagerMessage', 'message'); }
+function _warningDiv() { return _renderTopDiv('accountManagerWarning', 'warning'); }
+
+function onRequest() {
+ var parts = request.path.split('/');
+
+ function dispatchAccountAction(action, handlerGet, handlerPost) {
+ if ((parts[4] == action) && (isNumeric(parts[5]))) {
+ if (request.isGet) { handlerGet(+parts[5]); }
+ if (request.isPost) { handlerPost(+parts[5]); }
+ return true;
+ }
+ return false;
+ }
+
+ if (dispatchAccountAction('account', render_account_get, render_account_post)) {
+ return true;
+ }
+ if (dispatchAccountAction('delete-account', render_delete_account_get, render_delete_account_post)) {
+ return true;
+ };
+
+ return false;
+}
+
+function render_main() {
+ var accountList = pro_accounts.listAllDomainAccounts();
+ pro_admin_control.renderAdminPage('account-manager', {
+ accountList: accountList,
+ messageDiv: _messageDiv,
+ warningDiv: _warningDiv
+ });
+}
+
+function render_new_get() {
+ pro_admin_control.renderAdminPage('new-account', {
+ oldData: getSession().accountManagerFormData || {},
+ stringutils: stringutils,
+ errorDiv: _errorDiv
+ });
+}
+
+function _ensureBillingOK() {
+ var activeAccounts = pro_accounts.getCachedActiveCount(domains.getRequestDomainId());
+ if (activeAccounts < PRO_FREE_ACCOUNTS) {
+ return;
+ }
+
+ var status = team_billing.getDomainStatus(domains.getRequestDomainId());
+ if (!((status == team_billing.CURRENT)
+ || (status == team_billing.PAST_DUE))) {
+ _err(SPAN(
+ "A payment profile is required to create more than ", PRO_FREE_ACCOUNTS,
+ " accounts. ",
+ A({href: "/ep/admin/billing/", id: "billinglink"}, "Manage billing")));
+ }
+}
+
+function render_new_post() {
+ if (request.params.cancel) {
+ response.redirect('/ep/admin/account-manager/');
+ }
+
+ _ensureBillingOK();
+
+ var fullName = request.params.fullName;
+ var email = trim(request.params.email);
+ var tempPass = request.params.tempPass;
+ var makeAdmin = !!request.params.makeAdmin;
+
+ getSession().accountManagerFormData = {
+ fullName: fullName,
+ email: email,
+ tempPass: tempPass,
+ makeAdmin: makeAdmin
+ };
+
+ // validation
+ if (!tempPass) {
+ tempPass = stringutils.randomString(6);
+ }
+
+ _err(pro_accounts.validateEmail(email));
+ _err(pro_accounts.validateFullName(fullName));
+ _err(pro_accounts.validatePassword(tempPass));
+
+ var existingAccount = pro_accounts.getAccountByEmail(email, null);
+ if (existingAccount) {
+ _err("There is already a account with that email address.");
+ }
+
+ pro_accounts.createNewAccount(null, fullName, email, tempPass, makeAdmin);
+ var account = pro_accounts.getAccountByEmail(email, null);
+
+ pro_accounts.setTempPassword(account, tempPass);
+ sendWelcomeEmail(account, tempPass);
+
+ delete getSession().accountManagerFormData;
+ getSession().accountManagerMessage = "Account "+fullName+" ("+email+") created successfully.";
+ response.redirect('/ep/admin/account-manager/');
+}
+
+function sendWelcomeEmail(account, tempPass) {
+ var subj = "Welcome to EtherPad on "+pro_utils.getFullProDomain()+"!";
+ var toAddr = account.email;
+ var fromAddr = pro_utils.getEmailFromAddr();
+
+ var body = renderTemplateAsString('pro/account/account-welcome-email.ejs', {
+ account: account,
+ adminAccount: getSessionProAccount(),
+ signinLink: pro_accounts.getTempSigninUrl(account, tempPass),
+ toEmail: toAddr,
+ siteName: pro_config.getConfig().siteName
+ });
+ try {
+ sendEmail(toAddr, fromAddr, subj, {}, body);
+ } catch (ex) {
+ var d = DIV();
+ d.push(P("Warning: unable to send welcome email."));
+ if (pne_utils.isPNE()) {
+ d.push(P("Perhaps you have not ",
+ A({href: '/ep/admin/pne-config'}, "Configured SMTP on this server", "?")));
+ }
+ getSession().accountManagerWarning = d;
+ }
+}
+
+// Managing a single account.
+function render_account_get(accountId) {
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+ pro_admin_control.renderAdminPage('manage-account', {
+ account: account,
+ errorDiv: _errorDiv,
+ warningDiv: _warningDiv
+ });
+}
+
+function render_account_post(accountId) {
+ if (request.params.cancel) {
+ response.redirect('/ep/admin/account-manager/');
+ }
+ var newFullName = request.params.newFullName;
+ var newEmail = request.params.newEmail;
+ var newIsAdmin = !!request.params.newIsAdmin;
+
+ _err(pro_accounts.validateEmail(newEmail));
+ _err(pro_accounts.validateFullName(newFullName));
+
+ if ((!newIsAdmin) && (accountId == getSessionProAccount().id)) {
+ _err("You cannot remove your own administrator privileges.");
+ }
+
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+
+ pro_accounts.setEmail(account, newEmail);
+ pro_accounts.setFullName(account, newFullName);
+ pro_accounts.setIsAdmin(account, newIsAdmin);
+
+ getSession().accountManageMessage = "Info updated.";
+ response.redirect('/ep/admin/account-manager/');
+}
+
+function render_delete_account_get(accountId) {
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+ pro_admin_control.renderAdminPage('delete-account', {
+ account: account,
+ errorDiv: _errorDiv
+ });
+}
+
+function render_delete_account_post(accountId) {
+ if (request.params.cancel) {
+ response.redirect("/ep/admin/account-manager/account/"+accountId);
+ }
+
+ if (accountId == getSessionProAccount().id) {
+ getSession().accountManagerError = "You cannot delete your own account.";
+ response.redirect("/ep/admin/account-manager/account/"+accountId);
+ }
+
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+
+ pro_accounts.setDeleted(account);
+ getSession().accountManagerMessage = "The account "+account.fullName+" <"+account.email+"> has been deleted.";
+ response.redirect("/ep/admin/account-manager/");
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/license_manager_control.js b/etherpad/src/etherpad/control/pro/admin/license_manager_control.js
new file mode 100644
index 0000000..ca6d6a6
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/license_manager_control.js
@@ -0,0 +1,128 @@
+/**
+ * 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.
+ */
+
+import("fileutils.writeRealFile");
+import("stringutils");
+
+import("etherpad.licensing");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.pne.pne_utils");
+
+import("etherpad.control.pro.admin.pro_admin_control");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+// license manager
+//----------------------------------------------------------------
+
+function getPath() {
+ return '/ep/admin/pne-license-manager/';
+}
+
+function _getTemplateData(data) {
+ var licenseInfo = licensing.getLicense();
+ data.licenseInfo = licenseInfo;
+ data.isUnlicensed = !licenseInfo;
+ data.isEvaluation = licensing.isEvaluation();
+ data.isExpired = licensing.isExpired();
+ data.isTooOld = licensing.isVersionTooOld();
+ data.errorMessage = (getSession().errorMessage || null);
+ data.runningVersionString = pne_utils.getVersionString();
+ data.licenseVersionString = licensing.getVersionString();
+ return data;
+}
+
+function render_main_get() {
+ licensing.reloadLicense();
+ var licenseInfo = licensing.getLicense();
+ if (!licenseInfo || licensing.isExpired()) {
+ response.redirect(getPath()+'edit');
+ }
+
+ pro_admin_control.renderAdminPage('pne-license-manager',
+ _getTemplateData({edit: false}));
+}
+
+function render_edit_get() {
+ licensing.reloadLicense();
+
+ if (request.params.btn) { response.redirect(request.path); }
+
+ var licenseInfo = licensing.getLicense();
+ var oldData = getSession().oldLicenseData;
+ if (!oldData) {
+ oldData = {};
+ if (licenseInfo) {
+ oldData.orgName = licenseInfo.organizationName;
+ oldData.personName = licenseInfo.personName;
+ }
+ }
+
+ pro_admin_control.renderAdminPage('pne-license-manager',
+ _getTemplateData({edit: true, oldData: oldData}));
+
+ delete getSession().errorMessage;
+}
+
+function render_edit_post() {
+ pne_utils.enableTrackingAgain();
+
+ function _trim(s) {
+ if (!s) { return ''; }
+ return stringutils.trim(s);
+ }
+ function _clean(s) {
+ s = s.replace(/\W/g, '');
+ s = s.replace(/\+/g, '');
+ return s;
+ }
+
+ if (request.params.cancel) {
+ delete getSession().oldLicenseData;
+ response.redirect(getPath());
+ }
+
+ var personName = _trim(request.params.personName);
+ var orgName = _trim(request.params.orgName);
+ var licenseString = _clean(request.params.licenseString);
+
+ getSession().oldLicenseData = {
+ personName: personName, orgName: orgName, licenseString: licenseString};
+
+ var key = [personName,orgName,licenseString].join(":");
+ println("validating key [ "+key+" ]");
+
+ if (!licensing.isValidKey(key)) {
+ getSession().errorMessage = "Invalid License Key";
+ response.redirect(request.path);
+ }
+
+ // valid key. write to disk.
+ var writeSuccess = false;
+ try {
+ println("writing key file: ./data/license.key");
+ writeRealFile("./data/license.key", key);
+ writeSuccess = true;
+ } catch (ex) {
+ println("exception: "+ex);
+ getSession().errorMessage = "Failed to write key to disk. (Do you have permission to write ./data/license.key ?).";
+ }
+ response.redirect(getPath());
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js b/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js
new file mode 100644
index 0000000..51d6ba3
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js
@@ -0,0 +1,280 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("funhtml.*");
+import("dispatch.{Dispatcher,DirMatcher,forward}");
+
+import("etherpad.licensing");
+import("etherpad.control.admincontrol");
+import("etherpad.control.pro.admin.license_manager_control");
+import("etherpad.control.pro.admin.account_manager_control");
+import("etherpad.control.pro.admin.pro_config_control");
+import("etherpad.control.pro.admin.team_billing_control");
+
+import("etherpad.pad.padutils");
+
+import("etherpad.admin.shell");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.utils.*");
+
+//----------------------------------------------------------------
+
+var _pathPrefix = '/ep/admin/';
+
+var _PRO = 1;
+var _PNE_ONLY = 2;
+var _ONDEMAND_ONLY = 3;
+
+function _getLeftnavItems() {
+ var nav = [
+ _PRO, [
+ [_PRO, null, "Admin"],
+ [_PNE_ONLY, "pne-dashboard", "Server Dashboard"],
+ [_PRO, "account-manager/", "Manage Accounts"],
+ [_PRO, "recover-padtext", "Recover Pad Text"],
+ [_PRO, null, "Configuration"],
+ [_PRO, [[_PNE_ONLY, "pne-config", "Private Server Configuration"],
+ [_PRO, "pro-config", "Application Configuration"]]],
+ ]
+ ];
+ return nav;
+}
+
+function renderAdminLeftNav() {
+ function _make(x) {
+ if ((x[0] == _PNE_ONLY) && !pne_utils.isPNE()) {
+ return null;
+ }
+ if ((x[0] == _ONDEMAND_ONLY) && pne_utils.isPNE()) {
+ return null;
+ }
+
+ if (x[1] instanceof Array) {
+ return _makelist(x[1]);
+ } else {
+ return _makeitem(x);
+ }
+ }
+ var selected;
+ function _makeitem(x) {
+ if (x[1]) {
+ var p = x[1];
+ if (x[1].charAt(0) != '/') {
+ p = _pathPrefix+p;
+ }
+ var li = LI(A({href: p}, x[2]));
+ if (stringutils.startsWith(request.path, p)) {
+ // select the longest prefix match.
+ if (! selected || p.length > selected.path.length) {
+ selected = {path: p, li: li};
+ }
+ }
+ return li;
+ } else {
+ return LI(DIV({className: 'leftnav-title'}, x[2]));
+ }
+ }
+ function _makelist(x) {
+ var ul = UL();
+ x.forEach(function(y) {
+ var t = _make(y);
+ if (t) { ul.push(t); }
+ });
+ return ul;
+ }
+ var d = DIV(_make(_getLeftnavItems()));
+ if (selected) {
+ selected.li.attribs.className = "selected";
+ }
+ // leftnav looks stupid when it's not very tall.
+ for (var i = 0; i < 10; i++) { d.push(BR()); }
+ return d;
+}
+
+function renderAdminPage(p, data) {
+ appjet.requestCache.proTopNavSelection = 'admin';
+ function getAdminContent() {
+ if (typeof(p) == 'function') {
+ return p();
+ } else {
+ return renderTemplateAsString('pro/admin/'+p+'.ejs', data);
+ }
+ }
+ renderFramed('pro/admin/admin-template.ejs', {
+ getAdminContent: getAdminContent,
+ renderAdminLeftNav: renderAdminLeftNav,
+ validLicense: pne_utils.isServerLicensed(),
+ });
+}
+
+//----------------------------------------------------------------
+
+function onRequest() {
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [DirMatcher(license_manager_control.getPath()), forward(license_manager_control)],
+ [DirMatcher('/ep/admin/account-manager/'), forward(account_manager_control)],
+ [DirMatcher('/ep/admin/pro-config/'), forward(pro_config_control)],
+ [DirMatcher('/ep/admin/billing/'), forward(team_billing_control)],
+ ]);
+
+ if (disp.dispatch()) {
+ return true;
+ }
+
+ // request will be handled by this module.
+ pro_accounts.requireAdminAccount();
+}
+
+function render_main() {
+// renderAdminPage('admin');
+ response.redirect('/ep/admin/account-manager/')
+}
+
+function render_pne_dashboard() {
+ renderAdminPage('pne-dashboard', {
+ renderUptime: admincontrol.renderServerUptime,
+ renderResponseCodes: admincontrol.renderResponseCodes,
+ renderPadConnections: admincontrol.renderPadConnections,
+ renderTransportStats: admincontrol.renderCometStats,
+ todayActiveUsers: licensing.getActiveUserCount(),
+ userQuota: licensing.getActiveUserQuota()
+ });
+}
+
+var _documentedServerOptions = [
+ 'listen',
+ 'listenSecure',
+ 'transportUseWildcardSubdomains',
+ 'sslKeyStore',
+ 'sslKeyPassword',
+ 'etherpad.soffice',
+ 'etherpad.adminPass',
+ 'etherpad.SQL_JDBC_DRIVER',
+ 'etherpad.SQL_JDBC_URL',
+ 'etherpad.SQL_USERNAME',
+ 'etherpad.SQL_PASSWORD',
+ 'smtpServer',
+ 'smtpUser',
+ 'smtpPass',
+ 'configFile',
+ 'etherpad.licenseKey',
+ 'verbose'
+];
+
+function render_pne_config_get() {
+ renderAdminPage('pne-config', {
+ propKeys: _documentedServerOptions,
+ appjetConfig: appjet.config
+ });
+}
+
+function render_pne_advanced_get() {
+ response.redirect("/ep/admin/shell");
+}
+
+function render_shell_get() {
+ if (!(pne_utils.isPNE() || sessions.isAnEtherpadAdmin())) {
+ return false;
+ }
+ appjet.requestCache.proTopNavSelection = 'admin';
+ renderAdminPage('pne-shell', {
+ oldCmd: getSession().pneAdminShellCmd,
+ result: getSession().pneAdminShellResult,
+ elapsedMs: getSession().pneAdminShellElapsed
+ });
+ delete getSession().pneAdminShellResult;
+ delete getSession().pneAdminShellElapsed;
+}
+
+function render_shell_post() {
+ if (!(pne_utils.isPNE() || sessions.isAnEtherpadAdmin())) {
+ return false;
+ }
+ var cmd = request.params.cmd;
+ var start = +(new Date);
+ getSession().pneAdminShellCmd = cmd;
+ getSession().pneAdminShellResult = shell.getResult(cmd);
+ getSession().pneAdminShellElapsed = +(new Date) - start;
+ response.redirect(request.path);
+}
+
+function render_recover_padtext_get() {
+ function getNumRevisions(localPadId) {
+ return padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) { return null; }
+ return 1+pad.getHeadRevisionNumber();
+ });
+ }
+ function getPadText(localPadId, revNum) {
+ return padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) { return null; }
+ return pad.getRevisionText(revNum);
+ });
+ }
+
+ var localPadId = request.params.localPadId;
+ var revNum = request.params.revNum;
+
+ var d = DIV({style: "font-size: .8em;"});
+
+ d.push(FORM({action: request.path, method: "get"},
+ P({style: "margin-top: 0;"}, LABEL("Pad ID: "),
+ INPUT({type: "text", name: "localPadId", value: localPadId || ""}),
+ INPUT({type: "submit", value: "Submit"}))));
+
+ var showPadHelp = false;
+ var revisions = null;
+
+ if (!localPadId) {
+ showPadHelp = true;
+ } else {
+ revisions = getNumRevisions(localPadId);
+ if (!revisions) {
+ d.push(P("Pad not found: "+localPadId));
+ } else {
+ d.push(P(B(localPadId), " has ", revisions, " revisions."));
+ d.push(P("Enter a revision number (0-"+revisions+") to recover the pad text for that revision:"));
+ d.push(FORM({action: request.path, method: "get"},
+ P(LABEL("Revision number:"),
+ INPUT({type: "hidden", name: "localPadId", value: localPadId}),
+ INPUT({type: "text", name: "revNum", value: revNum || (revisions - 1)}),
+ INPUT({type: "submit", value: "Submit"}))));
+ }
+ }
+
+ if (showPadHelp) {
+ d.push(P({style: "font-size: 1em; color: #555;"},
+ 'The pad ID is the same as the URL to the pad, without the leading "/".',
+ ' For example, if the pad lives at http://pad.spline.inf.fu-berlin.de/foobar,',
+ ' then the pad ID is "foobar" (without the quotes).'))
+ }
+
+ if (revisions && revNum && (revNum < revisions)) {
+ var padText = getPadText(localPadId, revNum);
+ d.push(P(B("Pad text for ["+localPadId+"] revision #"+revNum)));
+ d.push(DIV({style: "font-family: monospace; border: 1px solid #ccc; background: #ffe; padding: 1em;"}, padText));
+ }
+
+ renderAdminPage(function() { return d; });
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/pro_config_control.js b/etherpad/src/etherpad/control/pro/admin/pro_config_control.js
new file mode 100644
index 0000000..b03da45
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/pro_config_control.js
@@ -0,0 +1,54 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+
+import("etherpad.sessions.getSession");
+import("etherpad.control.pro.admin.pro_admin_control");
+import("etherpad.pro.pro_config");
+
+function _renderTopDiv(mid, htmlId) {
+ var m = getSession()[mid];
+ if (m) {
+ delete getSession()[mid];
+ return DIV({id: htmlId}, m);
+ } else {
+ return '';
+ }
+}
+
+function _messageDiv() {
+ return _renderTopDiv('proConfigMessage', 'pro-config-message');
+}
+
+function render_main_get() {
+ pro_config.reloadConfig();
+ var config = pro_config.getConfig();
+ pro_admin_control.renderAdminPage('pro-config', {
+ config: config,
+ messageDiv: _messageDiv
+ });
+}
+
+function render_main_post() {
+ pro_config.setConfigVal('siteName', request.params.siteName);
+ pro_config.setConfigVal('alwaysHttps', !!request.params.alwaysHttps);
+ pro_config.setConfigVal('defaultPadText', request.params.defaultPadText);
+ getSession().proConfigMessage = "New settings applied.";
+ response.redirect(request.path);
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/team_billing_control.js b/etherpad/src/etherpad/control/pro/admin/team_billing_control.js
new file mode 100644
index 0000000..5be6a0e
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/team_billing_control.js
@@ -0,0 +1,447 @@
+/**
+ * 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.
+ */
+
+import("dateutils");
+import("email.sendEmail");
+import("fastJSON");
+import("funhtml.*");
+import("jsutils.*");
+import("sqlbase.sqlcommon.inTransaction");
+import("stringutils.*");
+
+import("etherpad.billing.billing");
+import("etherpad.billing.fields");
+import("etherpad.billing.team_billing");
+import("etherpad.control.pro.admin.pro_admin_control");
+import("etherpad.globals");
+import("etherpad.helpers");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_utils");
+import("etherpad.sessions");
+import("etherpad.store.checkout");
+import("etherpad.utils.*");
+
+import("static.js.billing_shared.{billing=>billingJS}");
+
+var billingButtonName = "Confirm"
+
+function _cart() {
+ var s = sessions.getSession();
+ if (! s.proBillingCart) {
+ s.proBillingCart = {};
+ }
+ return s.proBillingCart;
+}
+
+function _billingForm() {
+ return renderTemplateAsString('store/eepnet-checkout/billing-info.ejs', {
+ cart: _cart(),
+ billingButtonName: billingButtonName,
+ billingFinalPhrase: "",
+ helpers: helpers,
+ errorIfInvalid: _errorIfInvalid,
+ billing: billingJS,
+ obfuscateCC: checkout.obfuscateCC,
+ dollars: checkout.dollars,
+ countryList: fields.countryList,
+ usaStateList: fields.usaStateList,
+ getFullSuperdomainHost: pro_utils.getFullSuperdomainHost,
+ showCouponCode: true,
+ });
+}
+
+function _plural(num) {
+ return (num == 1 ? "" : "s");
+}
+
+function _billingSummary(domainId, subscription) {
+ var paymentInfo = team_billing.getRecurringBillingInfo(domainId);
+ if (! paymentInfo) {
+ return;
+ }
+ var latestInvoice = team_billing.getLatestPaidInvoice(subscription.id);
+ var usersSoFar = team_billing.getMaxUsers(domainId);
+ var costSoFar = team_billing.calculateSubscriptionCost(usersSoFar, subscription.coupon);
+
+ var lastPaymentString =
+ (latestInvoice ?
+ "US $"+checkout.dollars(billing.centsToDollars(latestInvoice.amt))+
+ " ("+latestInvoice.users+" account"+_plural(latestInvoice.users)+")"+
+ ", on "+checkout.formatDate(latestInvoice.time) :
+ "None");
+
+ var coupon = false;
+ if (subscription.coupon) {
+ println("has a coupon: "+subscription.coupon);
+ var cval = team_billing.getCouponValue(subscription.coupon);
+ coupon = [];
+ if (cval.freeUsers) {
+ coupon.push(cval.freeUsers+" free user"+(cval.freeUsers == 1 ? "" : "s"));
+ }
+ if (cval.pctDiscount) {
+ coupon.push(cval.pctDiscount+"% savings");
+ }
+ coupon = coupon.join(", ");
+ }
+
+ return {
+ fullName: paymentInfo.fullname,
+ paymentSummary:
+ paymentInfo.paymentsummary +
+ (paymentInfo.expiration ?
+ ", expires "+checkout.formatExpiration(paymentInfo.expiration) :
+ ""),
+ lastPayment: lastPaymentString,
+ nextPayment: checkout.formatDate(subscription.paidThrough),
+ maxUsers: usersSoFar,
+ estimatedPayment: "US $"+checkout.dollars(costSoFar),
+ coupon: coupon
+ }
+}
+
+function _statusMessage() {
+ if (_cart().statusMessage) {
+ return toHTML(P({style: "color: green;"}, _cart().statusMessage));
+ } else {
+ return '';
+ }
+}
+
+function renderMainPage(doEdit) {
+ var cart = _cart();
+ var domainId = domains.getRequestDomainId();
+ var subscription = team_billing.getSubscriptionForCustomer(domainId);
+ var pendingInvoice = team_billing.getLatestPendingInvoice(domainId)
+ var usersSoFar = team_billing.getMaxUsers(domainId);
+ var costSoFar = team_billing.calculateSubscriptionCost(usersSoFar, subscription && subscription.coupon);
+
+ checkout.guessBillingNames(cart, pro_accounts.getSessionProAccount().fullName);
+ if (! cart.billingReferralCode) {
+ if (subscription && subscription.coupon) {
+ cart.billingReferralCode = subscription.coupon;
+ }
+ }
+
+ var summary = _billingSummary(domainId, subscription);
+ if (! summary) {
+ doEdit = true;
+ }
+
+ pro_admin_control.renderAdminPage('manage-billing', {
+ billingForm: _billingForm,
+ doEdit: doEdit,
+ paymentInfo: summary,
+ getFullSuperdomainHost: pro_utils.getFullSuperdomainHost,
+ firstCharge: checkout.formatDate(subscription ? subscription.paidThrough : dateutils.nextMonth(new Date)),
+ billingButtonName: billingButtonName,
+ errorDiv: _errorDiv,
+ showBackButton: (summary != undefined),
+ statusMessage: _statusMessage,
+ isBehind: (subscription ? subscription.paidThrough < Date.now() - 86400*1000 : false),
+ amountDue: "US $"+checkout.dollars(billing.centsToDollars(pendingInvoice ? pendingInvoice.amt : costSoFar*100)),
+ cart: _cart()
+ });
+
+ delete _cart().errorId;
+ delete _cart().errorMsg;
+ delete _cart().statusMessage;
+}
+
+function render_main() {
+ renderMainPage(false);
+}
+
+function render_edit() {
+ renderMainPage(true);
+}
+
+function _errorDiv() {
+ var m = _cart().errorMsg;
+ if (m) {
+ return DIV({className: 'errormsg', id: 'errormsg'}, m);
+ } else {
+ return '';
+ }
+}
+
+function _validationError(id, errorMessage) {
+ var cart = _cart();
+ cart.errorMsg = errorMessage;
+ cart.errorId = {};
+ if (id instanceof Array) {
+ id.forEach(function(k) {
+ cart.errorId[k] = true;
+ });
+ } else {
+ cart.errorId[id] = true;
+ }
+ response.redirect('/ep/admin/billing/edit');
+}
+
+function _errorIfInvalid(id) {
+ var cart = _cart();
+ if (cart.errorId && cart.errorId[id]) {
+ return 'error';
+ } else {
+ return '';
+ }
+}
+
+function paypalNotifyUrl() {
+ return request.scheme+"://"+pro_utils.getFullSuperdomainHost()+"/ep/store/paypalnotify";
+}
+
+function _paymentSummary(payInfo) {
+ return payInfo.cardType + " ending in " + payInfo.cardNumber.substr(-4);
+}
+
+function _expiration(payInfo) {
+ return payInfo.cardExpiration;
+}
+
+function _attemptAuthorization(success_f) {
+ var cart = _cart();
+ var domain = domains.getRequestDomainRecord();
+ var domainId = domain.id;
+ var domainName = domain.subDomain;
+ var payInfo = checkout.generatePayInfo(cart);
+ var proAccount = pro_accounts.getSessionProAccount();
+ var fullName = cart.billingFirstName+" "+cart.billingLastName;
+ var email = proAccount.email;
+
+ // PCI rules require that we not store the CVV longer than necessary to complete the transaction
+ var savedCvv = payInfo.cardCvv;
+ delete payInfo.cardCvv;
+ checkout.writeToEncryptedLog(fastJSON.stringify({date: String(new Date()), domain: domain, payInfo: payInfo}));
+ payInfo.cardCvv = savedCvv;
+
+ var result = billing.authorizePurchase(payInfo, paypalNotifyUrl());
+ if (result.status == 'success') {
+ billing.log({type: 'new-subscription',
+ name: fullName,
+ domainId: domainId,
+ domainName: domainName});
+ success_f(result);
+ } else if (result.status == 'pending') {
+ _validationError('', "Your authorization is pending. When it clears, your account will be activated. "+
+ "You may choose to pay by different means now, or wait until your authorization clears.");
+ } else if (result.status == 'failure') {
+ var paypalResult = result.debug;
+ billing.log({'type': 'FATAL', value: "Direct purchase failed on paypal.", cart: cart, paypal: paypalResult});
+ checkout.validateErrorFields(_validationError, "There seems to be an error in your billing information."+
+ " Please verify and correct your ",
+ result.errorField.userErrors);
+ checkout.validateErrorFields(_validationError, "The bank declined your billing information. Please try a different ",
+ result.errorField.permanentErrors);
+ _validationError('', "A temporary error has prevented processing of your payment. Please try again later.");
+ } else {
+ billing.log({'type': 'FATAL', value: "Unknown error: "+result.status+" - debug: "+result.debug});
+ sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'UNKNOWN ERROR WARNING!', {},
+ "Hey,\n\nThis is a billing system error. Some unknown error occurred. "+
+ "This shouldn't ever happen. Probably good to let J.D. know. <grin>\n\n"+
+ fastJSON.stringify(cart));
+ _validationError('', "An unknown error occurred. We're looking into it!")
+ }
+}
+
+function _processNewSubscription() {
+ _attemptAuthorization(function(result) {
+ var domain = domains.getRequestDomainRecord();
+ var domainId = domain.id;
+ var domainName = domain.subDomain;
+
+ var cart = _cart();
+ var payInfo = checkout.generatePayInfo(cart);
+ var proAccount = pro_accounts.getSessionProAccount();
+ var fullName = cart.billingFirstName+" "+cart.billingLastName;
+ var email = proAccount.email;
+
+ inTransaction(function() {
+
+ var subscriptionId = team_billing.createSubscription(domainId, cart.billingReferralCode);
+
+ team_billing.setRecurringBillingInfo(
+ domainId,
+ fullName,
+ email,
+ _paymentSummary(payInfo),
+ _expiration(payInfo),
+ result.purchaseInfo.paypalId);
+ });
+
+ if (globals.isProduction()) {
+ sendEmail('sales@pad.spline.inf.fu-berlin.de', 'sales@pad.spline.inf.fu-berlin.de', "EtherPad: New paid pro account for "+fullName, {},
+ "This is an automatic notification.\n\n"+fullName+" ("+email+") successfully set up "+
+ "a billing profile for domain: "+domainName+".");
+ }
+ });
+}
+
+function _updateExistingSubscription(subscription) {
+ var cart = _cart();
+
+ _attemptAuthorization(function(result) {
+ inTransaction(function() {
+ var cart = _cart();
+ var domain = domains.getRequestDomainId();
+ var payInfo = checkout.generatePayInfo(cart);
+ var proAccount = pro_accounts.getSessionProAccount();
+ var fullName = cart.billingFirstName+" "+cart.billingLastName;
+ var email = proAccount.email;
+
+ var subscriptionId = subscription.id;
+
+ team_billing.setRecurringBillingInfo(
+ domain,
+ fullName,
+ email,
+ _paymentSummary(payInfo),
+ _expiration(payInfo),
+ result.purchaseInfo.paypalId);
+ });
+ });
+
+ if (subscription.paidThrough < new Date) {
+ // if they're behind, do the purchase!
+ if (team_billing.processSubscription(subscription)) {
+ cart.statusMessage = "Your payment was successful, and your account is now up to date! You will receive a receipt by email."
+ } else {
+ cart.statusMessage = "Your payment failed; you will receive further instructions by email.";
+ }
+ }
+}
+
+function _processBillingInfo() {
+ var cart = _cart();
+ var domain = domains.getRequestDomainId();
+
+ var subscription = team_billing.getSubscriptionForCustomer(domain);
+ if (! subscription) {
+ _processNewSubscription();
+ response.redirect('/ep/admin/billing/');
+ } else {
+ team_billing.updateSubscriptionCouponCode(subscription.id, cart.billingReferralCode);
+ if (cart.billingCCNumber.length > 0) {
+ _updateExistingSubscription(subscription);
+ }
+ response.redirect('/ep/admin/billing')
+ }
+}
+
+function _processPaypalPurchase() {
+ var domain = domains.getRequestDomainId();
+ billing.log({type: "paypal-attempt",
+ domain: domain,
+ message: "Someone tried to use paypal to pay for on-demand."+
+ " They got an error message. If this happens a lot, we should implement paypal."})
+ java.lang.Thread.sleep(5000);
+ _validationError('billingPurchaseType', "There was an error contacting PayPal. Please try another payment type.")
+}
+
+function _processInvoicePurchase() {
+ var output = [
+ "Name: "+cart.billingFirstName+" "+cart.billingLastName,
+ "\nAddress: ",
+ cart.billingAddressLine1+(cart.billingAddressLine2.length > 0 ? "\n"+cart.billingAddressLine2 : ""),
+ cart.billingCity + ", " + (cart.billingState.length > 0 ? cart.billingState : cart.billingProvince),
+ cart.billingZipCode.length > 0 ? cart.billingZipCode : cart.billingPostalCode,
+ cart.billingCountry,
+ "\nEmail: ",
+ pro_accounts.getSessionProAccount().email
+ ].join("\n");
+ var recipient = (globals.isProduction() ? 'sales@pad.spline.inf.fu-berlin.de' : 'jd@appjet.com');
+ sendEmail(
+ recipient,
+ 'sales@pad.spline.inf.fu-berlin.de',
+ 'Invoice payment request - '+pro_utils.getProRequestSubdomain(),
+ {},
+ "Hi there,\n\nA pro user tried to pay by invoice. Their information follows."+
+ "\n\nThanks!\n\n"+output);
+ _validationError('', "Your information has been sent to our sales department; a salesperson will contact you shortly regarding your invoice request.")
+}
+
+function render_apply() {
+ var cart = _cart();
+ eachProperty(request.params, function(k, v) {
+ if (startsWith(k, "billing")) {
+ if (k == "billingCCNumber" && v.charAt(0) == 'X') { return; }
+ cart[k] = toHTML(v);
+ }
+ });
+
+ if (! request.params.backbutton) {
+ var allPaymentFields = ["billingCCNumber", "billingExpirationMonth", "billingExpirationYear", "billingCSC", "billingAddressLine1", "billingAddressLine2", "billingCity", "billingState", "billingZipCode", "billingProvince", "billingPostalCode"];
+ var allBlank = true;
+ allPaymentFields.forEach(function(field) { if (cart[field].length > 0) { allBlank = false; }});
+ if (! allBlank) {
+ checkout.validateBillingCart(_validationError, cart);
+ }
+ } else {
+ response.redirect("/ep/admin/billing/");
+ }
+
+ var couponCode = cart.billingReferralCode;
+
+ if (couponCode.length != 0 && (couponCode.length != 8 || ! team_billing.getCouponValue(couponCode))) {
+ _validationError('billingReferralCode', 'Invalid referral code entered. Please verify your code and try again.');
+ }
+
+ if (cart.billingPurchaseType == 'paypal') {
+ _processPaypalPurchase();
+ } else if (cart.billingPurchaseType == 'invoice') {
+ _processInvoicePurchase();
+ }
+
+ _processBillingInfo();
+}
+
+function handlePaypalNotify() {
+ // XXX: handle delayed paypal authorization
+}
+
+function render_invoices() {
+ if (request.params.id) {
+ var purchaseId = team_billing.getSubscriptionForCustomer(domains.getRequestDomainId()).id;
+ var invoice = billing.getInvoice(request.params.id);
+ if (invoice.purchase != purchaseId) {
+ response.redirect(request.path);
+ }
+
+ var transaction;
+ var adjustments = billing.getAdjustments(invoice.id);
+ if (adjustments.length == 1) {
+ transaction = billing.getTransaction(adjustments[0].transaction);
+ }
+
+ pro_admin_control.renderAdminPage('single-invoice', {
+ formatDate: checkout.formatDate,
+ dollars: checkout.dollars,
+ centsToDollars: billing.centsToDollars,
+ invoice: invoice,
+ transaction: transaction
+ });
+ } else {
+ var invoices = team_billing.getAllInvoices(domains.getRequestDomainId());
+
+ pro_admin_control.renderAdminPage('billing-invoices', {
+ invoices: invoices,
+ formatDate: checkout.formatDate,
+ dollars: checkout.dollars,
+ centsToDollars: billing.centsToDollars
+ });
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/pro/pro_main_control.js b/etherpad/src/etherpad/control/pro/pro_main_control.js
new file mode 100644
index 0000000..b4e3bc4
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/pro_main_control.js
@@ -0,0 +1,150 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("dispatch.{Dispatcher,DirMatcher,forward}");
+import("funhtml.*");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.helpers");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+import("etherpad.licensing");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.pro_padlist");
+
+import("etherpad.control.pro.account_control");
+import("etherpad.control.pro.pro_padlist_control");
+import("etherpad.control.pro.admin.pro_admin_control");
+import("etherpad.control.pro.admin.account_manager_control");
+
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+
+
+function onRequest() {
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [DirMatcher('/ep/account/'), forward(account_control)],
+ [DirMatcher('/ep/admin/'), forward(pro_admin_control)],
+ [DirMatcher('/ep/padlist/'), forward(pro_padlist_control)],
+ ]);
+ return disp.dispatch();
+}
+
+function render_main() {
+ if (request.path == '/ep/') {
+ response.redirect('/');
+ }
+
+ // recent pad list
+ var livePads = pro_pad_db.listLiveDomainPads();
+ var recentPads = pro_pad_db.listAllDomainPads();
+
+ var renderLivePads = function() {
+ return pro_padlist.renderPadList(livePads, ['title', 'connectedUsers'], 10);
+ }
+
+ var renderRecentPads = function() {
+ return pro_padlist.renderPadList(recentPads, ['title'], 10);
+ };
+
+ var r = domains.getRequestDomainRecord();
+
+ renderFramed('pro/pro_home.ejs', {
+ isEvaluation: licensing.isEvaluation(),
+ account: getSessionProAccount(),
+ isPNE: pne_utils.isPNE(),
+ pneVersion: pne_utils.getVersionString(),
+ livePads: livePads,
+ recentPads: recentPads,
+ renderRecentPads: renderRecentPads,
+ renderLivePads: renderLivePads,
+ orgName: r.orgName
+ });
+ return true;
+}
+
+function render_finish_activation_get() {
+ if (!isActivationAllowed()) {
+ response.redirect('/');
+ }
+
+ var accountList = pro_accounts.listAllDomainAccounts();
+ if (accountList.length > 1) {
+ response.redirect('/');
+ }
+ if (accountList.length == 0) {
+ throw Error("accountList.length should never be 0.");
+ }
+
+ var acct = accountList[0];
+ var tempPass = stringutils.randomString(10);
+ pro_accounts.setTempPassword(acct, tempPass);
+ account_manager_control.sendWelcomeEmail(acct, tempPass);
+
+ var domainId = domains.getRequestDomainId();
+
+ syncedWithCache('pro-activations', function(c) {
+ delete c[domainId];
+ });
+
+ renderNoticeString(
+ DIV({style: "font-size: 16pt; border: 1px solid green; background: #eeffee; margin: 2em 4em; padding: 1em;"},
+ P("Success! You will receive an email shortly with instructions."),
+ DIV({style: "display: none;", id: "reference"}, acct.id, ":", tempPass)));
+}
+
+function isActivationAllowed() {
+ if (request.path != '/ep/finish-activation') {
+ return false;
+ }
+ var allowed = false;
+ var domainId = domains.getRequestDomainId();
+ return syncedWithCache('pro-activations', function(c) {
+ if (c[domainId]) {
+ return true;
+ }
+ return false;
+ });
+}
+
+function render_payment_required_get() {
+ // Users get to this page when there is a problem with billing:
+ // possibilities:
+ // * they try to create a new account but they have not entered
+ // payment information
+ //
+ // * their credit card lapses and any pro request fails.
+ //
+ // * others?
+
+ var message = getSession().billingProblem || "A payment is required to proceed.";
+ var adminList = pro_accounts.listAllDomainAdmins();
+
+ renderFramed("pro/pro-payment-required.ejs", {
+ message: message,
+ isAdmin: pro_accounts.isAdminSignedIn(),
+ adminList: adminList
+ });
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pro/pro_padlist_control.js b/etherpad/src/etherpad/control/pro/pro_padlist_control.js
new file mode 100644
index 0000000..9a90c67
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/pro_padlist_control.js
@@ -0,0 +1,200 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("jsutils.*");
+import("stringutils");
+
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.helpers");
+import("etherpad.pad.exporthtml");
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_padlist");
+
+jimport("java.lang.System.out.println");
+
+function onRequest(name) {
+ if (name == "all_pads.zip") {
+ render_all_pads_zip_get();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function _getBaseUrl() { return "/ep/padlist/"; }
+
+function _renderPadNav() {
+ var d = DIV({id: "padlist-nav"});
+ var ul = UL();
+ var items = [
+ ['allpads', 'all-pads', "All Pads"],
+ ['mypads', 'my-pads', "My Pads"],
+ ['archivedpads', 'archived-pads', "Archived Pads"]
+ ];
+ for (var i = 0; i < items.length; i++) {
+ var item = items[i];
+ var cn = "";
+ if (request.path.split("/").slice(-1)[0] == item[1]) {
+ cn = "selected";
+ }
+ ul.push(LI(A({id: "nav-"+item[1], href: _getBaseUrl()+item[1], className: cn}, item[2])));
+ }
+ ul.push(html(helpers.clearFloats()));
+ d.push(ul);
+ d.push(FORM({id: "newpadform", method: "get", action: "/ep/pad/newpad"},
+ INPUT({type: "submit", value: "New Pad"})));
+ d.push(html(helpers.clearFloats()));
+ return d;
+}
+
+function _renderPage(name, data) {
+ getSession().latestPadlistView = request.path + "?" + request.query;
+ var r = domains.getRequestDomainRecord();
+ appjet.requestCache.proTopNavSelection = 'padlist';
+ data.renderPadNav = _renderPadNav;
+ data.orgName = r.orgName;
+ data.renderNotice = function() {
+ var m = getSession().padlistMessage;
+ if (m) {
+ delete getSession().padlistMessage;
+ return DIV({className: "padlist-notice"}, m);
+ } else {
+ return "";
+ }
+ };
+
+ renderFramed("pro/padlist/"+name+".ejs", data);
+}
+
+function _renderListPage(padList, showingDesc, columns) {
+ _renderPage("pro-padlist", {
+ padList: padList,
+ renderPadList: function() {
+ return pro_padlist.renderPadList(padList, columns);
+ },
+ renderShowingDesc: function(count) {
+ return DIV({id: "showing-desc"},
+ "Showing "+showingDesc+" ("+count+").");
+ },
+ isAdmin: pro_accounts.isAdminSignedIn()
+ });
+}
+
+function render_main() {
+ if (!getSession().latestPadlistView) {
+ getSession().latestPadlistView = "/ep/padlist/all-pads";
+ }
+ response.redirect(getSession().latestPadlistView);
+}
+
+function render_all_pads_get() {
+ _renderListPage(
+ pro_pad_db.listAllDomainPads(),
+ "all pads",
+ ['secure', 'title', 'lastEditedDate', 'editors', 'actions']);
+}
+
+function render_all_pads_zip_get() {
+ if (! pro_accounts.isAdminSignedIn()) {
+ response.redirect(_getBaseUrl()+"all-pads");
+ }
+ var bytes = new java.io.ByteArrayOutputStream();
+ var zos = new java.util.zip.ZipOutputStream(bytes);
+
+ var pads = pro_pad_db.listAllDomainPads();
+ pads.forEach(function(pad) {
+ var padHtml;
+ var title;
+ padutils.accessPadLocal(pad.localPadId, function(p) {
+ title = padutils.getProDisplayTitle(pad.localPadId, pad.title);
+ padHtml = exporthtml.getPadHTML(p);
+ }, "r");
+
+ title = title.replace(/[^\w\s]/g, "-") + ".html";
+ zos.putNextEntry(new java.util.zip.ZipEntry(title));
+ var padBytes = (new java.lang.String(renderTemplateAsString('pad/exporthtml.ejs', {
+ content: padHtml,
+ pre: false
+ }))).getBytes("UTF-8");
+
+ zos.write(padBytes, 0, padBytes.length);
+ zos.closeEntry();
+ });
+ zos.close();
+ response.setContentType("application/zip");
+ response.writeBytes(bytes.toByteArray());
+}
+
+function render_my_pads_get() {
+ _renderListPage(
+ pro_pad_db.listMyPads(),
+ "pads created by me",
+ ['secure', 'title', 'lastEditedDate', 'editors', 'actions']);
+}
+
+function render_archived_pads_get() {
+ helpers.addClientVars({
+ showingArchivedPads: true
+ });
+ _renderListPage(
+ pro_pad_db.listArchivedPads(),
+ "archived pads",
+ ['secure', 'title', 'lastEditedDate', 'actions']);
+}
+
+function render_edited_by_get() {
+ var editorId = request.params.editorId;
+ var editorName = pro_accounts.getFullNameById(editorId);
+ _renderListPage(
+ pro_pad_db.listPadsByEditor(editorId),
+ "pads edited by "+editorName,
+ ['secure', 'title', 'lastEditedDate', 'editors', 'actions']);
+}
+
+function render_delete_post() {
+ var localPadId = request.params.padIdToDelete;
+
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ propad.markDeleted();
+ getSession().padlistMessage = 'Pad "'+propad.getDisplayTitle()+'" has been deleted.';
+ });
+
+ response.redirect(request.params.returnPath);
+}
+
+function render_toggle_archive_post() {
+ var localPadId = request.params.padIdToToggleArchive;
+
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ if (propad.isArchived()) {
+ propad.unmarkArchived();
+ getSession().padlistMessage = 'Pad "'+propad.getDisplayTitle()+'" has been un-archived.';
+ } else {
+ propad.markArchived();
+ getSession().padlistMessage = 'Pad "'+propad.getDisplayTitle()+'" has been archived. You can view archived pads by clicking on the "Archived" tab at the top of the pad list.';
+ }
+ });
+
+ response.redirect(request.params.returnPath);
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro_beta_control.js b/etherpad/src/etherpad/control/pro_beta_control.js
new file mode 100644
index 0000000..ec99b43
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro_beta_control.js
@@ -0,0 +1,136 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*", "stringutils.*");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("stringutils");
+import("email.sendEmail");
+
+import("etherpad.utils.*");
+import("etherpad.log");
+import("etherpad.sessions.getSession");
+
+jimport("java.lang.System.out.println");
+
+function render_main_get() {
+ if (isValveOpen()) {
+ response.redirect("/ep/pro-signup/");
+ }
+ renderFramed("beta/signup.ejs", {
+ errorMsg: getSession().betaSignupError
+ });
+ delete getSession().betaSignupError;
+}
+
+function render_signup_post() {
+ // record in sql: [id, email, activated=false, activationCode]
+ // log to disk
+
+ var email = request.params.email;
+ if (!isValidEmail(email)) {
+ getSession().betaSignupError = "Invalid email address.";
+ response.redirect('/ep/beta-account/');
+ }
+
+ // does email already exist?
+ if (sqlobj.selectSingle('pro_beta_signups', {email: email})) {
+ getSession().betaSignupError = "Email already signed up.";
+ response.redirect('/ep/beta-account/');
+ }
+
+ sqlobj.insert('pro_beta_signups', {
+ email: email,
+ isActivated: false,
+ signupDate: new Date()
+ });
+
+ response.redirect('/ep/beta-account/signup-ok');
+}
+
+function render_signup_ok() {
+ renderNoticeString(
+ DIV({style: "font-size: 16pt; border: 1px solid green; background: #eeffee; margin: 2em 4em; padding: 1em;"},
+ P("Great! We'll be in touch."),
+ P("In the meantime, you can ", A({href: '/ep/pad/newpad', style: 'text-decoration: underline;'},
+ "create a public pad"), " right now.")));
+}
+
+// return string if not valid, falsy otherwise.
+function isValidCode(code) {
+ if (isValveOpen()) {
+ return undefined;
+ }
+
+ function wr(m) {
+ return DIV(P(m), P("You can sign up for the beta ",
+ A({href: "/ep/beta-account/"}, "here")));
+ }
+
+ if (!code) {
+ return wr("Invalid activation code.");
+ }
+ var record = sqlobj.selectSingle('pro_beta_signups', { activationCode: code });
+ if (!record) {
+ return wr("Invalid activation code.");
+ }
+ if (record.isActivated) {
+ return wr("That activation code has already been used.");
+ }
+ return undefined;
+}
+
+function isValveOpen() {
+ if (appjet.cache.proBetaValveIsOpen === undefined) {
+ appjet.cache.proBetaValveIsOpen = true;
+ }
+ return appjet.cache.proBetaValveIsOpen;
+}
+
+function toggleValve() {
+ appjet.cache.proBetaValveIsOpen = !appjet.cache.proBetaValveIsOpen;
+}
+
+function sendInvite(recordId) {
+ var record = sqlobj.selectSingle('pro_beta_signups', {id: recordId});
+ if (record.activationCode) {
+ getSession().betaAdminMessage = "Already active";
+ return;
+ }
+
+ // create activation code
+ var code = stringutils.randomString(10);
+ sqlcommon.inTransaction(function() {
+ sqlobj.update('pro_beta_signups', {id: recordId}, {activationCode: code});
+ var body = renderTemplateAsString('email/pro_beta_invite.ejs', {
+ toAddr: record.email,
+ signupAgo: timeAgo(record.signupDate),
+ signupCode: code,
+ activationUrl: "http://"+httpHost(request.host)+"/ep/pro-signup/?sc="+code
+ });
+ sendEmail(record.email, "EtherPad <support@pad.spline.inf.fu-berlin.de>",
+ "Your EtherPad Professional Beta Account", {}, body);
+ });
+
+ getSession().betaAdminMessage = "Invite sent.";
+}
+
+function notifyActivated(code) {
+ println("updating: "+code);
+ sqlobj.update('pro_beta_signups', {activationCode: code},
+ {isActivated: true, activationDate: new Date()});
+}
+
diff --git a/etherpad/src/etherpad/control/pro_signup_control.js b/etherpad/src/etherpad/control/pro_signup_control.js
new file mode 100644
index 0000000..6bf7cc3
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro_signup_control.js
@@ -0,0 +1,173 @@
+/**
+ * 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.
+ */
+
+import("jsutils.*");
+import("cache_utils.syncedWithCache");
+import("funhtml.*");
+import("stringutils");
+import("stringutils.*");
+import("sqlbase.sqlcommon");
+
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.domains");
+
+import("etherpad.control.pro_beta_control");
+import("etherpad.control.pro.admin.account_manager_control");
+
+import("etherpad.helpers");
+
+function onRequest() {
+ if (!getSession().ods) {
+ getSession().ods = {};
+ }
+ if (request.method == "POST") {
+ // add params to cart
+ eachProperty(request.params, function(k,v) {
+ getSession().ods[k] = stringutils.toHTML(v);
+ });
+ }
+}
+
+function _errorDiv() {
+ var m = getSession().errorMessage;
+ if (m) {
+ delete getSession().errorMessage;
+ return DIV({className: 'err'}, m);
+ }
+ return "";
+}
+
+function _input(id, type) {
+ return INPUT({type: type ? type : 'text', name: id, id: id,
+ value: getSession().ods[id] || ""});
+}
+
+function _inf(id, label, type) {
+ return DIV(
+ DIV({style: "width: 100px; text-align: right; float: left; padding-top: 3px;"}, label, ": "),
+ DIV({style: "text-align: left; float: left;"},
+ _input(id, type)),
+ DIV({style: "height: 6px; clear: both;"}, " "));
+}
+
+function render_main_get() {
+ // observe activation code
+ if (request.params.sc) {
+ getSession().betaActivationCode = request.params.sc;
+ response.redirect(request.path);
+ }
+
+ // validate activation code
+ var activationCode = getSession().betaActivationCode;
+ var err = pro_beta_control.isValidCode(activationCode);
+ if (err) {
+ renderNoticeString(DIV({style: "border: 1px solid red; background: #fdd; font-weight: bold; padding: 1em;"},
+ err));
+ response.stop();
+ }
+
+ // serve activation page
+ renderFramed('main/pro_signup_body.ejs', {
+ errorDiv: _errorDiv,
+ input: _input,
+ inf: _inf
+ });
+}
+
+function _err(m) {
+ if (m) {
+ getSession().errorMessage = m;
+ response.redirect(request.path);
+ }
+}
+
+function render_main_post() {
+ var subdomain = trim(String(request.params.subdomain).toLowerCase());
+ var fullName = request.params.fullName;
+ var email = trim(request.params.email);
+
+ // validate activation code
+ var activationCode = getSession().betaActivationCode;
+ var err = pro_beta_control.isValidCode(activationCode);
+ if (err) {
+ resonse.write(err);
+ }
+
+ /*
+ var password = request.params.password;
+ var passwordConfirm = request.params.passwordConfirm;
+ */
+ var orgName = subdomain;
+
+ //---- basic validation ----
+ if (!/^\w[\w\d\-]*$/.test(subdomain)) {
+ _err("Invalid domain: "+subdomain);
+ }
+ if (subdomain.length < 2) {
+ _err("Subdomain must be at least 2 characters.");
+ }
+ if (subdomain.length > 60) {
+ _err("Subdomain must be <= 60 characters.");
+ }
+
+/*
+ if (password != passwordConfirm) {
+ _err("Passwords do not match.");
+ }
+ */
+
+ _err(pro_accounts.validateFullName(fullName));
+ _err(pro_accounts.validateEmail(email));
+
+ if (!(email.match(/[Ff][Uu]-[Bb][Ee][Rr][Ll][Ii][Nn].[Dd][Ee]$/))) { _err("Please use your *.fu-berlin.de email address."); }
+// _err(pro_accounts.validatePassword(password));
+
+ //---- database validation ----
+
+ if (domains.doesSubdomainExist(subdomain)) {
+ _err("The domain "+subdomain+" is already in use.");
+ }
+
+ //---- looks good. create records! ----
+
+ // TODO: log a bunch of stuff, and request IP address, etc.
+
+ var ok = false;
+ sqlcommon.inTransaction(function() {
+ var tempPass = stringutils.randomString(10);
+ // TODO: move validation code into domains.createNewSubdomain...
+ var domainId = domains.createNewSubdomain(subdomain, orgName);
+ var accountId = pro_accounts.createNewAccount(domainId, fullName, email, tempPass, true);
+ // send welcome email
+ syncedWithCache('pro-activations', function(c) {
+ c[domainId] = true;
+ });
+ ok = true;
+ if (activationCode) {
+ pro_beta_control.notifyActivated(activationCode);
+ }
+ });
+
+ if (ok) {
+ response.redirect('http://'+subdomain+"."+request.host+'/ep/finish-activation');
+ } else {
+ response.write("There was an error processing your request.");
+ }
+}
+
diff --git a/etherpad/src/etherpad/control/scriptcontrol.js b/etherpad/src/etherpad/control/scriptcontrol.js
new file mode 100644
index 0000000..16efc60
--- /dev/null
+++ b/etherpad/src/etherpad/control/scriptcontrol.js
@@ -0,0 +1,75 @@
+/**
+ * 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.
+ */
+
+import("etherpad.pad.dbwriter");
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+
+function onRequest() {
+ if (!isProduction()) {
+ return;
+ }
+ if (request.params.auth != 'f83kg840d12jk') {
+ response.forbid();
+ }
+}
+
+function render_setdbwritable() {
+ var dbwritable = (String(request.params.value).toLowerCase() != 'false'); // default to true
+
+ dbwriter.setWritableState({constant: dbwritable});
+
+ response.write("OK, set to "+dbwritable);
+}
+
+function render_getdbwritable() {
+ var state = dbwriter.getWritableState();
+
+ response.write(String(dbwriter.getWritableStateDescription(state)));
+}
+
+function render_pausedbwriter() {
+ var seconds = request.params.seconds;
+ var seconds = Number(seconds || 0);
+ if (isNaN(seconds)) seconds = 0;
+
+ var finishTime = (+new Date())+(1000*seconds);
+ dbwriter.setWritableState({trueAfter: finishTime});
+
+ response.write("Paused dbwriter for "+seconds+" seconds.");
+}
+
+function render_fake_pne_on() {
+ if (isProduction()) {
+ response.write("has no effect in production.");
+ } else {
+ appjet.cache.fakePNE = true;
+ response.write("OK");
+ }
+}
+
+function render_fake_pne_off() {
+ if (isProduction()) {
+ response.write("has no effect in production.");
+ } else {
+ appjet.cache.fakePNE = false;
+ response.write("OK");
+ }
+}
+
+
+
+
diff --git a/etherpad/src/etherpad/control/static_control.js b/etherpad/src/etherpad/control/static_control.js
new file mode 100644
index 0000000..d938b26
--- /dev/null
+++ b/etherpad/src/etherpad/control/static_control.js
@@ -0,0 +1,76 @@
+/**
+ * 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.
+ */
+
+import("faststatic");
+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);
+ var serveCompressed = faststatic.compressedFileServer(opts);
+ var serveJs = faststatic.directoryServer(staticBase+'/js/', opts);
+ var serveCss = faststatic.directoryServer(staticBase+'/css/', opts);
+ var serveSwf = faststatic.directoryServer(staticBase+'/swf/', opts);
+ var serveHtml = faststatic.directoryServer(staticBase+'/html/', opts);
+ var serveZip = faststatic.directoryServer(staticBase+'/zip/', opts);
+
+ disp.addLocations([
+ ['/favicon.ico', serveFavicon],
+ ['/robots.txt', serveRobotsTxt],
+ ['/crossdomain.xml', serveCrossDomain],
+ [PrefixMatcher('/static/html/'), serveHtml],
+ [PrefixMatcher('/static/js/'), serveJs],
+ [PrefixMatcher('/static/css/'), serveCss],
+ [PrefixMatcher('/static/swf/'), serveSwf],
+ [PrefixMatcher('/static/zip/'), serveZip],
+ [PrefixMatcher('/static/compressed/'), serveCompressed],
+ [PrefixMatcher('/static/'), serveStaticDir]
+ ]);
+
+ return disp.dispatch();
+}
+
+function serveRobotsTxt(name) {
+ response.neverCache();
+ response.setContentType('text/plain');
+ response.write('User-agent: *\n');
+ if (!isProduction()) {
+ response.write('Disallow: /\n');
+ }
+ response.stop();
+ return true;
+}
diff --git a/etherpad/src/etherpad/control/statscontrol.js b/etherpad/src/etherpad/control/statscontrol.js
new file mode 100644
index 0000000..3659107
--- /dev/null
+++ b/etherpad/src/etherpad/control/statscontrol.js
@@ -0,0 +1,1214 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("netutils");
+import("funhtml.*");
+import("stringutils.{html,sprintf,startsWith,md5}");
+import("jsutils.*");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+import("etherpad.sessions");
+import("etherpad.statistics.statistics");
+import("etherpad.log");
+import("etherpad.usage_stats.usage_stats");
+import("etherpad.helpers");
+
+//----------------------------------------------------------------
+// Usagestats
+//----------------------------------------------------------------
+
+var _defaultPrefs = {
+ topNCount: 5,
+ granularity: 1440
+}
+
+function onRequest() {
+ keys(_defaultPrefs).forEach(function(prefName) {
+ if (request.params[prefName]) {
+ _prefs()[prefName] = request.params[prefName];
+ }
+ });
+ if (request.isPost) {
+ response.redirect(
+ request.path+
+ (request.query ? "?"+request.query : "")+
+ (request.params.fragment ? "#"+request.params.fragment : ""));
+ }
+}
+
+function _prefs() {
+ if (! sessions.getSession().statsPrefs) {
+ sessions.getSession().statsPrefs = {}
+ }
+ return sessions.getSession().statsPrefs;
+}
+
+function _pref(pname) {
+ return _prefs()[pname] || _defaultPrefs[pname];
+}
+
+function _topN() {
+ return _pref('topNCount');
+}
+function _showLiveStats() {
+ return _timescale() < 1440;
+ // return _pref('granularity') == 'live';
+}
+function _showHistStats() {
+ return _timescale() >= 1440
+ // return _pref('showLiveOrHistorical') == 'hist';
+}
+function _timescale() {
+ return Number(_pref('granularity')) || 1;
+}
+
+// types:
+// compare - compare one or more single-value stats
+// top - show top values over time
+// histogram - show histogram over time
+
+var statDisplays = {
+ users: [
+ { name: "visitors",
+ description: "User visits, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "site_pageviews",
+ description: "Page views",
+ color: "FFA928" },
+ {stat: "site_unique_ips",
+ description: "Unique IPs",
+ color: "00FF00" } ] },
+
+ // free pad usage
+ { name: "free pad usage, 1 day",
+ description: "Free pad.spline.inf.fu-berlin.de users, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "active_user_ids",
+ description: "All users",
+ color: "FFA928" },
+ {stat: "users_1day_returning_7days",
+ description: "Users returning after 7 days",
+ color: "00FF00"},
+ {stat: "users_1day_returning_30days",
+ description: "Users returning after 30 days",
+ color: "FF0000"} ] },
+ { name: "free pad usage, 7 day",
+ description: "Free pad.spline.inf.fu-berlin.de users over the last 7 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_user_ids_7days",
+ description: "All users",
+ color: "FFA928" },
+ {stat: "users_7day_returning_7days",
+ description: "Users returning after 7 days",
+ color: "00FF00"},
+ {stat: "users_7day_returning_30days",
+ description: "Users returning after 30 days",
+ color: "FF0000"} ] },
+ { name: "free pad usage, 30 day",
+ description: "Free pad.spline.inf.fu-berlin.de users over the last 30 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_user_ids_30days",
+ description: "All users",
+ color: "FFA928" },
+ {stat: "users_30day_returning_7days",
+ description: "Users returning after 7 days",
+ color: "00FF00"},
+ {stat: "users_30day_returning_30days",
+ description: "Users returning after 30 days",
+ color: "FF0000"} ] },
+
+ // pro pad usage
+ { name: "active pro accounts, 1 day",
+ description: "Active pro accounts, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "active_pro_accounts",
+ description: "All accounts",
+ color: "FFA928" },
+ {stat: "pro_accounts_1day_returning_7days",
+ description: "Accounts older than 7 days",
+ color: "00FF00"},
+ {stat: "pro_accounts_1day_returning_30days",
+ description: "Accounts older than 30 days",
+ color: "FF0000"} ] },
+ { name: "active pro accounts, 7 day",
+ description: "Active pro accounts over the last 7 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_pro_accounts_7days",
+ description: "All accounts",
+ color: "FFA928" },
+ {stat: "pro_accounts_7day_returning_7days",
+ description: "Accounts older than 7 days",
+ color: "00FF00"},
+ {stat: "pro_accounts_7day_returning_30days",
+ description: "Accounts older than 30 days",
+ color: "FF0000"} ] },
+ { name: "active pro accounts, 30 day",
+ description: "Active pro accounts over the last 30 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_pro_accounts_30days",
+ description: "All accounts",
+ color: "FFA928" },
+ {stat: "pro_accounts_30day_returning_7days",
+ description: "Accounts older than 7 days",
+ color: "00FF00"},
+ {stat: "pro_accounts_30day_returning_30days",
+ description: "Accounts older than 30 days",
+ color: "FF0000"} ] },
+
+ // other stats
+ { name: "pad connections",
+ description: "Number of active comet connections, mean over a %t period",
+ type: "top",
+ options: {showOthers: false},
+ stats: ["streaming_connections"] },
+ { name: "referers",
+ description: "Referers, number of hits over a %t period",
+ type: "top",
+ options: {showOthers: false},
+ stats: ["top_referers"] },
+ ],
+ product: [
+ { name: "pads",
+ description: "Newly-created and active pads, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "active_pads",
+ description: "Active pads",
+ color: "FFA928" },
+ {stat: "new_pads",
+ description: "New pads",
+ color: "FF0000" }] },
+ { name: "chats",
+ description: "Chat messages and active chatters, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "chat_messages",
+ description: "Messages",
+ color: "FFA928" },
+ {stat: "active_chatters",
+ description: "Chatters",
+ color: "FF0000" }] },
+ { name: "import/export",
+ description: "Imports and Exports, total over a %t period",
+ type: "compare",
+ stats: [ {stat: {f: '+', args: ["imports_exports_counts:export", "imports_exports_counts:import"]},
+ description: "Total",
+ color: "FFA928" },
+ {stat: "imports_exports_counts:export",
+ description: "Exports",
+ color: "FF0000"},
+ {stat: "imports_exports_counts:import",
+ description: "Imports",
+ color: "00FF00"}] },
+ { name: "revenue",
+ description: "Revenue, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "revenue",
+ description: "Revenue",
+ color: "FFA928"}] }
+ ],
+ performance: [
+ { name: "dynamic page latencies",
+ description: "Slowest dynamic pages: mean load time in milliseconds over a %t period",
+ type: "top",
+ options: {showOthers: false},
+ stats: ["execution_latencies"] },
+ { name: "pad startup latencies",
+ description: "Pad startup times: percent load time in milliseconds over a %t period",
+ type: "histogram",
+ stats: ["pad_startup_times"] },
+ { name: "stream post latencies",
+ description: "Comet post latencies, percentiles in milliseconds over a %t period",
+ type: "histogram",
+ stats: ["streaming_latencies"] },
+ ],
+ health: [
+ { name: "disconnect causes",
+ description: "Causes of disconnects, total over a %t period",
+ type: "top",
+ stats: ["disconnect_causes"] },
+ { name: "paths with 404s",
+ description: "'Not found' responses, by path, number served over a %t period",
+ type: "top",
+ stats: ["paths_404"] },
+ { name: "exceptions",
+ description: "Total number of server exceptions over a %t period",
+ type: "compare",
+ stats: [ {stat: "exceptions",
+ description: "Exceptions",
+ color: "FF1928" } ] },
+ { name: "paths with 500s",
+ type: "top",
+ description: "'500' responses, by path, number served over a %t period",
+ type: "top",
+ stats: ["paths_500"] },
+ { name: "paths with exceptions",
+ description: "responses with exceptions, by path, number served over a %t period",
+ type: "top",
+ stats: ["paths_exception"] },
+ { name: "disconnects with client-side errors",
+ description: "user disconnects with an error on the client side, number over a %t period",
+ type: "compare",
+ stats: [ { stat: "disconnects_with_clientside_errors",
+ description: "Disconnects with errors",
+ color: "FFA928" } ] },
+ { name: "unnecessary disconnects",
+ description: "disconnects that were avoidable, number over a %t period",
+ type: "compare",
+ stats: [ { stat: "streaming_disconnects:disconnected_userids",
+ description: "Number of unique users disconnected",
+ color: "FFA928" },
+ { stat: "streaming_disconnects:total_disconnects",
+ description: "Total number of disconnects",
+ color: "FF0000" } ] },
+ ]
+}
+
+function getUsedStats(statStructure) {
+ var stats = {};
+ function getStructureValues(statStructure) {
+ if (typeof(statStructure) == 'string') {
+ stats[statStructure] = true;
+ } else {
+ statStructure.args.forEach(getStructureValues);
+ }
+ }
+ getStructureValues(statStructure);
+ return keys(stats);
+}
+
+function getStatData(statStructure, values_f) {
+ function getStructureValues(statStructure) {
+ if (typeof(statStructure) == 'string') {
+ return values_f(statStructure);
+ } else if (typeof(statStructure) == 'number') {
+ return statStructure;
+ } else {
+ var args = statStructure.args.map(getStructureValues);
+ return {
+ f: statStructure.f,
+ args: args
+ }
+ }
+ }
+
+ var mappedStructure = getStructureValues(statStructure);
+
+ function evalStructure(statStructure) {
+ if ((typeof(statStructure) == 'number') || (statStructure instanceof Array)) {
+ return statStructure;
+ } else {
+ var merge_f = statStructure.f;
+ if (typeof(merge_f) == 'string') {
+ switch (merge_f) {
+ case '+':
+ merge_f = function() {
+ var sum = 0;
+ for (var i = 0; i < arguments.length; ++i) {
+ sum += arguments[i];
+ }
+ return sum;
+ }
+ break;
+ case '*':
+ merge_f = function() {
+ var product = 0;
+ for (var i = 0; i < arguments.length; ++i) {
+ product *= arguments[i];
+ }
+ return product;
+ }
+ break;
+ case '/':
+ merge_f = function(a, b) { return a / b; }
+ break;
+ case '-':
+ merge_f = function(a, b) { return a - b; }
+ break;
+ }
+ }
+ var evaluatedArguments = statStructure.args.map(evalStructure);
+ var length = -1;
+ evaluatedArguments.forEach(function(arg) {
+ if (typeof(arg) == 'object' && (arg instanceof Array)) {
+ length = arg.length;
+ }
+ });
+ evaluatedArguments = evaluatedArguments.map(function(arg) {
+ if (typeof(arg) == 'number') {
+ var newArg = new Array(length);
+ for (var i = 0; i < newArg.length; ++i) {
+ newArg[i] = arg;
+ }
+ return newArg
+ } else {
+ return arg;
+ }
+ });
+ return mergeArrays.apply(this, [merge_f].concat(evaluatedArguments));
+ }
+ }
+ return evalStructure(mappedStructure);
+}
+
+var googleChartSimpleEncoding = "ABCDEFGHIJLKMNOPQRSTUVQXYZabcdefghijklmnopqrstuvwxyz0123456789-.";
+function _enc(value) {
+ return googleChartSimpleEncoding[Math.floor(value/64)] + googleChartSimpleEncoding[value%64];
+}
+
+function drawSparkline(dataSets, labels, colors, minutes) {
+ var max = 1;
+ var maxLength = 0;
+ dataSets.forEach(function(dataSet, i) {
+ if (dataSet.length > maxLength) {
+ maxLength = dataSet.length;
+ }
+ dataSet.forEach(function(point) {
+ if (point > max) {
+ max = point;
+ }
+ });
+ });
+ var data = dataSets.map(function(dataSet) {
+ var chars = dataSet.map(function(x) {
+ if (x !== undefined) {
+ return _enc(Math.round(x/max*4095));
+ } else {
+ return "__";
+ }
+ }).join("");
+ while (chars.length < maxLength*2) {
+ chars = "__"+chars;
+ }
+ return chars;
+ }).join(",");
+ var timeLabels;
+ if (minutes < 60*24) {
+ timeLabels = [4,3,2,1,0].map(function(t) {
+ var minutesPerTick = minutes/4;
+ var d = new Date(Date.now() - minutesPerTick*60000*t);
+ return (d.getHours()%12 || 12)+":"+(d.getMinutes() < 10 ? "0" : "")+d.getMinutes()+(d.getHours() < 12 ? "am":"pm");
+ }).join("|");
+ } else {
+ timeLabels = [4,3,2,1,0].map(function(t) {
+ var daysPerTick = (minutes/(60*24))/4;
+ var d = new Date(Date.now() - t*daysPerTick*24*60*60*1000);
+ return (d.getMonth()+1)+"/"+d.getDate();
+ }).join("|");
+ }
+ var pointLabels = dataSets.map(function(dataSet, i) {
+ return ["t"+dataSet[dataSet.length-1],colors[i],i,maxLength-1,12,0].join(",");
+ }).join("|");
+ labels = labels.map(function(label) {
+ return encodeURIComponent((label.length > 73) ? label.slice(0, 70) + "..." : label);
+ });
+ var step = Math.round(max/10);
+ step = Math.round(step/Math.pow(10, String(step).length-1))*Math.pow(10, String(step).length-1);
+ var srcUrl =
+ "http://chart.apis.google.com/chart?chs=600x300&cht=lc&chd=e:"+data+
+ "&chxt=y,x&chco="+colors.join(",")+"&chxr=0,0,"+max+","+step+"&chxl=1:|"+timeLabels+
+ "&chdl="+labels.join("|")+"&chdlp=b&chm="+pointLabels;
+ return toHTML(IMG({src: srcUrl}));
+}
+
+var liveDataNumSamples = 20;
+
+function extractStatValuesFunction(nameToValues_f) {
+ return function(statName) {
+ var value;
+ if (statName.indexOf(":") >= 0) {
+ [statName, value] = statName.split(":");
+ }
+ var h = nameToValues_f(statName);
+ if (value) {
+ h = h.map(function(topValues) {
+ if (! topValues) { return; }
+ var tv = topValues.topValues;
+ for (var i = 0; i < tv.length; ++i) {
+ if (tv[i].value == value) {
+ return tv[i].count;
+ }
+ }
+ return 0;
+ });
+ }
+ return h;
+ }
+}
+
+function sparkline_compare(history_f, minutesPerSample, stat) {
+ var histories = stat.stats.map(function(stat) {
+ var samples = getStatData(stat.stat, extractStatValuesFunction(history_f));
+ return [samples, stat.description, stat.color];
+ });
+ return drawSparkline(histories.map(function(history) { return history[0] }),
+ histories.map(function(history) { return history[1] }),
+ histories.map(function(history) { return history[2] }),
+ minutesPerSample*histories[0][0].length);
+}
+
+function sparkline_top(history_f, minutesPerSample, stat) {
+ var showOthers = ! stat.options || stat.options.showOthers != false;
+ var history = stat.stats.map(history_f)[0];
+
+ if (history.length == 0) {
+ return "<b>no data</b>";
+ }
+ var topRecents = {};
+ var topRecents_arr = [];
+ history.forEach(function(tv) {
+ if (! tv) { return; }
+ if (tv.topValues.length > 0) {
+ topRecents_arr = tv.topValues.map(function(x) { return x.value; });
+ }
+ });
+
+ if (topRecents_arr.length == 0) {
+ return "<b>no data</b>";
+ }
+ topRecents_arr = topRecents_arr.slice(0, _topN());
+ topRecents_arr.forEach(function(value, i) {
+ topRecents[value] = i;
+ });
+
+ if (showOthers) {
+ topRecents_arr.push("Other");
+ }
+ var max = 1;
+ var values = topRecents_arr.map(function() { return history.map(function() { return 0 }); });
+
+ history.forEach(function(tv, i) {
+ if (! tv) { return; }
+ tv.topValues.forEach(function(entry) {
+ if (entry.count > max) {
+ max = entry.count;
+ }
+ if (entry.value in topRecents) {
+ values[topRecents[entry.value]][i] = entry.count;
+ } else if (showOthers) {
+ values[values.length-1][i] += entry.count;
+ }
+ });
+ });
+ return drawSparkline(
+ values,
+ topRecents_arr,
+ ["FF0000", "00FF00", "0000FF", "FF00FF", "00FFFF"].slice(0, topRecents_arr.length-1).concat("FFA928"),
+ minutesPerSample*history.length);
+}
+
+function sparkline_histogram(history_f, minutesPerSample, stat) {
+ var history = stat.stats.map(history_f)[0];
+
+ if (history.length == 0) {
+ return "<b>no data</b>";
+ }
+ var percentiles = [50, 90, 95, 99];
+ var data = percentiles.map(function() { return []; })
+ history.forEach(function(hist) {
+ percentiles.forEach(function(pct, i) {
+ data[i].push((hist ? hist[""+pct] : undefined));
+ });
+ });
+ return drawSparkline(
+ data,
+ percentiles.map(function(pct) { return ""+pct+"%"; }),
+ ["FF0000","FF00FF","FFA928","00FF00"].reverse(),
+ minutesPerSample*history.length);
+}
+
+function liveHistoryFunction(minutesPerSample) {
+ return function(statName) {
+ return statistics.liveSnapshot(statName).history(minutesPerSample, liveDataNumSamples);
+ }
+}
+
+function _listStats(statName, count) {
+ var options = { orderBy: '-timestamp,id' };
+ if (count !== undefined) {
+ options.limit = count;
+ }
+ return sqlobj.selectMulti('statistics', {name: statName}, options);
+}
+
+function ancientHistoryFunction(time) {
+ return function(statName) {
+ var seenDates = {};
+ var samples = _listStats(statName);
+
+ samples = samples.reverse().map(function(json) {
+ if (seenDates[""+json.timestamp]) { return; }
+ seenDates[""+json.timestamp] = true;
+ return {timestamp: json.timestamp, json: json.value};
+ }).filter(function(x) { return x !== undefined });
+
+ samples = samples.reverse().slice(0, Math.round(time/(24*60)));
+ var samplesWithEmptyValues = [];
+ for (var i = 0; i < samples.length-1; ++i) {
+ var current = samples[i];
+ var next = samples[i+1];
+ samplesWithEmptyValues.push(current.json);
+ for (var j = current.timestamp+86400*1000; j < next.timestamp; j += 86400*1000) {
+ samplesWithEmptyValues.push(undefined);
+ }
+ }
+ if (samples.length > 0) {
+ samplesWithEmptyValues.push(samples[samples.length-1].json);
+ }
+ samplesWithEmptyValues = samplesWithEmptyValues.map(function(json) {
+ if (! json) { return; }
+ var obj = fastJSON.parse(json);
+ if (keys(obj).length == 1 && 'value' in obj) {
+ obj = obj.value;
+ }
+ return obj;
+ });
+
+ return samplesWithEmptyValues.reverse();
+ }
+}
+
+function sparkline(history_f, minutesPerSample, stat) {
+ if (this["sparkline_"+stat.type]) {
+ return this["sparkline_"+stat.type](history_f, minutesPerSample, stat);
+ } else {
+ return "<b>No sparkline handler!</b>";
+ }
+}
+
+function liveLatestFunction(minutesPerSample) {
+ return function(statName) {
+ return [statistics.liveSnapshot(statName).latest(minutesPerSample)];
+ }
+}
+
+function liveTotal(statName) {
+ return [statistics.liveSnapshot(statName).total];
+}
+
+function historyLatest(statName) {
+ return _listStats(statName, 1).map(function(x) {
+ var value = fastJSON.parse(x.value);
+ if (keys(value).length == 1 && 'value' in value) {
+ value = value.value;
+ }
+ return value;
+ });
+}
+
+function latest_compare(latest_f, stat) {
+ return stat.stats.map(function(stat) {
+ var sample = getStatData(stat.stat, extractStatValuesFunction(latest_f))[0];
+ return { value: sample, description: stat.description };
+ });
+}
+
+function latest_top(latest_f, stat) {
+ var showOthers = ! stat.options || stat.options.showOthers != false;
+
+ var sample = stat.stats.map(latest_f)[0][0];
+ if (! sample) {
+ return [];
+ }
+ var total = sample.count;
+
+ var values = sample.topValues.slice(0, _topN()).map(function(v) {
+ total -= v.count;
+ return { value: v.count, description: v.value };
+ });
+ if (showOthers) {
+ values.push({value: total, description: "Other"});
+ }
+ return values;
+}
+
+function latest_histogram(latest_f, stat) {
+ var sample = stat.stats.map(latest_f)[0][0];
+
+ if (! sample) {
+ return "<b>no data</b>";
+ }
+
+ var percentiles = [0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100].filter(function(pct) { return ((""+pct) in sample) });
+
+ var xpos = percentiles.map(function(x, i) { return sample[x] });
+ var xMax = 0;
+ var xMin = 1e12;
+ xpos.forEach(function(x) { xMax = (x > xMax ? x : xMax); xMin = (x < xMin ? x : xMin); });
+ xposNormalized = xpos.map(function(x) { return Math.round((x-xMin)/(xMax-xMin || 1)*100); });
+
+ var ypos = percentiles.slice(1).map(function(y, i) { return (y-percentiles[i])/(xpos[i+1] || 1); });
+ var yMax = 0;
+ ypos.forEach(function(y) { yMax = (y > yMax ? y : yMax); });
+ yposNormalized = ypos.map(function(y) { return Math.round(y/yMax*100); });
+
+ // var proposedLabels = mergeArrays(function(x, y) { return {pos: x, label: y}; }, xposNormalized, xpos);
+ // var keepLabels = [{pos: 0, label: 0}];
+ // proposedLabels.forEach(function(label) {
+ // if (label.pos - keepLabels[keepLabels.length-1].pos > 10) {
+ // keepLabels.push(label);
+ // }
+ // });
+ //
+ // var labelPos = keepLabels.map(function(x) { return x.pos });
+ // var labels = keepLabels.map(function(x) { return x.label });
+
+ return toHTML(IMG({src:
+ "http://chart.apis.google.com/chart?chs=340x100&cht=lxy&chd=t:"+xposNormalized.join(",")+"|0,"+yposNormalized.join(",")+
+ "&chxt=x&chxr=0,"+xMin+","+xMax+","+Math.floor((xMax-xMin)/5) // "l=0:|"+labels.join("|")+"&chxp=0,"+labelPos.join(",")
+ }));
+}
+
+function latest(latest_f, stat) {
+ if (this["latest_"+stat.type]) {
+ return this["latest_"+stat.type](latest_f, stat);
+ } else {
+ return "<b>No latest handler!</b>";
+ }
+}
+
+function dropdown(name, options, selected) {
+ var select;
+ if (typeof(name) == 'string') {
+ select = SELECT({name: name});
+ } else {
+ select = SELECT(name);
+ }
+
+ function addOption(value, content) {
+ var opt = OPTION({value: value}, content || value);
+ if (value == selected) {
+ opt.attribs.selected = "selected";
+ }
+ select.push(opt);
+ }
+
+ if (options instanceof Array) {
+ options.forEach(f_limitArgs(this, addOption, 1));
+ } else {
+ eachProperty(options, addOption);
+ }
+ return select;
+}
+
+function render_main() {
+ var categoriesToStats = {};
+
+ eachProperty(statDisplays, function(catName, statArray) {
+ categoriesToStats[catName] = statArray.map(_renderableStat);
+ });
+
+ renderHtml('statistics/stat_page.ejs',
+ {eachProperty: eachProperty,
+ statCategoryNames: keys(categoriesToStats),
+ categoriesToStats: categoriesToStats,
+ optionsForm: _optionsForm() });
+}
+
+function _optionsForm() {
+ return FORM({id: "statprefs", method: "POST"}, "Show data with granularity: ",
+ // dropdown({name: 'showLiveOrHistorical', onchange: 'formChanged();'},
+ // {live: 'live', hist: 'historical'},
+ // _pref('showLiveOrHistorical')),
+ // (_showLiveStats() ?
+ // SPAN(" with granularity ",
+ dropdown({name: 'granularity', onchange: 'formChanged();'},
+ {"1": '1 minute', "5": '5 minutes', "60": '1 hour', "1440": '1 day'},
+ _pref('granularity')), // ),
+ // : ""),
+ " top N:",
+ INPUT({type: "text", name: "topNCount", value: _topN()}),
+ INPUT({type: "submit", name: "Set", value: "set N"}),
+ INPUT({type: "hidden", name: "fragment", id: "fragment", value: "health"}));
+}
+
+// function render_main() {
+// var body = BODY();
+//
+// var cat = request.params.cat;
+// if (!cat) {
+// cat = 'health';
+// }
+//
+// body.push(A({id: "backtoadmin", href: "/ep/admin/"}, html("&laquo;"), " back to admin"));
+// body.push(_renderTopnav(cat));
+//
+// body.push(form);
+//
+// if (request.params.stat) {
+// body.push(A({className: "viewall",
+// href: qpath({stat: null})}, html("&laquo;"), " view all"));
+// }
+//
+// var statNames = statDisplays[cat];
+// statNames.forEach(function(sn) {
+// if (!request.params.stat || (request.params.stat == sn)) {
+// body.push(_renderableStat(sn));
+// }
+// });
+//
+// helpers.includeCss('admin/admin-stats.css');
+// response.write(HTML(HEAD(html(helpers.cssIncludes())), body));
+// }
+
+function _getLatest(stat) {
+ var minutesPerSample = _timescale();
+
+ if (_showLiveStats()) {
+ return latest(liveLatestFunction(minutesPerSample), stat);
+ } else {
+ return latest(liveTotal, stat);
+ }
+}
+
+function _getGraph(stat) {
+ var minutesPerSample = _timescale();
+
+ if (_showLiveStats()) {
+ return html(sparkline(liveHistoryFunction(minutesPerSample), minutesPerSample, stat));
+ } else {
+ return html(sparkline(ancientHistoryFunction(60*24*60), 24*60, stat));
+ }
+}
+
+function _getDataLinks(stat) {
+ if (_showLiveStats()) {
+ return;
+ }
+
+ function listToLinks(list) {
+ var links = []; //SPAN({className: "datalink"}, "(data for ");
+ list.forEach(function(statName) {
+ links.push(toHTML(A({href: "/ep/admin/usagestats/data?statName="+statName}, statName)));
+ });
+// links.push(")");
+ return links;
+ }
+
+ switch (stat.type) {
+ case 'compare':
+ var stats = [];
+ stat.stats.map(function(stat) { return getUsedStats(stat.stat); }).forEach(function(list) {
+ stats = stats.concat(list);
+ });
+ return listToLinks(stats);
+ case 'top':
+ return listToLinks(stat.stats);
+ case 'histogram':
+ return listToLinks(stat.stats);
+ }
+}
+
+function _renderableStat(stat) {
+ var minutesPerSample = _timescale();
+
+ var period = (_showLiveStats() ? minutesPerSample : 24*60);
+
+ if (period < 24*60 && stat.hideLive) {
+ return;
+ }
+
+ if (period < 60) {
+ period = ""+period+"-minute";
+ } else if (period < 24*60) {
+ period = ""+period/(60)+"-hour";
+ } else if (period >= 24*60) {
+ period = ""+period/(24*60)+"-day";
+ }
+ var graph = _getGraph(stat);
+ var id = stat.name.replace(/[^a-zA-Z0-9]/g, "");
+
+ var displayName = stat.description.replace("%t", period);
+ var latest = _getLatest(stat);
+ var dataLinks = _getDataLinks(stat);
+
+ return {
+ id: id,
+ specialState: "",
+ displayName: displayName,
+ name: stat.name,
+ graph: graph,
+ latest: latest,
+ dataLinks: dataLinks
+ }
+}
+
+function render_data() {
+ var sn = request.params.statName;
+ var t = TABLE({border: 1, cellpadding: 2, style: "font-family: monospace;"});
+ _listStats(sn).forEach(function(s) {
+ var tr = TR();
+ tr.push(TD((s.id)));
+ tr.push(TD((new Date(s.timestamp * 1000)).toString()));
+ tr.push(TD(s.value));
+ t.push(tr);
+ });
+ response.write(HTML(BODY(t)));
+}
+
+
+// function renderStat(body, statName) {
+// var div = DIV({className: 'statbox'});
+// div.push(A({className: "stat-title", href: qpath({stat: statName})},
+// statName, descriptions[statName] || ""));
+// if (_showHistStats()) {
+// div.push(
+// DIV({className: "stat-graph"},
+// A({href: '/ep/admin/usagestats/graph?size=1080x420&statName='+statName},
+// IMG({src: '/ep/admin/usagestats/graph?size=400x200&statName='+statName,
+// style: 'border: 1px solid #ccc; margin: 10px 0 0 20px;'})),
+// BR(),
+// DIV({style: 'text-align: right;'},
+// A({style: 'text-decoration: none; font-size: .8em;',
+// href: '/ep/admin/usagestats/data?statName='+statName}, "(data)")))
+// );
+// }
+// if (_showLiveStats()) {
+// var data = statistics.getStatData(statName);
+// var displayData = statistics.liveSnapshot(data);
+// var t = TABLE({border: 0});
+// var tcount = 0;
+// ["minute", "hour", "fourHour", "day"].forEach(function(timescale) {
+// if (! _showTimescale(timescale)) { return; }
+// var tr = TR();
+// t.push(tr);
+// tr.push(TD({valign: "top"}, B("Last ", timescale)));
+// var td = TD();
+// var cell = SPAN();
+// tr.push(td);
+// td.push(cell);
+// switch (data.plotType) {
+// case 'line':
+// cell.push(B(displayData[timescale])); break;
+// case 'topValues':
+// var top = displayData[timescale].topValues;
+// if (tcount != 0) {
+// tr[0].attribs.style = cell.attribs.style = "border-top: 2px solid black;";
+// }
+// // println(statName+" / top length: "+top.length);
+// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
+// cell.push(B(top[i].count), ": ", top[i].value, BR());
+// }
+// break;
+// case 'histogram':
+// var percentiles = displayData[timescale];
+// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+// var max = percentiles["100"] || 1000;
+// cell.push(IMG({src: "http://chart.apis.google.com/chart?chs=340x100&cht=bvs&chd=t:"+
+// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
+// "&chxt=x,y&chxl=0:|"+
+// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
+// "&chxr=0,0,100|1,0,"+max+""}))
+// // td.push("50%: ", B(percentiles["50"]), " ",
+// // "90%: ", B(percentiles["90"]), " ",
+// // "max: ", B(percentiles["100"]));
+// break;
+// }
+// tcount++;
+// });
+// div.push(DIV({className: "stat-table"}, t));
+// div.push(html(helpers.clearFloats()));
+// }
+// body.push(div);
+// }
+// =======
+// >>>>>>> Stashed changes:etherpad/src/etherpad/control/statscontrol.js
+
+
+// old output.
+
+//
+// function getStatsForCategory(category) {
+// var statnames = statistics.getAllStatNames();
+//
+// var matchingStatNames = [];
+// statnames.forEach(function(sn) {
+// if (statistics.getStatData(sn).category == category) {
+// matchingStatNames.push(sn);
+// }
+// });
+//
+// return matchingStatNames;
+// }
+//
+// function renderCategoryList() {
+// var body = BODY();
+//
+// catNames = getCategoryNames();
+// body.push(P("Please select a statistics category:"));
+// catNames.sort().forEach(function(catname) {
+// body.push(P(A({href: "/ep/admin/usagestats/?cat="+catname}, catname)));
+// });
+// response.write(body);
+// }
+//
+// function getCategoryNames() {
+// var statnames = statistics.getAllStatNames();
+// var catNames = {};
+// statnames.forEach(function(sn) {
+// catNames[statistics.getStatData(sn).category] = true;
+// });
+// return keys(catNames);
+// }
+//
+// function dropdown(name, options, selected) {
+// var select;
+// if (typeof(name) == 'string') {
+// select = SELECT({name: name});
+// } else {
+// select = SELECT(name);
+// }
+//
+// function addOption(value, content) {
+// var opt = OPTION({value: value}, content || value);
+// if (value == selected) {
+// opt.attribs.selected = "selected";
+// }
+// select.push(opt);
+// }
+//
+// if (options instanceof Array) {
+// options.forEach(f_limitArgs(this, addOption, 1));
+// } else {
+// eachProperty(options, addOption);
+// }
+// return select;
+// }
+//
+// function getCategorizedStats() {
+// var statnames = statistics.getAllStatNames();
+// var categories = {}
+// statnames.forEach(function(sn) {
+// var category = statistics.getStatData(sn).category
+// if (! categories[category]) {
+// categories[category] = [];
+// }
+// categories[category].push(statistics.getStatData(sn));
+// });
+// return categories;
+// }
+//
+// function render_ajax() {
+// var categoriesToStats = getCategorizedStats();
+//
+// eachProperty(categoriesToStats, function(catName, statArray) {
+// categoriesToStats[catName] = statArray.map(function(statObject) {
+// return {
+// specialState: "",
+// displayName: statObject.name,
+// name: statObject.name,
+// data: liveStatDisplayHtml(statObject)
+// }
+// })
+// });
+//
+// renderHtml('statistics/stat_page.ejs',
+// {eachProperty: eachProperty,
+// statCategoryNames: keys(categoriesToStats),
+// categoriesToStats: categoriesToStats });
+// }
+
+// function render_main() {
+// var body = BODY();
+//
+// var statNames = statistics.getAllStatNames(); //getStatsForCategory(request.params.cat);
+// statNames.forEach(function(sn) {
+// renderStat(body, sn);
+// });
+// response.write(body);
+// }
+//
+// var descriptions = {
+// execution_latencies: ", mean response time in milliseconds",
+// static_file_latencies: ", mean response time in milliseconds",
+// pad_startup_times: ", max response time in milliseconds of fastest N% of requests"
+// };
+//
+// function liveStatDisplayHtml(statObject) {
+// var displayData = statistics.liveSnapshot(statObject);
+// switch (statObject.plotType) {
+// case 'line':
+// return displayData;
+// case 'topValues':
+// var data = {}
+// eachProperty(displayData, function(timescale, tsdata) {
+// data[timescale] = ""
+// var top = tsdata.topValues;
+// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
+// data[timescale] += [B(top[i].count), ": ", top[i].value, BR()].map(toHTML).join("");
+// }
+// if (data[timescale] == "") {
+// data[timescale] = "(no data)";
+// }
+// });
+// return data;
+// case 'histogram':
+// var imgs = {}
+// eachProperty(displayData, function(timescale, tsdata) {
+// var percentiles = tsdata;
+// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+// var max = percentiles["100"] || 1000;
+// imgs[timescale] =
+// toHTML(IMG({src: "http://chart.apis.google.com/chart?chs=400x100&cht=bvs&chd=t:"+
+// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
+// "&chxt=x,y&chxl=0:|"+
+// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
+// "&chxr=0,0,100|1,0,"+max+""}));
+// });
+// return imgs;
+// }
+// }
+//
+// function renderStat(body, statName) {
+// var div = DIV({style: 'float: left; text-align: center; margin: 3px; border: 1px solid black;'})
+// div.push(P(statName, descriptions[statName] || ""));
+// if (_showLiveStats()) {
+// var data = statistics.getStatData(statName);
+// var displayData = statistics.liveSnapshot(data);
+// var t = TABLE();
+// var tcount = 0;
+// ["minute", "hour", "fourHour", "day"].forEach(function(timescale) {
+// if (! _showTimescale(timescale)) { return; }
+// var tr = TR();
+// t.push(tr);
+// tr.push(TD("last ", timescale));
+// var td = TD();
+// tr.push(td);
+// switch (data.plotType) {
+// case 'line':
+// td.push(B(displayData[timescale])); break;
+// case 'topValues':
+// var top = displayData[timescale].topValues;
+// if (tcount != 0) {
+// tr[0].attribs.style = td.attribs.style = "border-top: 1px solid gray;";
+// }
+// // println(statName+" / top length: "+top.length);
+// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
+// td.push(B(top[i].count), ": ", top[i].value, BR());
+// }
+// break;
+// case 'histogram':
+// var percentiles = displayData[timescale];
+// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+// var max = percentiles["100"] || 1000;
+// td.push(IMG({src: "http://chart.apis.google.com/chart?chs=340x100&cht=bvs&chd=t:"+
+// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
+// "&chxt=x,y&chxl=0:|"+
+// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
+// "&chxr=0,0,100|1,0,"+max+""}))
+// // td.push("50%: ", B(percentiles["50"]), " ",
+// // "90%: ", B(percentiles["90"]), " ",
+// // "max: ", B(percentiles["100"]));
+// break;
+// }
+// tcount++;
+// });
+// div.push(t)
+// }
+// if (_showHistStats()) {
+// div.push(A({href: '/ep/admin/usagestats/graph?size=1080x420&statName='+statName},
+// IMG({src: '/ep/admin/usagestats/graph?size=400x200&statName='+statName,
+// style: 'border: 1px solid #ccc; margin: 10px 0 0 20px;'})),
+// BR(),
+// DIV({style: 'text-align: right;'},
+// A({style: 'text-decoration: none; font-size: .8em;',
+// href: '/ep/admin/usagestats/data?statName='+statName}, "(data)")));
+// }
+// body.push(div);
+// }
+//
+// function render_graph() {
+// var sn = request.params.statName;
+// if (!sn) {
+// render404();
+// }
+// usage_stats.respondWithGraph(sn);
+// }
+//
+//
+// function render_exceptions() {
+// var logNames = ["frontend/exception", "backend/exceptions"];
+// }
+
+// function render_updatehistory() {
+//
+// sqlcommon.withConnection(function(conn) {
+// var stmnt = "delete from statistics;";
+// var s = conn.createStatement();
+// sqlcommon.closing(s, function() {
+// s.execute(stmnt);
+// });
+// });
+//
+// var processed = {};
+//
+// function _domonth(y, m) {
+// for (var i = 0; i < 32; i++) {
+// _processStatsDay(y, m, i, processed);
+// }
+// }
+//
+// _domonth(2008, 10);
+// _domonth(2008, 11);
+// _domonth(2008, 12);
+// _domonth(2009, 1);
+// _domonth(2009, 2);
+// _domonth(2009, 3);
+// _domonth(2009, 4);
+// _domonth(2009, 5);
+// _domonth(2009, 6);
+// _domonth(2009, 7);
+//
+// response.redirect('/ep/admin/usagestats');
+// }
+
+// function _processStatsDay(year, month, date, processed) {
+// var now = new Date();
+// var day = new Date();
+//
+// for (var i = 0; i < 10; i++) {
+// day.setFullYear(year);
+// day.setDate(date);
+// day.setMonth(month-1);
+// }
+//
+// if ((+day < +now) &&
+// (!((day.getFullYear() == now.getFullYear()) &&
+// (day.getMonth() == now.getMonth()) &&
+// (day.getDate() == now.getDate())))) {
+//
+// var dayNoon = statistics.noon(day);
+//
+// if (processed[dayNoon]) {
+// return;
+// } else {
+// statistics.processLogDay(new Date(dayNoon));
+// processed[dayNoon] = true;
+// }
+// } else {
+// /* nothing */
+// }
+// }
+
diff --git a/etherpad/src/etherpad/control/store/eepnet_checkout_control.js b/etherpad/src/etherpad/control/store/eepnet_checkout_control.js
new file mode 100644
index 0000000..ddd4973
--- /dev/null
+++ b/etherpad/src/etherpad/control/store/eepnet_checkout_control.js
@@ -0,0 +1,757 @@
+/**
+ * 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.
+ */
+
+import("email.sendEmail");
+import("fastJSON");
+import("funhtml.*");
+import("jsutils.*");
+import("sqlbase.sqlobj");
+import("stringutils");
+import("sync");
+
+import("etherpad.billing.billing");
+import("etherpad.billing.fields");
+import("etherpad.globals");
+import("etherpad.globals.*");
+import("etherpad.helpers");
+import("etherpad.licensing");
+import("etherpad.pro.pro_utils");
+import("etherpad.sessions.{getSession,getTrackingId,getSessionId}");
+import("etherpad.store.checkout");
+import("etherpad.store.eepnet_checkout");
+import("etherpad.utils.*");
+
+import("static.js.billing_shared.{billing=>billingJS}");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+var STORE_URL = '/ep/store/eepnet-checkout/';
+
+var _pageSequence = [
+ ['purchase', "Number of Users", true],
+ ['support-contract', "Support Contract", true],
+ ['license-info', "License Information", true],
+ ['billing-info', "Billing Information", true],
+ ['confirmation', "Confirmation", false]
+];
+
+var _specialPages = {
+ 'receipt': ['receipt', "Receipt", false]
+}
+
+//----------------------------------------------------------------
+
+function _cart() {
+ return getSession().eepnetCart;
+}
+
+function _currentPageSegment() {
+ return request.path.split('/')[4];
+}
+
+function _currentPageId() {
+ return _applyToCurrentPageSequenceEntry(function(ps) { return ps[0]; });
+}
+
+function _applyToCurrentPageSequenceEntry(f) {
+ for (var i = 0; i < _pageSequence.length; i++) {
+ if (_pageSequence[i][0] == _currentPageSegment()) {
+ return f(_pageSequence[i], i, true);
+ }
+ }
+ if (_specialPages[_currentPageSegment()]) {
+ return f(_specialPages[_currentPageSegment()], -1, false);
+ }
+ return undefined;
+}
+
+function _currentPageIndex() {
+ return _applyToCurrentPageSequenceEntry(function(ps, i) { return i; });
+}
+
+function _currentPageTitle() {
+ return _applyToCurrentPageSequenceEntry(function(ps) { return ps[1]; });
+}
+
+function _currentPageShowCart() {
+ return _applyToCurrentPageSequenceEntry(function(ps) { return ps[2]; });
+}
+
+function _currentPageInFlow() {
+ return _applyToCurrentPageSequenceEntry(function(ps, i, isSpecial) { return isSpecial });
+}
+
+function _pageId(d) {
+ return _applyToCurrentPageSequenceEntry(function(ps, i) {
+ if (_pageSequence[i+d]) {
+ return _pageSequence[i+d][0];
+ }
+ });
+}
+
+function _nextPageId() { return _pageId(+1); }
+function _prevPageId() { return _pageId(-1); }
+
+function _advancePage() {
+ response.redirect(_pathTo(_nextPageId()));
+}
+
+function _pathTo(id) {
+ return STORE_URL+id;
+}
+
+// anything starting with 'billing' is also ok.
+function _isAutomaticallySetParam(p) {
+ var _automaticallySetParams = arrayToSet([
+ 'numUsers', 'couponCode', 'supportContract',
+ 'email', 'ownerName', 'orgName', 'licenseAgreement'
+ ]);
+
+ return _automaticallySetParams[p] || stringutils.startsWith(p, "billing");
+}
+
+function _lastSubmittedPage() {
+ var cart = _cart();
+ return isNaN(cart.lastSubmittedPage) ? -1 : Number(cart.lastSubmittedPage);
+}
+
+function _shallowSafeCopy(obj) {
+ return billing.clearKeys(obj, [
+ {name: 'billingCCNumber',
+ valueTest: function(s) { return /^\d{15,16}$/.test(s) },
+ valueReplace: billing.replaceWithX },
+ {name: 'billingCSC',
+ valueTest: function(s) { return /^\d{3,4}$/.test(s) },
+ valueReplace: billing.replaceWithX }]);
+}
+
+function onRequest() {
+ billing.log({
+ 'type': "billing-request",
+ 'date': +(new Date),
+ 'method': request.method,
+ 'path': request.path,
+ 'query': request.query,
+ 'host': request.host,
+ 'scheme': request.scheme,
+ 'params': _shallowSafeCopy(request.params),
+ 'cart': _shallowSafeCopy(_cart())
+ });
+ if (request.path == STORE_URL+"paypalnotify") {
+ _handlePaypalNotification();
+ }
+ if (request.path == STORE_URL+"paypalredirect") {
+ _handlePayPalRedirect();
+ }
+ var cart = _cart();
+ if (!cart || request.params.clearcart) {
+ getSession().eepnetCart = {
+ lastSubmittedPage: -1,
+ invoiceId: billing.createInvoice()
+ };
+ if (request.params.clearcart) {
+ response.redirect(request.path);
+ }
+ if (_currentPageId() != 'purchase') {
+ response.redirect(_pathTo('purchase'));
+ }
+ cart = _cart();
+ }
+ if (request.params.invoice) {
+ cart.billingPurchaseType = 'invoice';
+ }
+ if (cart.purchaseComplete && _currentPageId() != 'receipt') {
+ cart.showStartOverMessage = true;
+ response.redirect(_pathTo('receipt'));
+ }
+ // somehow user got too far?
+ if (_currentPageIndex() > _lastSubmittedPage() + 1) {
+ response.redirect(_pathTo(_pageSequence[_lastSubmittedPage()+1][0]));
+ }
+ if (request.isGet) {
+ // see if this is a standard cart-page get
+ if (_currentPageId()) {
+ _renderCartPage();
+ return true;
+ }
+ }
+ if (request.isPost) {
+ // add params to cart
+ eachProperty(request.params, function(k,v) {
+ if (! _isAutomaticallySetParam(k)) { return; }
+ if (k == "billingCCNumber" && v.charAt(0) == 'X') { return; }
+ cart[k] = stringutils.toHTML(v);
+ });
+ if (_currentPageId() == 'license-info' && ! request.params.licenseAgreement) {
+ delete cart.licenseAgreement;
+ }
+ if (_currentPageIndex() > cart.lastSubmittedPage) {
+ cart.lastSubmittedPage = _currentPageIndex();
+ }
+ }
+ if (request.params.backbutton) {
+ _updateCosts();
+ response.redirect(_pathTo(_prevPageId()));
+ }
+ return false; // commence auto-dispatch
+}
+
+function _getCoupon(code) {
+ return sqlobj.selectSingle('checkout_referral', {id: code});
+}
+
+function _supportCost() {
+ var cart = _cart();
+ return Math.max(eepnet_checkout.SUPPORT_MIN_COST, eepnet_checkout.SUPPORT_COST_PCT/100*cart.baseCost);
+}
+
+function _discountedSupportCost() {
+ var cart = _cart();
+ if ('couponSupportPctDiscount' in cart) {
+ return _supportCost() -
+ (cart.couponSupportPctDiscount ?
+ cart.couponSupportPctDiscount/100 * _supportCost() :
+ 0);
+ }
+}
+
+function _updateCosts() {
+ var cart = _cart();
+
+ if (cart.numUsers) {
+ cart.numUsers = Number(cart.numUsers);
+
+ cart.baseCost = cart.numUsers * eepnet_checkout.COST_PER_USER;
+
+ if (cart.supportContract == "true") {
+ cart.supportCost = _supportCost();
+ } else {
+ delete cart.supportCost;
+ }
+
+ var coupon = _getCoupon(cart.couponCode);
+ if (coupon) {
+ for (i in coupon) {
+ cart["coupon"+stringutils.makeTitle(i)] = coupon[i];
+ }
+ cart.coupon = coupon;
+ } else {
+ for (i in cart.coupon) {
+ delete cart["coupon"+stringutils.makeTitle(i)];
+ }
+ delete cart.coupon;
+ }
+
+ if (cart.couponProductPctDiscount) {
+ cart.productReferralDiscount =
+ cart.couponProductPctDiscount/100 * cart.baseCost;
+ } else {
+ delete cart.productReferralDiscount;
+ }
+ if (cart.couponSupportPctDiscount) {
+ cart.supportReferralDiscount =
+ cart.couponSupportPctDiscount/100 * (cart.supportCost || 0);
+ } else {
+ delete cart.supportReferralDiscount;
+ }
+ cart.subTotal =
+ cart.baseCost - (cart.productReferralDiscount || 0) +
+ (cart.supportCost || 0) - (cart.supportReferralDiscount || 0);
+
+ if (cart.couponTotalPctDiscount) {
+ cart.totalReferralDiscount =
+ cart.couponTotalPctDiscount/100 * cart.subTotal;
+ } else {
+ delete cart.totalReferralDiscount;
+ }
+
+ if (cart.couponFreeUsersCount || cart.couponFreeUsersPct) {
+ cart.freeUserCount =
+ Math.round(cart.couponFreeUsersCount +
+ cart.couponFreeUsersPct/100 * cart.numUsers);
+ } else {
+ delete cart.freeUserCount;
+ }
+ cart.userCount = Number(cart.numUsers) + Number(cart.freeUserCount || 0);
+
+ cart.total =
+ cart.subTotal - (cart.totalReferralDiscount || 0);
+ }
+}
+
+//----------------------------------------------------------------
+// template helper functions
+//----------------------------------------------------------------
+
+function _cartDebug() {
+ if (globals.isProduction()) {
+ return '';
+ }
+
+ var d = DIV({style: 'font-family: monospace; font-size: 1em; border: 1px solid #ccc; padding: 1em; margin: 1em;'});
+ d.push(H3({style: "font-size: 1.5em; font-weight: bold;"}, "Debug Info:"));
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 4});
+ keys(_cart()).sort().forEach(function(k) {
+ var v = _cart()[k];
+ if (typeof(v) == 'object' && v != null) {
+ v = v.toSource();
+ }
+ t.push(TR(TD({style: 'padding: 2px 6px;', align: 'right'}, k),
+ TD({style: 'padding: 2px 6px;', align: 'left'}, v)));
+ });
+ d.push(t);
+ return d;
+}
+
+var billingButtonName = "Review Order";
+
+function _templateContext(extra) {
+ var cart = _cart();
+
+ var pageId = _currentPageId();
+
+ var ret = {
+ cart: cart,
+ costPerUser: eepnet_checkout.COST_PER_USER,
+ supportCostPct: eepnet_checkout.SUPPORT_COST_PCT,
+ supportMinCost: eepnet_checkout.SUPPORT_MIN_COST,
+ errorIfInvalid: _errorIfInvalid,
+ dollars: checkout.dollars,
+ countryList: fields.countryList,
+ usaStateList: fields.usaStateList,
+ obfuscateCC: checkout.obfuscateCC,
+ helpers: helpers,
+ inFlow: _currentPageInFlow(),
+ displayCart: _displayCart,
+ displaySummary: _displaySummary,
+ pathTo: _pathTo,
+ billing: billingJS,
+ handlePayPalRedirect: _handlePayPalRedirect,
+ supportCost: _supportCost,
+ discountedSupportCost: _discountedSupportCost,
+ billingButtonName: billingButtonName,
+ billingFinalPhrase: "<p>You will not be charged until you review"+
+ " and confirm your order on the next page.</p>",
+ getFullSuperdomainHost: pro_utils.getFullSuperdomainHost,
+ showCouponCode: false
+ };
+ eachProperty(extra, function(k, v) {
+ ret[k] = v;
+ });
+ return ret;
+}
+
+function _displayCart(cartid, editable) {
+ return renderTemplateAsString('store/eepnet-checkout/cart.ejs', _templateContext({
+ shoppingcartid: cartid || "shoppingcart",
+ editable: editable
+ }));
+}
+
+function _displaySummary(editable) {
+ return renderTemplateAsString('store/eepnet-checkout/summary.ejs', _templateContext({
+ editable: editable
+ }));
+}
+
+function _renderCartPage() {
+ var cart = _cart();
+
+ var pageId = _currentPageId();
+ var title = _currentPageTitle();
+
+ function _getContent() {
+ return renderTemplateAsString('store/eepnet-checkout/'+pageId+'.ejs', _templateContext());
+ }
+
+ renderFramed('store/eepnet-checkout/checkout-template.ejs', {
+ cartDebug: _cartDebug,
+ errorDiv: _errorDiv,
+ pageId: pageId,
+ getContent: _getContent,
+ title: title,
+ inFlow: _currentPageInFlow(),
+ displayCart: _displayCart,
+ showCart: _currentPageShowCart(),
+ cart: cart,
+ billingButtonName: billingButtonName
+ });
+
+ // clear errors
+ delete cart.errorMsg;
+ delete cart.errorId;
+}
+
+function _errorDiv() {
+ var m = _cart().errorMsg;
+ if (m) {
+ return DIV({className: 'errormsg', id: 'errormsg'}, m);
+ } else {
+ return '';
+ }
+}
+
+function _errorIfInvalid(id) {
+ var e = _cart().errorId
+ if (e && e[id]) {
+ return 'error';
+ } else {
+ return '';
+ }
+}
+
+function _validationError(id, msg, pageId) {
+ var cart = _cart();
+ cart.errorMsg = msg;
+ cart.errorId = {};
+ if (id instanceof Array) {
+ id.forEach(function(k) {
+ cart.errorId[k] = true;
+ });
+ } else {
+ cart.errorId[id] = true;
+ }
+ if (pageId) {
+ response.redirect(_pathTo(pageId));
+ }
+ response.redirect(request.path);
+}
+
+//--------------------------------------------------------------------------------
+// main
+//--------------------------------------------------------------------------------
+
+function render_main() {
+ response.redirect(STORE_URL+'purchase');
+}
+
+//--------------------------------------------------------------------------------
+// cart
+//--------------------------------------------------------------------------------
+
+function render_purchase_post() {
+ var cart = _cart();
+
+ // validate numUsers and couponCode
+ if (! checkout.isOnlyDigits(cart.numUsers)) {
+ _validationError("numUsers", "Please enter a valid number of users.");
+ }
+ if (Number(cart.numUsers) < 1) {
+ _validationError("numUsers", "Please specify at least one user.");
+ }
+
+ if (cart.couponCode && (cart.couponCode.length != 8 || ! _getCoupon(cart.couponCode))) {
+ _validationError("couponCode", "That coupon code does not appear to be valid.");
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+//--------------------------------------------------------------------------------
+// support-contract
+//--------------------------------------------------------------------------------
+
+function render_support_contract_post() {
+ var cart = _cart();
+
+ if (cart.supportContract != "true" && cart.supportContract != "false") {
+ _validationError("supportContract", "Please select one of the options.");
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+//--------------------------------------------------------------------------------
+// license-info
+//--------------------------------------------------------------------------------
+
+function render_license_info_post() {
+ var cart = _cart();
+
+ if (!isValidEmail(cart.email)) {
+ _validationError("email", "That email address does not look valid.");
+ }
+ if (!cart.ownerName) {
+ _validationError("ownerName", "Please enter a license owner name.");
+ }
+ if (!cart.orgName) {
+ _validationError("orgName", "Please enter an organization name.");
+ }
+ if (!cart.licenseAgreement) {
+ _validationError("licenseAgreement", "You must agree to the terms of the license to purchase EtherPad PNE.");
+ }
+
+ if ((! cart.billingFirstName) && ! (cart.billingLastName)) {
+ var nameParts = cart.ownerName.split(/\s+/);
+ if (nameParts.length == 1) {
+ cart.billingFirstName = nameParts[0];
+ } else {
+ cart.billingLastName = nameParts[nameParts.length-1];
+ cart.billingFirstName = nameParts.slice(0, nameParts.length-1).join(' ');
+ }
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+//--------------------------------------------------------------------------------
+// billing-info
+//--------------------------------------------------------------------------------
+
+function render_billing_info_post() {
+ var cart = _cart();
+
+ checkout.validateBillingCart(_validationError, cart);
+ if (cart.billingPurchaseType == 'paypal') {
+ _beginPaypalPurchase();
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+function _absoluteUrl(id) {
+ return request.scheme+"://"+request.host+_pathTo(id);
+}
+
+function _beginPaypalPurchase() {
+ _updateCosts();
+
+ var cart = _cart();
+
+ var purchase = _generatePurchaseRecord();
+ var result =
+ billing.beginExpressPurchase(cart.invoiceId, cart.customerId,
+ "EEPNET", cart.total || 0.01, cart.couponCode || "",
+ _absoluteUrl('paypalredirect?status=ok'),
+ _absoluteUrl('paypalredirect?status=fail'),
+ _absoluteUrl('paypalnotify'));
+ if (result.status != 'success') {
+ _validationError("billingPurchaseType",
+ "PayPal purchase not available at the moment. "+
+ "Please try again later, or try using a different payment option.");
+ }
+ cart.paypalPurchaseInfo = result.purchaseInfo;
+ response.redirect(billing.paypalPurchaseUrl(result.purchaseInfo.token));
+}
+
+//--------------------------------------------------------------------------------
+// confirmation
+//--------------------------------------------------------------------------------
+
+function _handlePaypalNotification() {
+ var ret = billing.handlePaypalNotification();
+ if (ret.status == 'completion') {
+ var purchaseInfo = ret.purchaseInfo;
+ var eepnetPurchase = eepnet_checkout.getPurchaseByInvoiceId(purchaseInfo.invoiceId);
+ var fakeCart = {
+ ownerName: eepnetPurchase.owner,
+ orgName: eepnetPurchase.organization,
+ email: eepnetPurchase.emails,
+ customerId: eepnetPurchase.id,
+ userCount: eepnetPurchase.numUsers,
+ receiptEmail: eepnetPurchase.receiptEmail,
+ }
+ eepnet_checkout.generateLicenseKey(fakeCart);
+ eepnet_checkout.sendReceiptEmail(fakeCart);
+ eepnet_checkout.sendLicenseEmail(fakeCart);
+ billing.log({type: 'purchase-complete', dollars: purchaseInfo.cost});
+ }
+}
+
+function _handlePayPalRedirect() {
+ var cart = _cart();
+
+ if (request.params.status == 'ok' && cart.paypalPurchaseInfo) {
+ var result = billing.continueExpressPurchase(cart.paypalPurchaseInfo);
+ if (result.status == 'success') {
+ cart.paypalPayerInfo = result.payerInfo;
+ response.redirect(_pathTo('confirmation'));
+ } else {
+ _validationError("billingPurchaseType",
+ "There was an error processing your payment through PayPal. "+
+ "Please try again later, or use a different payment option.",
+ 'billing-info');
+ }
+ } else {
+ _validationError("billingPurchaseType",
+ "PayPal payment didn't go through. "+
+ "Please try again later, or use a different payment option.",
+ 'billing-info');
+ }
+}
+
+function _recordPurchase(p) {
+ return sqlobj.insert("checkout_purchase", p);
+}
+
+function _generatePurchaseRecord() {
+ var cart = _cart();
+
+ if (! cart.invoiceId) {
+ throw Error("No invoice id!");
+ }
+
+ var purchase = {
+ invoiceId: cart.invoiceId,
+ email: cart.email,
+ firstName: cart.billingFirstName,
+ lastName: cart.billingLastName,
+ owner: cart.ownerName || "",
+ organization: cart.orgName || "",
+ addressLine1: cart.billingAddressLine1 || "",
+ addressLine2: cart.billingAddressLine2 || "",
+ city: cart.billingCity || "",
+ state: cart.billingState || "",
+ zip: cart.billingZipCode || "",
+ referral: cart.couponCode,
+ cents: cart.total*100, // cents here.
+ numUsers: cart.userCount,
+ purchaseType: cart.billingPurchaseType,
+ }
+ cart.customerId = _recordPurchase(purchase);
+ return purchase;
+}
+
+function _performCreditCardPurchase() {
+ var cart = _cart();
+ var purchase = _generatePurchaseRecord();
+ var payInfo = checkout.generatePayInfo(cart);
+
+ // log everything but the CVV, which we're not allowed to store
+ // any longer than it takes to process this transaction.
+ var savedCvv = payInfo.cardCvv;
+ delete payInfo.cardCvv;
+ checkout.writeToEncryptedLog(fastJSON.stringify({date: String(new Date()), purchase: purchase, customerId: cart.customerId, payInfo: payInfo}));
+ payInfo.cardCvv = savedCvv;
+
+ var result =
+ billing.directPurchase(cart.invoiceId, cart.customerId,
+ "EEPNET", cart.total || 0.01,
+ cart.couponCode || "",
+ payInfo, _absoluteUrl('paypalnotify'));
+
+ if (result.status == 'success') {
+ cart.status = 'success';
+ cart.purchaseComplete = true;
+ eepnet_checkout.generateLicenseKey(cart);
+ eepnet_checkout.sendReceiptEmail(cart);
+ eepnet_checkout.sendLicenseEmail(cart);
+ billing.log({type: 'purchase-complete', dollars: cart.total,
+ email: cart.email, user: cart.ownerName,
+ org: cart.organization});
+ // TODO: generate key and include in receipt page, and add to purchase table.
+ } else if (result.status == 'pending') {
+ cart.status = 'pending';
+ cart.purchaseComplete = true;
+ eepnet_checkout.sendReceiptEmail(cart);
+ // save the receipt email text to resend later.
+ eepnet_checkout.updatePurchaseWithReceipt(cart.customerId,
+ eepnet_checkout.receiptEmailText(cart));
+ } else if (result.status == 'failure') {
+ var paypalResult = result.debug;
+ billing.log({'type': 'FATAL', value: "Direct purchase failed on paypal.", cart: cart, paypal: paypalResult});
+ if (result.errorField.permanentErrors[0] == 'invoiceId') {
+ // repeat invoice id. damnit, this is bad.
+ sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'DUPLICATE INVOICE WARNING!', {},
+ "Hey,\n\nThis is a billing system error. The EEPNET checkout tried to make a "+
+ "purchase with PayPal and got a duplicate invoice error on invoice ID "+cart.invoiceId+
+ ".\n\nUnless you're expecting this (or recently ran a selenium test, or have reason to "+
+ "believe this isn't an exceptional condition, please look into this "+
+ "and get back to the user ASAP!\n\n"+fastJSON.stringify(cart));
+ _validationError('', "Your payment was processed, but we cannot proceed. "+
+ "You will hear from us shortly via email. (If you don't hear from us "+
+ "within 24 hours, please email <a href='mailto:sales@pad.spline.inf.fu-berlin.de'>"+
+ "sales@pad.spline.inf.fu-berlin.de</a>.)");
+ }
+ checkout.validateErrorFields(function(x, y) { _validationError(x, y, 'billing-info') }, "There seems to be an error in your billing information."+
+ " Please verify and correct your ",
+ result.errorField.userErrors);
+ checkout.validateErrorFields(function(x, y) { _validationError(x, y, 'billing-info') }, "The bank declined your billing information. Please try a different ",
+ result.errorField.permanentErrors);
+ _validationError('', "A temporary error has prevented processing of your payment. Please try again later.");
+ } else {
+ billing.log({'type': 'FATAL', value: "Unknown error: "+result.status+" - debug: "+result.debug});
+ sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'UNKNOWN ERROR WARNING!', {},
+ "Hey,\n\nThis is a billing system error. Some unknown error occurred. "+
+ "This shouldn't ever happen. Probably good to let J.D. know. <grin>\n\n"+
+ fastJSON.stringify(cart));
+ _validationError('', "An unknown error occurred. We're looking into it!")
+ }
+}
+
+function _completePaypalPurchase() {
+ var cart = _cart();
+ var purchaseInfo = cart.paypalPurchaseInfo;
+ var payerInfo = cart.paypalPayerInfo;
+
+ var result = billing.completeExpressPurchase(purchaseInfo, payerInfo, _absoluteUrl('paypalnotify'));
+ if (result.status == 'success') {
+ cart.status = 'success';
+ cart.purchaseComplete = true;
+ eepnet_checkout.generateLicenseKey(cart);
+ eepnet_checkout.sendReceiptEmail(cart);
+ eepnet_checkout.sendLicenseEmail(cart);
+ billing.log({type: 'purchase-complete', dollars: cart.total,
+ email: cart.email, user: cart.ownerName,
+ org: cart.organization});
+
+ } else if (result.status == 'pending') {
+ cart.status = 'pending';
+ cart.purchaseComplete = true;
+ eepnet_checkout.sendReceiptEmail(cart);
+ // save the receipt email text to resend later.
+ eepnet_checkout.updatePurchaseWithReceipt(cart.customerId,
+ eepnet_checkout.receiptEmailText(cart));
+ } else {
+ billing.log({'type': 'FATAL', value: "Paypal failed.", cart: cart, paypal: paypalResult});
+ _validationError("billingPurchaseType",
+ "There was an error processing your payment through PayPal. "+
+ "Please try again later, or use a different payment option.",
+ 'billing-info');
+ }
+}
+
+function _showReceipt() {
+ response.redirect(_pathTo('receipt'));
+}
+
+function render_confirmation_post() {
+ var cart = _cart();
+
+ _updateCosts(); // no fishy business, please.
+
+ if (cart.billingPurchaseType == 'creditcard') {
+ _performCreditCardPurchase();
+ _showReceipt();
+ } else if (cart.billingPurchaseType == 'paypal') {
+ _completePaypalPurchase();
+ _showReceipt();
+ }
+}
+
+//--------------------------------------------------------------------------------
+// receipt
+//--------------------------------------------------------------------------------
+
+function render_receipt_post() {
+ response.redirect(request.path);
+}
diff --git a/etherpad/src/etherpad/control/store/storecontrol.js b/etherpad/src/etherpad/control/store/storecontrol.js
new file mode 100644
index 0000000..43569e4
--- /dev/null
+++ b/etherpad/src/etherpad/control/store/storecontrol.js
@@ -0,0 +1,201 @@
+/**
+ * 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.
+ */
+
+import("dispatch.{Dispatcher,DirMatcher,forward}");
+import("fastJSON");
+import("funhtml.*");
+
+import('etherpad.globals.*');
+import("etherpad.store.eepnet_trial");
+import("etherpad.store.eepnet_checkout");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+
+import("etherpad.control.store.eepnet_checkout_control");
+import("etherpad.control.pro.admin.team_billing_control");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+function onStartup() {}
+
+function onRequest() {
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [DirMatcher('/ep/store/eepnet-checkout/'), forward(eepnet_checkout_control)],
+ ]);
+ return disp.dispatch();
+}
+
+//----------------------------------------------------------------
+
+function render_main() {
+ response.redirect("/ep/about/pricing");
+}
+
+//----------------------------------------------------------------
+// Flow goes through these 4 pages in order:
+//----------------------------------------------------------------
+
+function render_eepnet_eval_signup_get() {
+ renderFramed("store/eepnet_eval_signup.ejs", {
+ trialDays: eepnet_trial.getTrialDays(),
+ oldData: (getSession().pricingContactData || {}),
+ sfIndustryList: eepnet_trial.getSalesforceIndustryList()
+ });
+ delete getSession().errorMsg;
+}
+
+// function render_eepnet_eval_signup_post() {
+// response.setContentType("text/plain; charset=utf-8");
+// var data = {};
+// var fields = ['firstName', 'lastName', 'email', 'orgName',
+// 'jobTitle', 'phone', 'estUsers', 'industry'];
+//
+// if (!getSession().pricingContactData) {
+// getSession().pricingContactData = {};
+// }
+//
+// function _redirectErr(msg) {
+// response.write(fastJSON.stringify({error: msg}));
+// response.stop();
+// }
+//
+// fields.forEach(function(f) {
+// getSession().pricingContactData[f] = request.params[f];
+// });
+//
+// fields.forEach(function(f) {
+// data[f] = request.params[f];
+// if (!(data[f] && (data[f].length > 0))) {
+// _redirectErr("All fields are required.");
+// }
+// });
+//
+// // validate email
+// if (!isValidEmail(data.email)) {
+// _redirectErr("That email address doesn't look valid.");
+// }
+//
+// // check that email not already registered.
+// if (eepnet_trial.hasEmailAlreadyDownloaded(data.email)) {
+// _redirectErr("That email has already downloaded a free trial."+
+// ' <a href="/ep/store/eepnet-recover-license">Recover a lost license key here</a>.');
+// }
+//
+// // Looks good! Create and email license key...
+// eepnet_trial.createAndMailNewLicense(data);
+// getSession().message = "A license key has been sent to "+data.email;
+//
+// // Generate web2lead info and return it
+// var web2leadData = eepnet_trial.getWeb2LeadData(data, request.clientAddr, getSession().initialReferer);
+// response.write(fastJSON.stringify(web2leadData));
+// }
+//
+// function render_salesforce_web2lead_ok() {
+// renderFramedHtml([
+// '<script>',
+// 'top.location.href = "'+request.scheme+'://'+request.host+'/ep/store/eepnet-download";',
+// '</script>'
+// ].join('\n'));
+// }
+//
+// function render_eepnet_eval_download() {
+// // NOTE: keep this URL around for historical reasons?
+// response.redirect("/ep/store/eepnet-download");
+// }
+//
+// function render_eepnet_download() {
+// renderFramed("store/eepnet_download.ejs", {
+// message: (getSession().message || null),
+// versionString: (PNE_RELEASE_VERSION+"&nbsp;("+PNE_RELEASE_DATE +")")
+// });
+// delete getSession().message;
+// }
+//
+// function render_eepnet_download_zip() {
+// response.redirect("/static/zip/pne-release/etherpad-pne-"+PNE_RELEASE_VERSION+".zip");
+// }
+//
+// function render_eepnet_download_nextsteps() {
+// renderFramed("store/eepnet_eval_nextsteps.ejs");
+// }
+
+//----------------------------------------------------------------
+// recover a lost license
+//----------------------------------------------------------------
+function render_eepnet_recover_license_get() {
+ var d = DIV({className: "fpcontent"});
+
+ d.push(P("Recover your lost license key."));
+
+ if (getSession().message) {
+ d.push(DIV({id: "resultmsg",
+ style: "border: 1px solid #333; padding: 0 1em; background: #efe; margin: 1em 0;"}, getSession().message));
+ delete getSession().message;
+ }
+ if (getSession().error) {
+ d.push(DIV({id: "errormsg",
+ style: "border: 1px solid red; padding: 0 1em; background: #fee; margin: 1em 0;"}, getSession().error));
+ delete getSession().error;
+ }
+
+ d.push(FORM({style: "border: 1px solid #222; padding: 2em; background: #eee;",
+ action: request.path, method: "post"},
+ LABEL({htmlFor: "email"},
+ "Your email address:"),
+ INPUT({type: "text", name: "email", id: "email"}),
+ INPUT({type: "submit", id: "submit", value: "Submit"})));
+
+ renderFramedHtml(d);
+}
+
+function render_eepnet_recover_license_post() {
+ var email = request.params.email;
+ if (!eepnet_trial.hasEmailAlreadyDownloaded(email) && !eepnet_trialhasEmailAlreadyPurchased(email)) {
+ getSession().error = P("License not found for email: \"", email, "\".");
+ response.redirect(request.path);
+ }
+ if (eepnet_checkout.hasEmailAlreadyPurchased(email)) {
+ eepnet_checkout.mailLostLicense(email);
+ } else if (eepnet_trial.hasEmailAlreadyDownloaded(email)) {
+ eepnet_trial.mailLostLicense(email);
+ }
+ getSession().message = P("Your license information has been sent to ", email, ".");
+ response.redirect(request.path);
+}
+
+//----------------------------------------------------------------
+function render_eepnet_purchase_get() {
+ renderFramed("store/eepnet_purchase.ejs", {});
+}
+
+//--------------------------------------------------------------------------------
+// csc-help page
+//--------------------------------------------------------------------------------
+
+function render_csc_help_get() {
+ response.write(renderTemplateAsString("store/csc-help.ejs"));
+}
+
+//--------------------------------------------------------------------------------
+// paypal notifications for pro
+//--------------------------------------------------------------------------------
+
+function render_paypalnotify() {
+ team_billing_control.handlePaypalNotify();
+}
diff --git a/etherpad/src/etherpad/control/testcontrol.js b/etherpad/src/etherpad/control/testcontrol.js
new file mode 100644
index 0000000..ed13006
--- /dev/null
+++ b/etherpad/src/etherpad/control/testcontrol.js
@@ -0,0 +1,74 @@
+/**
+ * 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.
+ */
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+var tests = [
+ "t0000_test",
+ "t0001_sqlbase_transaction_rollback",
+ "t0002_license_generation",
+ "t0003_persistent_vars",
+ "t0004_sqlobj",
+ "t0005_easysync"
+];
+
+var tscope = this;
+tests.forEach(function(t) {
+ import.call(tscope, 'etherpad.testing.unit_tests.'+t);
+});
+//----------------------------------------------------------------
+
+function _testName(x) {
+ x = x.replace(/^t\d+\_/, '');
+ return x;
+}
+
+function render_run() {
+ response.setContentType("text/plain; charset=utf-8");
+ if (isProduction() && (request.params.p != "waverunner")) {
+ response.write("access denied");
+ response.stop();
+ }
+
+ var singleTest = request.params.t;
+ var numRun = 0;
+
+ println("----------------------------------------------------------------");
+ println("running tests");
+ println("----------------------------------------------------------------");
+ tests.forEach(function(t) {
+ var testName = _testName(t);
+ if (singleTest && (singleTest != testName)) {
+ return;
+ }
+ println("running test: "+testName);
+ numRun++;
+ tscope[t].run();
+ println("|| pass ||");
+ });
+ println("----------------------------------------------------------------");
+
+ if (numRun == 0) {
+ response.write("Error: no tests found");
+ } else {
+ response.write("OK");
+ }
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0000_test.js b/etherpad/src/etherpad/db_migrations/m0000_test.js
new file mode 100644
index 0000000..7df9bfd
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0000_test.js
@@ -0,0 +1,23 @@
+/**
+ * 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.
+ */
+
+
+function run() {
+ // nothing
+}
+
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js b/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js
new file mode 100644
index 0000000..0e65779
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js
@@ -0,0 +1,38 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('eepnet_signups', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ email: 'VARCHAR(128) NOT NULL UNIQUE',
+ date: 'TIMESTAMP',
+ signupIp: 'VARCHAR(16)',
+ fullName: 'VARCHAR(255) NOT NULL',
+ orgName: 'VARCHAR(255) NOT NULL',
+ jobTitle: 'VARCHAR(255) NOT NULL',
+ estUsers: 'VARCHAR(255) NOT NULL',
+ licenseKey: 'VARCHAR(1024) NOT NULL'
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js b/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js
new file mode 100644
index 0000000..786e4e9
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js
@@ -0,0 +1,47 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ // add new columns.
+ sqlobj.addColumns('eepnet_signups', {
+ firstName: 'VARCHAR(128) NOT NULL DEFAULT \'\'',
+ lastName: 'VARCHAR(128) NOT NULL DEFAULT \'\'',
+ phone: 'VARCHAR(128) NOT NULL DEFAULT \'\''
+ });
+
+ // split name into first/last
+ var rows = sqlobj.selectMulti('eepnet_signups', {}, {});
+ rows.forEach(function(r) {
+ var name = r.fullName;
+ r.firstName = (r.fullName.split(' ')[0]) || "?";
+ r.lastName = (r.fullName.split(' ').slice(1).join(' ')) || "?";
+ r.phone = "?";
+ sqlobj.updateSingle('eepnet_signups', {id: r.id}, r);
+ });
+
+ // drop column fullName
+ sqlobj.dropColumn('eepnet_signups', 'fullName');
+}
+
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js b/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js
new file mode 100644
index 0000000..f121145
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js
@@ -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.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (sqlcommon.doesTableExist('just_a_test')) {
+ sqlobj.dropTable('just_a_test');
+ }
+ sqlobj.createTable('just_a_test', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ x: 'VARCHAR(128)'
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js b/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js
new file mode 100644
index 0000000..959865d
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js
@@ -0,0 +1,38 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+import('etherpad.db_migrations.migration_runner.dmesg');
+
+function run() {
+ // This migration only applies to MySQL
+ if (!sqlcommon.isMysql()) {
+ return;
+ }
+
+ var tables = sqlobj.listTables();
+ tables.forEach(function(t) {
+ if (sqlobj.getTableEngine(t) != "InnoDB") {
+ dmesg("Converting table "+t+" to InnoDB...");
+ sqlobj.setTableEngine(t, "InnoDB");
+ }
+ });
+
+};
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js b/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js
new file mode 100644
index 0000000..0dfd37e
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js
@@ -0,0 +1,73 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('billing_purchase', {
+ id: idColspec,
+ type: "ENUM('onetimepurchase', 'subscription')",
+ customer: "INT(11) NOT NULL",
+ product: "VARCHAR(128) NOT NULL",
+ cost: "INT(11) NOT NULL",
+ coupon: "VARCHAR(128) NOT NULL",
+ time: "DATETIME",
+ paidThrough: "DATETIME",
+ status: "ENUM('active', 'inactive')"
+ }, {
+ type: true,
+ customer: true,
+ product: true
+ });
+
+ sqlobj.createTable('billing_invoice', {
+ id: idColspec,
+ time: "DATETIME",
+ purchase: "INT(11) NOT NULL",
+ amt: "INT(11) NOT NULL",
+ status: "ENUM('pending', 'paid', 'void', 'refunded')"
+ }, {
+ status: true
+ });
+
+ sqlobj.createTable('billing_transaction', {
+ id: idColspec,
+ customer: "INT(11)",
+ time: "DATETIME",
+ amt: "INT(11)",
+ payInfo: "VARCHAR(128)",
+ txnId: "VARCHAR(128)", // depends on gateway used?
+ status: "ENUM('new', 'success', 'failure', 'pending')"
+ }, {
+ customer: true,
+ txnId: true
+ });
+
+ sqlobj.createTable('billing_adjustment', {
+ id: idColspec,
+ transaction: "INT(11)",
+ invoice: "INT(11)",
+ time: "DATETIME",
+ amt: "INT(11)"
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js b/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js
new file mode 100644
index 0000000..349b27a
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js
@@ -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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ // add new columns.
+ sqlobj.addColumns('eepnet_signups', {
+ industry: 'VARCHAR(128)',
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js b/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js
new file mode 100644
index 0000000..bda5853
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js
@@ -0,0 +1,67 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ ['pro_domains', 'pro_users', 'pro_padmeta'].forEach(function(t) {
+ if (sqlcommon.doesTableExist(t)) {
+ sqlobj.dropTable(t);
+ }
+ });
+
+ sqlobj.createTable('pro_domains', {
+ id: sqlobj.getIdColspec(),
+ subDomain: 'VARCHAR(128) UNIQUE NOT NULL',
+ extDomain: 'VARCHAR(128) DEFAULT NULL',
+ orgName: 'VARCHAR(128)'
+ });
+
+ sqlobj.createIndex('pro_domains', ['subDomain']);
+ sqlobj.createIndex('pro_domains', ['extDomain']);
+
+ sqlobj.createTable('pro_users', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT NOT NULL',
+ fullName: 'VARCHAR(128) NOT NULL',
+ email: 'VARCHAR(128) NOT NULL', // not unique because same
+ // email can be on multiple domains.
+ passwordHash: 'VARCHAR(128) NOT NULL',
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastLoginDate: sqlobj.getDateColspec("DEFAULT NULL"),
+ isAdmin: sqlobj.getBoolColspec("DEFAULT 0")
+ });
+
+ sqlobj.createTable('pro_padmeta', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT NOT NULL',
+ localPadId: 'VARCHAR(128) NOT NULL',
+ title: 'VARCHAR(128)',
+ creatorId: 'INT DEFAULT NULL',
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastEditorId: 'INT DEFAULT NULL',
+ lastEditedDate: sqlobj.getDateColspec("DEFAULT NULL")
+ });
+
+ sqlobj.createIndex('pro_padmeta', ['domainId', 'localPadId']);
+
+ var pneDomain = "<<private-network>>";
+ if (!sqlobj.selectSingle('pro_domains', {subDomain: pneDomain})) {
+ sqlobj.insert('pro_domains', {subDomain: pneDomain});
+ }
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js b/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js
new file mode 100644
index 0000000..30e379a
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js
@@ -0,0 +1,31 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+
+ var idColspec = 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY';
+
+ sqlobj.createTable('persistent_vars', {
+ id: idColspec,
+ name: 'VARCHAR(128) UNIQUE NOT NULL',
+ stringVal: 'VARCHAR(1024)'
+ });
+
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js b/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js
new file mode 100644
index 0000000..93f5a62
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js
@@ -0,0 +1,31 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlbase");
+
+function run() {
+
+ // These table creations used to be in etherpad.pad.model.onStartup, but
+ // they make more sense here because later migrations access these tables.
+ sqlbase.createJSONTable("PAD_META");
+ sqlbase.createJSONTable("PAD_APOOL");
+ sqlbase.createStringArrayTable("PAD_REVS");
+ sqlbase.createStringArrayTable("PAD_CHAT");
+ sqlbase.createStringArrayTable("PAD_REVMETA");
+ sqlbase.createStringArrayTable("PAD_AUTHORS");
+
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js b/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js
new file mode 100644
index 0000000..36150b1
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js
@@ -0,0 +1,71 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlbase");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("etherpad.utils.startConsoleProgressBar");
+
+
+function run() {
+
+ sqlobj.dropAndCreateTable('PAD_SQLMETA', {
+ id: 'VARCHAR(128) PRIMARY KEY NOT NULL',
+ version: 'INT NOT NULL',
+ creationTime: sqlobj.getDateColspec('NOT NULL'),
+ lastWriteTime: sqlobj.getDateColspec('NOT NULL'),
+ headRev: 'INT NOT NULL'
+ });
+
+ sqlobj.createIndex('PAD_SQLMETA', ['version']);
+
+ var allPadIds = sqlbase.getAllJSONKeys("PAD_META");
+
+ // If this is a new database, there are no pads; else
+ // it is an old database with version 1 pads.
+ if (allPadIds.length == 0) {
+ return;
+ }
+
+ var numPadsTotal = allPadIds.length;
+ var numPadsSoFar = 0;
+ var progressBar = startConsoleProgressBar();
+
+ allPadIds.forEach(function(padId) {
+ var meta = sqlbase.getJSON("PAD_META", padId);
+
+ sqlobj.insert("PAD_SQLMETA", {
+ id: padId,
+ version: 1,
+ creationTime: new Date(meta.creationTime || 0),
+ lastWriteTime: new Date(),
+ headRev: meta.head
+ });
+
+ delete meta.creationTime; // now stored in SQLMETA
+ delete meta.version; // just in case (was used during development)
+ delete meta.dirty; // no longer stored in DB
+ delete meta.lastAccess; // no longer stored in DB
+
+ sqlbase.putJSON("PAD_META", padId, meta);
+
+ numPadsSoFar++;
+ progressBar.update(numPadsSoFar/numPadsTotal, numPadsSoFar+"/"+numPadsTotal+" pads");
+ });
+
+ progressBar.finish();
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js b/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js
new file mode 100644
index 0000000..5ac8b26
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js
@@ -0,0 +1,33 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ // allow null values in passwordHash
+ if (sqlcommon.isDerby()) {
+ sqlobj.alterColumn('pro_users', 'passwordHash', 'NULL');
+ } else {
+ sqlobj.modifyColumn('pro_users', 'passwordHash', 'VARCHAR(128)');
+ }
+ sqlobj.addColumns('pro_users', {
+ tempPassHash: 'VARCHAR(128)'
+ });
+}
+
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js b/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js
new file mode 100644
index 0000000..ddd4cf6
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js
@@ -0,0 +1,30 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_users_auto_signin', {
+ id: sqlobj.getIdColspec(),
+ cookie: 'VARCHAR(128) UNIQUE NOT NULL',
+ userId: 'INT UNIQUE NOT NULL',
+ expires: sqlobj.getDateColspec('NOT NULL')
+ });
+ sqlobj.createIndex('pro_users_auto_signin', ['cookie']);
+ sqlobj.createIndex('pro_users_auto_signin', ['userId']);
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js b/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js
new file mode 100644
index 0000000..146923a
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js
@@ -0,0 +1,54 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.startConsoleProgressBar");
+import("etherpad.pad.easysync2migration");
+import("etherpad.pne.pne_utils");
+import("sqlbase.sqlobj");
+import("etherpad.log");
+
+function run() {
+
+ // this is a PNE-only migration
+ if (! pne_utils.isPNE()) {
+ return;
+ }
+
+ var migrationsNeeded = sqlobj.selectMulti("PAD_SQLMETA", {version: 1});
+
+ if (migrationsNeeded.length == 0) {
+ return;
+ }
+
+ var migrationsTotal = migrationsNeeded.length;
+ var migrationsSoFar = 0;
+ var progressBar = startConsoleProgressBar();
+
+ migrationsNeeded.forEach(function(obj) {
+ var padId = String(obj.id);
+
+ log.info("Migrating pad "+padId+" from version 1 to version 2...");
+ easysync2migration.migratePad(padId);
+ sqlobj.update("PAD_SQLMETA", {id: padId}, {version: 2});
+ log.info("Migrated pad "+padId+".");
+
+ migrationsSoFar++;
+ progressBar.update(migrationsSoFar/migrationsTotal, migrationsSoFar+"/"+migrationsTotal+" pads");
+ });
+
+ progressBar.finish();
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js b/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js
new file mode 100644
index 0000000..445b32d
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js
@@ -0,0 +1,102 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.startConsoleProgressBar");
+import("etherpad.pne.pne_utils");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlbase");
+import("etherpad.log");
+import("sqlbase.sqlcommon.*");
+import("etherpad.pad.padutils");
+
+function run() {
+
+ // this is a PNE-only migration
+ if (! pne_utils.isPNE()) {
+ return;
+ }
+
+ var renamesNeeded = sqlobj.selectMulti("PAD_SQLMETA", {});
+
+ if (renamesNeeded.length == 0) {
+ return;
+ }
+
+ var renamesTotal = renamesNeeded.length;
+ var renamesSoFar = 0;
+ var progressBar = startConsoleProgressBar();
+
+ renamesNeeded.forEach(function(obj) {
+ var oldPadId = String(obj.id);
+ var newPadId;
+ if (/^1\$[a-zA-Z0-9\-]+$/.test(oldPadId)) {
+ // not expecting a user pad beginning with "1$";
+ // this case is to avoid trashing dev databases
+ newPadId = oldPadId;
+ }
+ else {
+ var localPadId = padutils.makeValidLocalPadId(oldPadId);
+ newPadId = "1$"+localPadId;
+
+ // PAD_SQLMETA
+ obj.id = newPadId;
+ sqlobj.deleteRows("PAD_SQLMETA", {id:oldPadId});
+ sqlobj.insert("PAD_SQLMETA", obj);
+
+ // PAD_META
+ var meta = sqlbase.getJSON("PAD_META", oldPadId);
+ meta.padId = newPadId;
+ sqlbase.deleteJSON("PAD_META", oldPadId);
+ sqlbase.putJSON("PAD_META", newPadId, meta);
+
+ // PAD_APOOL
+ var apool = sqlbase.getJSON("PAD_APOOL", oldPadId);
+ sqlbase.deleteJSON("PAD_APOOL", oldPadId);
+ sqlbase.putJSON("PAD_APOOL", newPadId, apool);
+
+ function renamePadInStringArrayTable(arrayName) {
+ var stmnt = "UPDATE "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " SET "+btquote("ID")+" = ? WHERE "+btquote("ID")+" = ?";
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setString(1, newPadId);
+ pstmnt.setString(2, oldPadId);
+ pstmnt.executeUpdate();
+ });
+ });
+ }
+
+ renamePadInStringArrayTable("revs");
+ renamePadInStringArrayTable("chat");
+ renamePadInStringArrayTable("revmeta");
+ renamePadInStringArrayTable("authors");
+
+ sqlobj.insert('pro_padmeta', {
+ localPadId: localPadId,
+ title: localPadId,
+ createdDate: obj.creationTime,
+ domainId: 1 // PNE
+ });
+ }
+
+ renamesSoFar++;
+ progressBar.update(renamesSoFar/renamesTotal, renamesSoFar+"/"+renamesTotal+" pads");
+ });
+
+ progressBar.finish();
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js b/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js
new file mode 100644
index 0000000..8fa98bb
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js
@@ -0,0 +1,25 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ password: 'VARCHAR(128) DEFAULT NULL'
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js b/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js
new file mode 100644
index 0000000..abcc93f
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js
@@ -0,0 +1,35 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('pne_tracking_data', {
+ id: sqlobj.getIdColspec(),
+ date: sqlobj.getDateColspec("NOT NULL"),
+ keyHash: 'VARCHAR(128) DEFAULT NULL',
+ name: 'VARCHAR(128) NOT NULL',
+ value: 'VARCHAR(1024) NOT NULL'
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js b/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js
new file mode 100644
index 0000000..1067840
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js
@@ -0,0 +1,30 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.addColumns('pne_tracking_data', {
+ remoteIp: 'VARCHAR(128) NOT NULL'
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js b/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js
new file mode 100644
index 0000000..6e10000
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js
@@ -0,0 +1,82 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('checkout_purchase', {
+ id: idColspec,
+ invoiceId: "INT NOT NULL",
+ owner: "VARCHAR(128) NOT NULL",
+ email: "VARCHAR(128) NOT NULL",
+ organization: "VARCHAR(128) NOT NULL",
+ firstName: "VARCHAR(100) NOT NULL",
+ lastName: "VARCHAR(100) NOT NULL",
+ addressLine1: "VARCHAR(100) NOT NULL",
+ addressLine2: "VARCHAR(100) NOT NULL",
+ city: "VARCHAR(40) NOT NULL",
+ state: "VARCHAR(2) NOT NULL",
+ zip: "VARCHAR(10) NOT NULL",
+ numUsers: "INT NOT NULL",
+ date: "TIMESTAMP NOT NULL",
+ cents: "INT NOT NULL",
+ referral: "VARCHAR(8)",
+ receiptEmail: "TEXT",
+ purchaseType: "ENUM('creditcard', 'invoice', 'paypal') NOT NULL",
+ licenseKey: "VARCHAR(1024)"
+ }, {
+ email: true,
+ invoiceId: true
+ });
+
+ sqlobj.createTable('checkout_referral', {
+ id: "VARCHAR(8) NOT NULL PRIMARY KEY",
+ productPctDiscount: "INT",
+ supportPctDiscount: "INT",
+ totalPctDiscount: "INT",
+ freeUsersCount: "INT",
+ freeUsersPct: "INT"
+ });
+
+ // add a sample referral code.
+ sqlobj.insert('checkout_referral', {
+ id: 'EPCO6128',
+ productPctDiscount: 50,
+ supportPctDiscount: 25,
+ totalPctDiscount: 15,
+ freeUsersCount: 20,
+ freeUsersPct: 10
+ });
+
+ // add a "free" referral code.
+ sqlobj.insert('checkout_referral', {
+ id: 'EP99FREE',
+ totalPctDiscount: 99
+ });
+
+ sqlobj.insert('checkout_referral', {
+ id: 'EPFREE68',
+ totalPctDiscount: 100
+ });
+
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js b/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js
new file mode 100644
index 0000000..1f9ecbb
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js
@@ -0,0 +1,24 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ isDeleted: sqlobj.getBoolColspec("NOT NULL DEFAULT 0")
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js b/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js
new file mode 100644
index 0000000..a776622
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js
@@ -0,0 +1,25 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ isArchived: sqlobj.getBoolColspec("NOT NULL DEFAULT 0")
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js b/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js
new file mode 100644
index 0000000..9f357b7
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js
@@ -0,0 +1,57 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ proAttrsJson: sqlobj.getLongtextColspec("")
+ });
+
+ // convert all existing columns into metaJSON
+
+ sqlcommon.inTransaction(function() {
+ var records = sqlobj.selectMulti('pro_padmeta', {}, {});
+ records.forEach(function(r) {
+ migrateRecord(r);
+ });
+ });
+}
+
+function migrateRecord(r) {
+ var editors = [];
+ if (r.creatorId) {
+ editors.push(r.creatorId);
+ }
+ if (r.lastEditorId) {
+ if (editors.indexOf(r.lastEditorId) < 0) {
+ editors.push(r.lastEditorId);
+ }
+ }
+ editors.sort();
+
+ var proAttrs = {
+ editors: editors,
+ };
+
+ var proAttrsJson = fastJSON.stringify(proAttrs);
+
+ sqlobj.update('pro_padmeta', {id: r.id}, {proAttrsJson: proAttrsJson});
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js b/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js
new file mode 100644
index 0000000..23ca8d3
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js
@@ -0,0 +1,30 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('pad_cookie_userids', {
+ id: "VARCHAR(40) NOT NULL PRIMARY KEY",
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastActiveDate: sqlobj.getDateColspec("NOT NULL")
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js b/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js
new file mode 100644
index 0000000..927cdc9
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js
@@ -0,0 +1,32 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('usage_stats', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) NOT NULL',
+ timestamp: 'INT NOT NULL',
+ value: 'INT NOT NULL'
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js b/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js
new file mode 100644
index 0000000..9d6e58c
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js
@@ -0,0 +1,42 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("fastJSON");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('statistics', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) NOT NULL',
+ timestamp: 'INT NOT NULL',
+ value: 'TEXT NOT NULL'
+ });
+
+ var oldStats = sqlobj.selectMulti('usage_stats', {});
+ oldStats.forEach(function(stat) {
+ sqlobj.insert('statistics', {
+ timestamp: stat.timestamp,
+ name: stat.name,
+ value: fastJSON.stringify({value: stat.value})
+ });
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js b/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js
new file mode 100644
index 0000000..a429f41
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js
@@ -0,0 +1,26 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ sqlobj.renameTable('pro_users', 'pro_accounts');
+ sqlobj.renameTable('pro_users_auto_signin', 'pro_accounts_auto_signin');
+ sqlobj.changeColumn('pro_accounts_auto_signin', 'userId', 'accountId INT UNIQUE NOT NULL');
+ sqlobj.createIndex('pro_accounts_auto_signin', ['accountId']);
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js b/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js
new file mode 100644
index 0000000..7c41309
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js
@@ -0,0 +1,37 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (sqlcommon.doesTableExist("pad_guests")) {
+ sqlobj.dropTable("pad_guests");
+ }
+
+ sqlobj.createTable('pad_guests', {
+ id: sqlobj.getIdColspec(),
+ privateKey: 'VARCHAR(63) UNIQUE NOT NULL',
+ userId: 'VARCHAR(63) UNIQUE NOT NULL',
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastActiveDate: sqlobj.getDateColspec("NOT NULL"),
+ data: sqlobj.getLongtextColspec("")
+ });
+
+ sqlobj.createIndex('pad_guests', ['privateKey']);
+ sqlobj.createIndex('pad_guests', ['userId']);
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0027_pro_config.js b/etherpad/src/etherpad/db_migrations/m0027_pro_config.js
new file mode 100644
index 0000000..9cbb629
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0027_pro_config.js
@@ -0,0 +1,27 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_config', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT',
+ name: 'VARCHAR(128)',
+ jsonVal: sqlobj.getLongtextColspec("")
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js b/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js
new file mode 100644
index 0000000..f708363
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js
@@ -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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_beta_signups', {
+ id: sqlobj.getIdColspec(),
+ email: 'VARCHAR(256)',
+ activationCode: 'VARCHAR(128)',
+ isActivated: sqlobj.getBoolColspec(),
+ signupDate: sqlobj.getDateColspec(),
+ activationDate: sqlobj.getDateColspec()
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js b/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js
new file mode 100644
index 0000000..36b76ab
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js
@@ -0,0 +1,31 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ var recordList = sqlobj.selectMulti('pro_domains', {});
+ recordList.forEach(function(r) {
+ var subDomain = r.subDomain;
+ if (subDomain != subDomain.toLowerCase()) {
+ // delete this domain record and all accounts associated with it.
+ sqlobj.deleteRows('pro_domains', {id: r.id});
+ sqlobj.deleteRows('pro_accounts', {domainId: r.id});
+ }
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js b/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js
new file mode 100644
index 0000000..aeaa40f
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js
@@ -0,0 +1,26 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.modifyColumn('statistics', 'value', 'MEDIUMTEXT NOT NULL');
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js b/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js
new file mode 100644
index 0000000..b9744a3
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js
@@ -0,0 +1,24 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_accounts', {
+ isDeleted: sqlobj.getBoolColspec("NOT NULL DEFAULT 0")
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js b/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js
new file mode 100644
index 0000000..5e748f5
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js
@@ -0,0 +1,39 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+import("fastJSON");
+
+import("etherpad.statistics.statistics");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ statistics.getAllStatNames().forEach(function(statName) {
+ if (statistics.getStatData(statName).dataType == 'topValues') {
+ var entries = sqlobj.selectMulti('statistics', {name: statName});
+ entries.forEach(function(statEntry) {
+ var value = fastJSON.parse(statEntry.value);
+ value.topValues = value.topValues.slice(0, 50);
+ statEntry.value = fastJSON.stringify(value);
+ sqlobj.update('statistics', {id: statEntry.id}, statEntry);
+ });
+ }
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js b/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js
new file mode 100644
index 0000000..4b33f52
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js
@@ -0,0 +1,30 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_account_usage', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT NOT NULL UNIQUE',
+ count: 'INT NOT NULL DEFAULT 0',
+ lastReset: sqlobj.getDateColspec(),
+ lastUpdated: sqlobj.getDateColspec()
+ });
+ sqlobj.createIndex('pro_account_usage', ['domainId']);
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js b/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js
new file mode 100644
index 0000000..491581b
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js
@@ -0,0 +1,42 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('billing_payment_info', {
+ customer: "INT(11) NOT NULL PRIMARY KEY",
+ fullname: "VARCHAR(128)",
+ paymentsummary: "VARCHAR(128)",
+ expiration: "VARCHAR(6)", // MMYYYY
+ transaction: "VARCHAR(128)"
+ });
+
+ sqlobj.addColumns('billing_purchase', {
+ error: "TEXT"
+ });
+
+ sqlobj.addColumns('billing_invoice', {
+ users: "INT(11)"
+ })
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js b/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js
new file mode 100644
index 0000000..a49e9f9
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js
@@ -0,0 +1,28 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.addColumns('billing_payment_info', {
+ email: "VARCHAR(255)"
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js b/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js
new file mode 100644
index 0000000..ce77734
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js
@@ -0,0 +1,45 @@
+/**
+ * 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.
+ */
+
+import("dateutils");
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var allDomains = sqlobj.selectMulti('pro_domains', {}, {});
+
+ allDomains.forEach(function(domain) {
+ var domainId = domain.id;
+ var accounts = sqlobj.selectMulti('pro_accounts', {domainId: domainId}, {});
+ if (accounts.length > 3) {
+ if (! sqlobj.selectSingle('billing_purchase', {product: "ONDEMAND", customer: domainId}, {})) {
+ sqlobj.insert('billing_purchase', {
+ product: "ONDEMAND",
+ paidThrough: dateutils.noon(new Date(Date.now()-1000*86400)),
+ type: 'subscription',
+ customer: domainId,
+ status: 'inactive',
+ cost: 0,
+ coupon: ""
+ });
+ }
+ }
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js b/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js
new file mode 100644
index 0000000..7a9982c
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js
@@ -0,0 +1,32 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('checkout_pro_referral', {
+ id: "VARCHAR(8) NOT NULL PRIMARY KEY",
+ pctDiscount: "INT",
+ freeUsers: "INT",
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js b/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js
new file mode 100644
index 0000000..1e9a53c
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js
@@ -0,0 +1,26 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlbase");
+
+function run() {
+
+ sqlbase.createStringArrayTable("PAD_REVS10");
+ sqlbase.createStringArrayTable("PAD_REVS100");
+ sqlbase.createStringArrayTable("PAD_REVS1000");
+
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js b/etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js
new file mode 100644
index 0000000..62e8ff7
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js
@@ -0,0 +1,40 @@
+/**
+ * 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.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ sqlobj.createTable('plugin', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) character set utf8 collate utf8_bin UNIQUE NOT NULL'
+ });
+ sqlobj.createTable('hook_type', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) character set utf8 collate utf8_bin UNIQUE NOT NULL'
+ });
+ sqlobj.createTable('hook', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ type_id: 'INT NOT NULL REFERENCES hook_type(id)',
+ name: 'VARCHAR(128) character set utf8 collate utf8_bin NOT NULL'
+ });
+ sqlobj.createTable('plugin_hook', {
+ plugin_id: 'INT NOT NULL REFERENCES plugin(id)',
+ hook_id: 'INT NOT NULL REFERENCES hook(id)',
+ original_name: 'VARCHAR(128) character set utf8 collate utf8_bin'
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/migration_runner.js b/etherpad/src/etherpad/db_migrations/migration_runner.js
new file mode 100644
index 0000000..f4fa861
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/migration_runner.js
@@ -0,0 +1,148 @@
+/**
+ * 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.
+ */
+
+// Database migrations.
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+// 1 migration per file
+//----------------------------------------------------------------
+
+var migrations = [
+ "m0000_test",
+ "m0001_eepnet_signups_init",
+ "m0002_eepnet_signups_2",
+ "m0003_create_tests_table_v2",
+ "m0004_convert_all_tables_to_innodb",
+ "m0005_create_billing_tables",
+ "m0006_eepnet_signups_3",
+ "m0007_create_pro_tables_v4",
+ "m0008_persistent_vars",
+ "m0009_pad_tables",
+ "m0010_pad_sqlmeta",
+ "m0011_pro_users_temppass",
+ "m0012_pro_users_auto_signin",
+ "m0013_pne_padv2_upgrade",
+ "m0014_pne_globalpadids",
+ "m0015_padmeta_passwords",
+ "m0016_pne_tracking_data",
+ "m0017_pne_tracking_data_v2",
+ "m0018_eepnet_checkout_tables",
+ "m0019_padmeta_deleted",
+ "m0020_padmeta_archived",
+ "m0021_pro_padmeta_json",
+ "m0022_create_userids_table",
+ "m0023_create_usagestats_table",
+ "m0024_statistics_table",
+ "m0025_rename_pro_users_table",
+ "m0026_create_guests_table",
+ "m0027_pro_config",
+ "m0028_ondemand_beta_emails",
+ "m0029_lowercase_subdomains",
+ "m0030_fix_statistics_values",
+ "m0031_deleted_pro_users",
+ "m0032_reduce_topvalues_counts",
+ "m0033_pro_account_usage",
+ "m0034_create_recurring_billing_table",
+ "m0035_add_email_to_paymentinfo",
+ "m0036_create_missing_subscription_records",
+ "m0037_create_pro_referral_table",
+ "m0038_pad_coarse_revs",
+ "m0040_create_plugin_tables"
+];
+
+var mscope = this;
+migrations.forEach(function(m) {
+ import.call(mscope, "etherpad.db_migrations."+m);
+});
+
+//----------------------------------------------------------------
+
+function dmesg(m) {
+ if ((!isProduction()) || appjet.cache.db_migrations_print_debug) {
+ log.info(m);
+ println(m);
+ }
+}
+
+function onStartup() {
+ appjet.cache.db_migrations_print_debug = true;
+ if (!sqlcommon.doesTableExist("db_migrations")) {
+ appjet.cache.db_migrations_print_debug = false;
+ sqlobj.createTable('db_migrations', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(255) NOT NULL UNIQUE',
+ completed: 'TIMESTAMP'
+ });
+ }
+
+ if (pne_utils.isPNE()) { pne_utils.checkDbVersionUpgrade(); }
+ runMigrations();
+ if (pne_utils.isPNE()) { pne_utils.saveDbVersion(); }
+}
+
+function _migrationName(m) {
+ m = m.replace(/^m\d+\_/, '');
+ m = m.replace(/\_/g, '-');
+ return m;
+}
+
+function getCompletedMigrations() {
+ var completedMigrationsList = sqlobj.selectMulti('db_migrations', {}, {});
+ var completedMigrations = {};
+
+ completedMigrationsList.forEach(function(c) {
+ completedMigrations[c.name] = true;
+ });
+
+ return completedMigrations;
+}
+
+function runMigrations() {
+ var completedMigrations = getCompletedMigrations();
+
+ dmesg("Checking for database migrations...");
+ migrations.forEach(function(m) {
+ var name = _migrationName(m);
+ if (!completedMigrations[name]) {
+ sqlcommon.inTransaction(function() {
+ dmesg("performing database migration: ["+name+"]");
+ var startTime = +(new Date);
+
+ mscope[m].run();
+
+ var elapsedMs = +(new Date) - startTime;
+ dmesg("migration completed in "+elapsedMs+"ms");
+
+ sqlobj.insert('db_migrations', {
+ name: name,
+ completed: new Date()
+ });
+ });
+ }
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/debug.js b/etherpad/src/etherpad/debug.js
new file mode 100644
index 0000000..069ad14
--- /dev/null
+++ b/etherpad/src/etherpad/debug.js
@@ -0,0 +1,26 @@
+/**
+ * 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.
+ */
+
+import("etherpad.globals.*");
+
+jimport("java.lang.System.out.println");
+
+function dmesg(m) {
+ if (!isProduction()) {
+ println(m);
+ }
+}
+
diff --git a/etherpad/src/etherpad/globals.js b/etherpad/src/etherpad/globals.js
new file mode 100644
index 0000000..fcd5519
--- /dev/null
+++ b/etherpad/src/etherpad/globals.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2009 Google Inc.
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * 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.
+ */
+
+//----------------------------------------------------------------
+// global variabls
+//----------------------------------------------------------------
+
+var COMETPATH = "/comet";
+
+var COLOR_PALETTE = ['#ffc7c7','#fff1c7','#e3ffc7','#c7ffd5','#c7ffff','#c7d5ff','#e3c7ff','#ffc7f1','#ff8f8f','#ffe38f','#c7ff8f','#8fffab','#8fffff','#8fabff','#c78fff','#ff8fe3','#d97979','#d9c179','#a9d979','#79d991','#79d9d9','#7991d9','#a979d9','#d979c1','#d9a9a9','#d9cda9','#c1d9a9','#a9d9b5','#a9d9d9','#a9b5d9','#c1a9d9','#d9a9cd'];
+
+var trueRegex = /\s*true\s*/i;
+
+function isProduction() {
+ return (trueRegex.test(appjet.config['etherpad.isProduction']));
+}
+
+function isProAccountEnabled() {
+ return (appjet.config['etherpad.proAccounts'] == "true");
+}
+
+function domainEnabled(domain) {
+ var enabled = appjet.config.topdomains.split(',');
+ for (var i = 0; i < enabled.length; i++)
+ if (domain == enabled[i])
+ return true;
+ return false;
+}
+
+var PNE_RELEASE_VERSION = "1.1.3";
+var PNE_RELEASE_DATE = "June 15, 2009";
+
+var PRO_FREE_ACCOUNTS = 1e9;
+
+
diff --git a/etherpad/src/etherpad/helpers.js b/etherpad/src/etherpad/helpers.js
new file mode 100644
index 0000000..3996a3b
--- /dev/null
+++ b/etherpad/src/etherpad/helpers.js
@@ -0,0 +1,306 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("jsutils.eachProperty");
+import("faststatic");
+import("comet");
+import("funhtml.META");
+
+import("etherpad.globals.*");
+import("etherpad.debug.dmesg");
+
+import("etherpad.pro.pro_utils");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+// array that supports contains() in O(1)
+
+var _UniqueArray = function() {
+ this._a = [];
+ this._m = {};
+};
+_UniqueArray.prototype.add = function(x) {
+ if (!this._m[x]) {
+ this._a.push(x);
+ this._m[x] = true;
+ }
+};
+_UniqueArray.prototype.asArray = function() {
+ return this._a;
+};
+
+//----------------------------------------------------------------
+// EJS template helpers
+//----------------------------------------------------------------
+
+function _hd() {
+ if (!appjet.requestCache.helperData) {
+ appjet.requestCache.helperData = {
+ clientVars: {},
+ htmlTitle: "",
+ headExtra: "",
+ bodyId: "",
+ bodyClasses: new _UniqueArray(),
+ cssIncludes: new _UniqueArray(),
+ jsIncludes: new _UniqueArray(),
+ includeCometJs: false,
+ suppressGA: false,
+ showHeader: true,
+ robotsPolicy: null
+ };
+ }
+ return appjet.requestCache.helperData;
+}
+
+function addBodyClass(c) {
+ _hd().bodyClasses.add(c);
+}
+
+function addClientVars(vars) {
+ eachProperty(vars, function(k,v) {
+ _hd().clientVars[k] = v;
+ });
+}
+
+function getClientVar(name) {
+ return _hd().clientVars[name];
+}
+
+function addToHead(stuff) {
+ _hd().headExtra += stuff;
+}
+
+function setHtmlTitle(t) {
+ _hd().htmlTitle = t;
+}
+
+function setBodyId(id) {
+ _hd().bodyId = id;
+}
+
+function includeJs(relpath) {
+ _hd().jsIncludes.add(relpath);
+}
+
+function includeJQuery() {
+ includeJs("jquery-1.3.2.js");
+}
+
+function includeCss(relpath) {
+ _hd().cssIncludes.add(relpath);
+}
+
+function includeCometJs() {
+ _hd().includeCometJs = true;
+}
+
+function suppressGA() {
+ _hd().suppressGA = true;
+}
+
+function hideHeader() {
+ _hd().showHeader = false;
+}
+
+//----------------------------------------------------------------
+// for rendering HTML
+//----------------------------------------------------------------
+
+function bodyClasses() {
+ return _hd().bodyClasses.asArray().join(' ');
+}
+
+function clientVarsScript() {
+ var x = _hd().clientVars;
+ x = fastJSON.stringify(x);
+ if (x == '{}') {
+ return '<!-- no client vars -->';
+ }
+ x = x.replace(/</g, '\\x3c');
+ return [
+ '<script type="text/javascript">',
+ ' // <![CDATA[',
+ 'var clientVars = '+x+';',
+ ' // ]]>',
+ '</script>'
+ ].join('\n');
+}
+
+function htmlTitle() {
+ return _hd().htmlTitle;
+}
+
+function bodyId() {
+ return _hd().bodyId;
+}
+
+function baseHref() {
+ return request.scheme + "://"+ request.host + "/";
+}
+
+function headExtra() {
+ return _hd().headExtra;
+}
+
+function jsIncludes() {
+ if (isProduction()) {
+ var jsincludes = _hd().jsIncludes.asArray();
+ if (_hd().includeCometJs) {
+ jsincludes.splice(0, 0, {
+ getPath: function() { return 'comet-client.js'; },
+ getContents: function() { return comet.clientCode(); },
+ getMTime: function() { return comet.clientMTime(); }
+ });
+ }
+ if (jsincludes.length < 1) { return ''; }
+ var key = faststatic.getCompressedFilesKey('js', '/static/js', jsincludes);
+ return '<script type="text/javascript" src="/static/compressed/'+key+'"></script>';
+ } else {
+ var ts = +(new Date);
+ var r = [];
+ if (_hd().includeCometJs) {
+ r.push('<script type="text/javascript" src="'+COMETPATH+'/js/client.js?'+ts+'"></script>');
+ }
+ _hd().jsIncludes.asArray().forEach(function(relpath) {
+ r.push('<script type="text/javascript" src="/static/js/'+relpath+'?'+ts+'"></script>');
+ });
+ return r.join('\n');
+ }
+}
+
+function cssIncludes() {
+ if (isProduction()) {
+ var key = faststatic.getCompressedFilesKey('css', '/static/css', _hd().cssIncludes.asArray());
+ return '<link href="/static/compressed/'+key+'" rel="stylesheet" type="text/css" />';
+ } else {
+ var ts = +(new Date);
+ var r = [];
+ _hd().cssIncludes.asArray().forEach(function(relpath) {
+ r.push('<link href="/static/css/'+relpath+'?'+ts+'" rel="stylesheet" type="text/css" />');
+ });
+ return r.join('\n');
+ }
+}
+
+function oemail(username) {
+ return '&lt;<a class="obfuscemail" href="mailto:'+username+'@p*d.sp***e.inf.fu-berlin.de">'+
+ username+'@p*d.sp***e.inf.fu-berlin.de</a>&gt;';
+}
+
+function googleAnalytics() {
+ // GA disabled always now.
+ return '';
+
+ if (!isProduction()) { return ''; }
+ if (_hd().suppressGA) { return ''; }
+ return [
+ '<script type="text/javascript">',
+ ' var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");',
+ ' document.write(unescape("%3Cscript src=\'" + gaJsHost + "google-analytics.com/ga.js\' type=\'text/javascript\'%3E%3C/script%3E"));',
+ '</script>',
+ '<script type="text/javascript">',
+ 'try {',
+ ' var pageTracker = _gat._getTracker("UA-6236278-1");',
+ ' pageTracker._trackPageview();',
+ '} catch(err) {}</script>'
+ ].join('\n');
+}
+
+function isHeaderVisible() {
+ return _hd().showHeader;
+}
+
+function setRobotsPolicy(policy) {
+ _hd().robotsPolicy = policy;
+}
+function robotsMeta() {
+ if (!_hd().robotsPolicy) { return ''; }
+ var content = "";
+ content += (_hd().robotsPolicy.index ? 'INDEX' : 'NOINDEX');
+ content += ", ";
+ content += (_hd().robotsPolicy.follow ? 'FOLLOW' : 'NOFOLLOW');
+ return META({name: "ROBOTS", content: content});
+}
+
+function thawteSiteSeal() {
+ return [
+ '<div>',
+ '<table width="10" border="0" cellspacing="0" align="center">',
+ '<tr>',
+ '<td>',
+ '<script src="https://siteseal.thawte.com/cgi/server/thawte_seal_generator.exe"></script>',
+ '</td>',
+ '</tr>',
+ '<tr>',
+ '<td height="0" align="center">',
+ '<a style="color:#AD0034" target="_new"',
+ 'href="http://www.thawte.com/digital-certificates/">',
+ '<span style="font-family:arial; font-size:8px; color:#AD0034">',
+ 'ABOUT SSL CERTIFICATES</span>',
+ '</a>',
+ '</td>',
+ '</tr>',
+ '</table>',
+ '</div>'
+ ].join('\n');
+}
+
+function clearFloats() {
+ return '<div style="clear: both;"><!-- --></div>';
+}
+
+function rafterBlogUrl() {
+ return '/ep/blog/posts/google-acquires-appjet';
+}
+
+function rafterNote() {
+ return """<div style='border: 1px solid #ccc; background: #fee; padding: 1em; margin: 1em 0;'>
+ <b>Note: </b>We are no longer accepting new accounts. <a href='"""+rafterBlogUrl()+"""'>Read more</a>.
+ </div>""";
+}
+
+function rafterTerminationDate() {
+ return "March 31, 2010";
+}
+
+function updateToUrl(setParams, deleteParams, setPath) {
+ var params = {};
+
+ for (param in request.params)
+ if (deleteParams === undefined || deleteParams.indexOf(param) == -1)
+ params[param] = request.params[param];
+
+ if (setParams !== undefined)
+ for (param in setParams)
+ params[param] = setParams[param];
+
+ var path = request.path;
+ if (setPath !== undefined)
+ path = setPath;
+
+ var paramStr = '';
+ for (param in params) {
+ if (paramStr == '')
+ paramStr += '?';
+ else
+ paramStr += '&';
+ paramStr += param + '=' + params[param];
+ }
+
+ return path + paramStr;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/importexport/importexport.js b/etherpad/src/etherpad/importexport/importexport.js
new file mode 100644
index 0000000..304a1f4
--- /dev/null
+++ b/etherpad/src/etherpad/importexport/importexport.js
@@ -0,0 +1,241 @@
+/**
+ * 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.
+ */
+
+jimport("java.io.File");
+jimport("java.io.FileOutputStream");
+jimport("java.lang.System.out.println");
+jimport("java.io.ByteArrayInputStream");
+jimport("java.io.ByteArrayOutputStream");
+jimport("java.io.DataInputStream");
+jimport("java.io.DataOutputStream");
+jimport("net.appjet.common.sars.SarsClient");
+jimport("com.etherpad.openofficeservice.OpenOfficeService");
+jimport("com.etherpad.openofficeservice.UnsupportedFormatException");
+jimport("com.etherpad.openofficeservice.TemporaryFailure");
+
+import("etherpad.log");
+import("etherpad.utils");
+import("sync");
+import("execution");
+import("varz");
+import("exceptionutils");
+
+function _log(obj) {
+ log.custom("import-export", obj);
+}
+
+function onStartup() {
+ execution.initTaskThreadPool("importexport", 1);
+}
+
+var formats = {
+ pdf: 'application/pdf',
+ doc: 'application/msword',
+ html: 'text/html; charset=utf-8',
+ odt: 'application/vnd.oasis.opendocument.text',
+ txt: 'text/plain; charset=utf-8'
+}
+
+function _createTempFile(bytes, type) {
+ var f = File.createTempFile("ooconvert-", (type === null ? null : (type == "" ? "" : "."+type)));
+ if (bytes) {
+ var fos = new FileOutputStream(f);
+ fos.write(bytes);
+ }
+ return f;
+}
+
+function _initConverterClient(convertServer) {
+ if (convertServer) {
+ var convertHost = convertServer.split(":")[0];
+ var convertPort = Number(convertServer.split(":")[1]);
+ if (! appjet.scopeCache.converter) {
+ var converter = new SarsClient("ooffice-password", convertHost, convertPort);
+ appjet.scopeCache.converter = converter;
+ converter.setConnectTimeout(5000);
+ converter.setReadTimeout(40000);
+ appjet.scopeCache.converter.connect();
+ }
+ return appjet.scopeCache.converter;
+ } else {
+ return null;
+ }
+}
+
+function _conversionSarsFailure() {
+ delete appjet.scopeCache.converter;
+}
+
+function errorUnsupported(from) {
+ return "Unsupported file type"+(from ? ": <strong>"+from+"</strong>." : ".")+" Etherpad can only import <strong>txt</strong>, <strong>html</strong>, <strong>rtf</strong>, <strong>doc</strong>, and <strong>docx</strong> files.";
+}
+var errorTemporary = "A temporary failure occurred; please try again later.";
+
+function doSlowFileConversion(from, to, bytes, continuation) {
+ var bytes = convertFileSlowly(from, to, bytes);
+ continuation.resume();
+ return bytes;
+}
+
+function _convertOverNetwork(convertServer, from, to, bytes) {
+ var c = _initConverterClient(convertServer);
+ var reqBytes = new ByteArrayOutputStream();
+ var req = new DataOutputStream(reqBytes);
+ req.writeUTF(from);
+ req.writeUTF(to);
+ req.writeInt(bytes.length);
+ req.write(bytes, 0, bytes.length);
+
+ var retBtyes;
+ try {
+ retBytes = c.message(reqBytes.toByteArray());
+ } catch (e) {
+ if (e.javaException) {
+ net.appjet.oui.exceptionlog.apply(e.javaException)
+ }
+ _conversionSarsFailure();
+ return "A communications failure occurred; please try again later.";
+ }
+
+ if (retBytes.length == 0) {
+ return "An unknown failure occurred; please try again later. (#5)";
+ }
+ var res = new DataInputStream(new ByteArrayInputStream(retBytes));
+ var status = res.readInt();
+ if (status == 0) { // success
+ var len = res.readInt();
+ var resBytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, len);
+ res.readFully(resBytes);
+ return resBytes;
+ } else if (status == 1) {
+ return errorTemporary;
+ } else if (status == 2) {
+ var permFailureCode = res.readInt();
+ if (permFailureCode == 0) {
+ return "An unknown failure occurred. (#1)";
+ } else if (permFailureCode == 1) {
+ return errorUnsupported(from);
+ }
+ } else {
+ return "An unknown failure occurred. (#2)";
+ }
+}
+
+function convertFileSlowly(from, to, bytes) {
+ var convertServer = appjet.config["etherpad.sofficeConversionServer"];
+ if (convertServer) {
+ return _convertOverNetwork(convertServer, from, to, bytes);
+ }
+
+ if (! utils.hasOffice()) {
+ return "EtherPad is not configured to import or export formats other than <strong>txt</strong> and <strong>html</strong>. Please contact your system administrator for details.";
+ }
+ OpenOfficeService.setExecutable(appjet.config["etherpad.soffice"]);
+ try {
+ return OpenOfficeService.convertFile(from, to, bytes);
+ } catch (e) {
+ if (e.javaException instanceof TemporaryFailure) {
+ return errorTemporary;
+ } else if (e.javaException instanceof UnsupportedFormatException) {
+ return errorUnsupported(from);
+ } else {
+ return "An unknown failure occurred. (#3)";
+ }
+ }
+}
+
+function _noteConversionAttempt() {
+ varz.incrementInt("importexport-conversions-attempted");
+}
+
+function _noteConversionSuccess() {
+ varz.incrementInt("importexport-conversions-successful");
+}
+
+function _noteConversionFailure() {
+ varz.incrementInt("importexport-conversions-failed");
+}
+
+function _noteConversionTimeout() {
+ varz.incrementInt("importexport-conversions-timeout");
+}
+
+function _noteConversionImpossible() {
+ varz.incrementInt("importexport-conversions-impossible");
+}
+
+function precomputedConversionResult(from, to, bytes) {
+ try {
+ var retBytes = request.cache.conversionCallable.get(500, java.util.concurrent.TimeUnit.MILLISECONDS);
+ var delay = Date.now() - request.cache.startTime;
+ _log({type: "conversion-latency", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ delay: delay});
+ varz.addToInt("importexport-total-conversion-millis", delay);
+ if (typeof(retBytes) == 'string') {
+ _log({type: "error", error: "conversion-failed", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ delay: delay});
+ _noteConversionFailure();
+ } else {
+ _noteConversionSuccess();
+ }
+ return retBytes;
+ } catch (e) {
+ if (e.javaException instanceof java.util.concurrent.TimeoutException) {
+ _noteConversionTimeout();
+ request.cache.conversionCallable.cancel(false);
+ _log({type: "error", error: "conversion-failed", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ delay: -1});
+ return "Conversion timed out. Please try again later.";
+ }
+ _log({type: "error", error: "conversion-failed", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ trace: exceptionutils.getStackTracePlain(e)});
+ _noteConversionFailure();
+ return "An unknown failure occurred. (#4)";
+ }
+}
+
+function convertFile(from, to, bytes) {
+ if (request.cache.conversionCallable) {
+ return precomputedConversionResult(from, to, bytes);
+ }
+
+ _noteConversionAttempt();
+ if (from == to) {
+ _noteConversionSuccess();
+ return bytes;
+ }
+ if (from == "txt" && to == "html") {
+ _noteConversionSuccess();
+ return (new java.lang.String(utils.renderTemplateAsString('pad/exporthtml.ejs', {
+ content: String(new java.lang.String(bytes, "UTF-8")).replace(/&/g, "&amp;").replace(/</g, "&lt;"),
+ pre: true
+ }))).getBytes("UTF-8");
+ }
+
+ request.cache.conversionByteLength = bytes.length;
+ request.cache.conversionCallable =
+ execution.scheduleTask("importexport", "doSlowFileConversion", 0, [
+ from, to, bytes, request.continuation
+ ]);
+ request.cache.startTime = Date.now();
+ request.continuation.suspend(45000);
+ _noteConversionImpossible();
+ return "An unexpected error occurred."; // Shouldn't ever get here.
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/legacy_urls.js b/etherpad/src/etherpad/legacy_urls.js
new file mode 100644
index 0000000..d8aa629
--- /dev/null
+++ b/etherpad/src/etherpad/legacy_urls.js
@@ -0,0 +1,37 @@
+/**
+ * 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.
+ */
+
+/* legacy URLs only apply to the public main site. (not Pro or PNE). */
+
+var _legacyURLs = {
+ '/ep/beta-signup': '/',
+ '/ep/talktostrangers': '/',
+ '/ep/about/pricing-eepod': '/ep/about/pricing-pro',
+ '/static/html/enterprise-etherpad-installguide.html': '/ep/pne-manual/',
+ '/static/html/eepnet/eepnet-changelog.html': '/ep/pne-manual/changelog',
+ '/static/html/eepnet/eepnet-installguide.html': '/ep/pne-manual/',
+ '/ep/blog/posts/back-online-until-open-sourced': '/ep/blog/posts/etherpad-back-online-until-open-sourced'
+};
+
+function checkPath() {
+ var p = request.path;
+ var match = _legacyURLs[p];
+
+ if (match) {
+ response.redirect(match);
+ }
+}
+
diff --git a/etherpad/src/etherpad/licensing.js b/etherpad/src/etherpad/licensing.js
new file mode 100644
index 0000000..2337456
--- /dev/null
+++ b/etherpad/src/etherpad/licensing.js
@@ -0,0 +1,163 @@
+/**
+ * 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.
+ */
+
+
+/*
+ * This file used to control access restrictions for various sites like
+ * pad.spline.inf.fu-berlin.de or on-prem installations of etherpad, or evaluation
+ * editions. For the open-source effort, I have gutted out the
+ * restrictions. --aiba
+ */
+
+import("sync.callsync");
+import("stringutils");
+import("fileutils.readRealFile");
+import("jsutils.*");
+
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padutils");
+import("etherpad.pne.pne_utils");
+
+jimport("com.etherpad.Licensing");
+jimport("java.lang.System.out.println");
+
+var _editionNames = {
+ 0: 'ETHERPAD.COM',
+ 1: 'PRIVATE_NETWORK_EVALUATION',
+ 2: 'PRIVATE_NETWORK'
+};
+
+function onStartup() { }
+
+//----------------------------------------------------------------
+
+/**
+ * expires is a long timestamp (set to null for never expiring).
+ * maxUsers is also a long (set to -1 for infinite users).
+ */
+function generateNewKey(personName, orgName, expires, editionId, maxUsers) {
+ return null;
+}
+
+function decodeLicenseInfoFromKey(key) {
+ return null;
+}
+
+//----------------------------------------------------------------
+
+function _getCache() {
+ return {};
+}
+
+function _readKeyFile(f) {
+ return null;
+}
+
+function _readLicenseKey() {
+ return null;
+}
+
+function reloadLicense() {
+}
+
+function getLicense() {
+ return null;
+}
+
+function isPrivateNetworkEdition() {
+ return false;
+}
+
+// should really only be called for PNE requests.
+// see etherpad.quotas module
+function getMaxUsersPerPad() {
+ return 1e9;
+}
+
+function getEditionId(editionName) {
+ return _editionNames[0];
+}
+
+function getEditionName(editionId) {
+ return _editionNames[editionId];
+}
+
+function isEvaluation() {
+ return false;
+}
+
+function isExpired() {
+ return false;
+}
+
+function isValidKey(key) {
+ return true;
+}
+
+function getVersionString() {
+ return "0";
+}
+
+function isVersionTooOld() {
+ return false;
+}
+
+//----------------------------------------------------------------
+// counting active users
+//----------------------------------------------------------------
+
+function getActiveUserQuota() {
+ return 1e9;
+}
+
+function _previousMidnight() {
+ // return midnight of today.
+ var d = new Date();
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ d.setMilliseconds(1); // just north of midnight
+ return d;
+}
+
+function _resetActiveUserStats() {
+}
+
+function getActiveUserWindowStart() {
+ return null;
+}
+
+function getActiveUserWindowHours() {
+ return null;
+}
+
+function getActiveUserCount() {
+ return 0;
+}
+
+function canSessionUserJoin() {
+ return true;
+}
+
+function onUserJoin(userInfo) {
+}
+
+function onUserLeave() {
+ // do nothing.
+}
+
+
diff --git a/etherpad/src/etherpad/log.js b/etherpad/src/etherpad/log.js
new file mode 100644
index 0000000..cfc82de
--- /dev/null
+++ b/etherpad/src/etherpad/log.js
@@ -0,0 +1,255 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("stringutils.startsWith");
+import("sync.{callsync,callsyncIfTrue}");
+import("jsutils.*");
+import("exceptionutils");
+
+import("etherpad.globals.*");
+import("etherpad.pad.padutils");
+import("etherpad.sessions");
+import("etherpad.utils.*");
+
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.io.FileWriter");
+jimport("java.lang.System.out.println");
+jimport("java.io.File");
+jimport("net.appjet.ajstdlib.execution");
+
+
+function getReadableTime() {
+ return (new Date()).toString().split(' ').slice(0, 5).join('-');
+}
+
+serverhandlers.tasks.trackerAndSessionIds = function() {
+ var m = new Packages.scala.collection.mutable.HashMap();
+ if (request.isDefined) {
+ try {
+ if (sessions.getTrackingId()) {
+ m.update("tracker", sessions.getTrackingId());
+ }
+ if (sessions.getSessionId()) {
+ m.update("session", sessions.getSessionId());
+ }
+ if (request.path) {
+ m.update("path", request.path);
+ }
+ if (request.clientAddr) {
+ m.update("clientAddr", request.clientAddr);
+ }
+ if (request.host) {
+ m.update("host", request.host);
+ }
+ if (getSessionProAccount()) {
+ m.update("proAccountId", getSessionProAccount().id);
+ }
+ } catch (e) {
+ // do nothing.
+ }
+ }
+ return m;
+}
+
+function onStartup() {
+ var f = execution.wrapRunTask("trackerAndSessionIds", null,
+ java.lang.Class.forName("scala.collection.mutable.HashMap"));
+ net.appjet.oui.GenericLoggerUtils.setExtraPropertiesFunction(f);
+}
+
+//----------------------------------------------------------------
+// Logfile parsing
+//----------------------------------------------------------------
+
+function _n(x) {
+ if (x < 10) { return "0"+x; }
+ else { return x; }
+}
+
+function logFileName(prefix, logName, day) {
+ var fmt = [day.getFullYear(), _n(day.getMonth()+1), _n(day.getDate())].join('-');
+ var fname = (appjet.config['logDir'] + '/'+prefix+'/' + logName + '/' +
+ logName + '-' + fmt + '.jslog');
+
+ // make sure file exists
+ if (!(new File(fname)).exists()) {
+ //log.warn("WARNING: file does not exist: "+fname);
+ return null;
+ }
+
+ return fname;
+}
+
+function frontendLogFileName(logName, day) {
+ return logFileName('frontend', logName, day);
+}
+
+function backendLogFileName(logName, day) {
+ return logFileName('backend', logName, day);
+}
+
+//----------------------------------------------------------------
+function _getRequestLogEntry() {
+ if (request.isDefined) {
+ var logEntry = {
+ clientAddr: request.clientAddr,
+ method: request.method.toLowerCase(),
+ scheme: request.scheme,
+ host: request.host,
+ path: request.path,
+ query: request.query,
+ referer: request.headers['Referer'],
+ userAgent: request.headers['User-Agent'],
+ statusCode: response.getStatusCode(),
+ }
+ if ('globalPadId' in request.cache) {
+ logEntry.padId = request.cache.globalPadId;
+ }
+ return logEntry;
+ } else {
+ return {};
+ }
+}
+
+function logRequest() {
+ if ((! request.isDefined) ||
+ startsWith(request.path, COMETPATH) ||
+ isStaticRequest()) {
+ return;
+ }
+
+ _log("request", _getRequestLogEntry());
+}
+
+function _log(name, m) {
+ var cache = appjet.cache;
+
+ callsyncIfTrue(
+ cache,
+ function() { return ! ('logWriters' in cache)},
+ function() { cache.logWriters = {}; }
+ );
+
+ callsyncIfTrue(
+ cache.logWriters,
+ function() { return !(name in cache.logWriters) },
+ function() {
+ lw = new net.appjet.oui.GenericLogger('frontend', name, true);
+ if (! isProduction()) {
+ lw.setEchoToStdOut(true);
+ }
+ lw.start();
+ cache.logWriters[name] = lw;
+ });
+
+ var lw = cache.logWriters[name];
+ if (typeof(m) == 'object') {
+ lw.logObject(m);
+ } else {
+ lw.log(m);
+ }
+}
+
+function custom(name, m) {
+ _log(name, m);
+}
+
+function _stampedMessage(m) {
+ var obj = {};
+ if (typeof(m) == 'string') {
+ obj.message = m;
+ } else {
+ eachProperty(m, function(k, v) {
+ obj[k] = v;
+ });
+ }
+ // stamp message with pad and path
+ if (request.isDefined) {
+ obj.path = request.path;
+ }
+
+ var currentPad = padutils.getCurrentPad();
+ if (currentPad) {
+ obj.currentPad = currentPad;
+ }
+
+ return obj;
+}
+
+//----------------------------------------------------------------
+// logException
+//----------------------------------------------------------------
+
+function logException(ex) {
+ if (typeof(ex) != 'object' || ! (ex instanceof java.lang.Throwable)) {
+ ex = new java.lang.RuntimeException(String(ex));
+ }
+ // NOTE: ex is always a java.lang.Throwable
+ var m = _getRequestLogEntry();
+ m.jsTrace = exceptionutils.getStackTracePlain(ex);
+ var s = new java.io.StringWriter();
+ ex.printStackTrace(new java.io.PrintWriter(s));
+ m.trace = s.toString();
+ _log("exception", m);
+}
+
+function callCatchingExceptions(func) {
+ try {
+ return func();
+ }
+ catch (e) {
+ logException(toJavaException(e));
+ }
+ return undefined;
+}
+
+//----------------------------------------------------------------
+// warning
+//----------------------------------------------------------------
+function warn(m) {
+ _log("warn", _stampedMessage(m));
+}
+
+//----------------------------------------------------------------
+// info
+//----------------------------------------------------------------
+function info(m) {
+ _log("info", _stampedMessage(m));
+}
+
+function onUserJoin(userId) {
+ function doUpdate() {
+ sqlobj.update('pad_cookie_userids', {id: userId}, {lastActiveDate: new Date()});
+ }
+ try {
+ sqlcommon.inTransaction(function() {
+ if (sqlobj.selectSingle('pad_cookie_userids', {id: userId})) {
+ doUpdate();
+ } else {
+ sqlobj.insert('pad_cookie_userids',
+ {id: userId, createdDate: new Date(), lastActiveDate: new Date()});
+ }
+ });
+ }
+ catch (e) {
+ sqlcommon.inTransaction(function() {
+ doUpdate();
+ });
+ }
+}
diff --git a/etherpad/src/etherpad/metrics/metrics.js b/etherpad/src/etherpad/metrics/metrics.js
new file mode 100644
index 0000000..435a5be
--- /dev/null
+++ b/etherpad/src/etherpad/metrics/metrics.js
@@ -0,0 +1,438 @@
+/**
+ * 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.
+ */
+
+import("etherpad.log.frontendLogFileName");
+import("jsutils.eachProperty");
+import("stringutils.startsWith");
+import("fileutils.eachFileLine");
+
+jimport("java.lang.System.out.println");
+
+var _idleTime = 5*60*1000 // 5 minutes?
+
+function _isPadUrl(url) {
+ return url != '/' && ! startsWith(url, '/ep/');
+}
+
+function VisitData(url, referer) {
+ this.url = url;
+ this.referer = referer;
+ this.__defineGetter__('isPadVisit', function() {
+ return _isPadUrl(this.url);
+ });
+}
+VisitData.prototype.toString = function() {
+ var re = new RegExp("^https?://"+request.host);
+ if (this.referer && ! re.test(this.referer)) {
+ return this.url+", from "+this.referer;
+ } else {
+ return this.url;
+ }
+}
+
+function Event(time, type, data) {
+ this.time = time;
+ this.type = type;
+ this.data = data;
+}
+Event.prototype.toString = function() {
+ return "("+this.type+" "+this.data+" @ "+this.time.getTime()+")";
+}
+
+function Flow(sessionKey, startEvent) {
+ this.sessionKey = sessionKey;
+ this.events = [];
+ this.visitedPaths = {};
+ var visitCount = 0;
+ var visitsCache;
+ this._updateVisitedPaths = function(url) {
+ if (! this.visitedPaths[url]) {
+ this.visitedPaths[url] = [visitCount];
+ } else {
+ this.visitedPaths[url].push(visitCount);
+ }
+ }
+ var isInPad = 0;
+ this.push = function(evt) {
+ evt.flow = this;
+ this.events.push(evt);
+ if (evt.type == 'visit') {
+ this._updateVisitedPaths(evt.data.url);
+ if (_isPadUrl(evt.data.url)) {
+ this._updateVisitedPaths("(pad)");
+ }
+ visitCount++;
+ visitsCache = undefined;
+ } else if (evt.type == 'userjoin') {
+ isInPad++;
+ } else if (evt.type == 'userleave') {
+ isInPad--;
+ }
+ }
+ this.__defineGetter__("isInPad", function() { return isInPad > 0; });
+ this.__defineGetter__("lastEvent", function() {
+ return this.events[this.events.length-1];
+ });
+ this.__defineGetter__("visits", function() {
+ if (! visitsCache) {
+ visitsCache = this.events.filter(function(x) { return x.type == "visit" });
+ }
+ return visitsCache;
+ });
+ startEvent.flow = this;
+ this.push(startEvent);
+}
+Flow.prototype.toString = function() {
+ return "["+this.events.map(function(x) { return x.toString(); }).join(", ")+"]";
+}
+Flow.prototype.includesVisit = function(path, index, useExactIndexMatch) {
+ if (! this.visitedPaths[path]) return false;
+ if (useExactIndexMatch) {
+ return this.visitedPaths[path].some(function(x) { return x == index });
+ } else {
+ if (index) {
+ for (var i = 0; i < this.visitedPaths[path].length; ++i) {
+ if (this.visitedPaths[path][i] >= index)
+ return this.visitedPaths[path][i];
+ }
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+Flow.prototype.visitIndices = function(path) {
+ return this.visitedPaths[path] || [];
+}
+
+function getKeyForDate(date) {
+ return date.getYear()+":"+date.getMonth()+":"+date.getDay();
+}
+
+function parseEvents(dates) {
+ if (! appjet.cache["metrics-events"]) {
+ appjet.cache["metrics-events"] = {};
+ }
+ var events = {};
+ function eventArray(key) {
+ if (! events[key]) {
+ events[key] = [];
+ }
+ return events[key];
+ }
+
+ dates.sort(function(a, b) { return a.getTime() - b.getTime(); });
+ dates.forEach(function(day) {
+ if (! appjet.cache["metrics-events"][getKeyForDate(day)]) {
+ var daysEvents = {};
+ function daysEventArray(key) {
+ if (! daysEvents[key]) {
+ daysEvents[key] = [];
+ }
+ return daysEvents[key];
+ }
+ var requestLog = frontendLogFileName("request", day);
+ if (requestLog) {
+ eachFileLine(requestLog, function(line) {
+ var s = line.split("\t");
+ var sessionKey = s[3];
+ if (sessionKey == "-") { return; }
+ var time = new Date(Number(s[1]));
+ var path = s[7];
+ var referer = (s[9] == "-" ? null : s[9]);
+ var userAgent = s[10];
+ var statusCode = s[5];
+ // Remove bots and other automatic or irrelevant requests.
+ // There's got to be something better than a whitelist.
+ if (userAgent.indexOf("Mozilla") < 0 &&
+ userAgent.indexOf("Opera") < 0) {
+ return;
+ }
+ if (path == "/favicon.ico") { return; }
+ daysEventArray(sessionKey).push(new Event(time, "visit", new VisitData(path, referer)));
+ });
+ }
+ var padEventLog = frontendLogFileName("padevents", day);
+ if (padEventLog) {
+ eachFileLine(padEventLog, function(line) {
+ var s = line.split("\t");
+ var sessionKey = s[7];
+ if (sessionKey == "-") { return; }
+ var time = new Date(Number(s[1]));
+ var padId = s[3];
+ var evt = s[2];
+ daysEventArray(sessionKey).push(new Event(time, evt, padId));
+ });
+ }
+ var chatLog = frontendLogFileName("chat", day);
+ if (chatLog) {
+ eachFileLine(chatLog, function(line) {
+ var s = line.split("\t");
+ var sessionKey = s[4];
+ if (sessionKey == "-") { return; }
+ var time = new Date(Number(s[1]));
+ var padId = s[2];
+ daysEventArray(sessionKey).push(new Event(time, "chat", padId));
+ });
+ }
+ eachProperty(daysEvents, function(k, v) {
+ v.sort(function(a, b) { return a.time.getTime() - b.time.getTime()});
+ });
+ appjet.cache["metrics-events"][getKeyForDate(day)] = daysEvents;
+ }
+ eachProperty(appjet.cache["metrics-events"][getKeyForDate(day)], function(k, v) {
+ Array.prototype.push.apply(eventArray(k), v);
+ });
+ });
+
+ return events;
+}
+
+function getFlows(startDate, endDate) {
+ if (! endDate) { endDate = startDate; }
+ if (! appjet.cache.flows || request.params.clearCache == "1") {
+ appjet.cache.flows = {};
+ }
+ if (appjet.cache.flows[getKeyForDate(startDate)+"-"+getKeyForDate(endDate)]) {
+ return appjet.cache.flows[getKeyForDate(startDate)+"-"+getKeyForDate(endDate)];
+ }
+
+ var datesForEvents = [];
+ for (var i = startDate; i.getTime() <= endDate.getTime(); i = new Date(i.getTime()+86400*1000)) {
+ datesForEvents.push(i);
+ }
+
+ var events = parseEvents(datesForEvents);
+ var flows = {};
+
+ eachProperty(events, function(k, eventArray) {
+ flows[k] = [];
+ function lastFlow() {
+ var f = flows[k];
+ if (f.length > 0) {
+ return f[f.length-1];
+ }
+ }
+ var lastTime = 0;
+ eventArray.forEach(function(evt) {
+ var l = lastFlow();
+
+ if (l && (l.lastEvent.time.getTime() + _idleTime > evt.time.getTime() || l.isInPad)) {
+ l.push(evt);
+ } else {
+ flows[k].push(new Flow(k, evt));
+ }
+ });
+ });
+ appjet.cache.flows[getKeyForDate(startDate)+"-"+getKeyForDate(endDate)] = flows;
+ return flows;
+}
+
+function _uniq(array) {
+ var seen = {};
+ return array.filter(function(x) {
+ if (seen[x]) {
+ return false;
+ }
+ seen[x] = true;
+ return true;
+ });
+}
+
+function getFunnel(startDate, endDate, pathsArray, useConsecutivePaths) {
+ var flows = getFlows(startDate, endDate)
+
+ var flowsAtStep = pathsArray.map(function() { return []; });
+ eachProperty(flows, function(k, flowArray) {
+ flowArray.forEach(function(flow) {
+ if (flow.includesVisit(pathsArray[0])) {
+ flowsAtStep[0].push({f: flow, i: flow.visitIndices(pathsArray[0])});
+ }
+ });
+ });
+ for (var i = 0; i < pathsArray.length-1; ++i) {
+ flowsAtStep[i].forEach(function(fobj) {
+ var newIndices = fobj.i.map(function(index) {
+ var nextIndex =
+ fobj.f.includesVisit(pathsArray[i+1], index+1, useConsecutivePaths);
+ if (nextIndex !== false) {
+ return (useConsecutivePaths ? index+1 : nextIndex);
+ }
+ }).filter(function(x) { return x !== undefined; });
+ if (newIndices.length > 0) {
+ flowsAtStep[i+1].push({f: fobj.f, i: newIndices});
+ }
+ });
+ }
+ return {
+ flows: flowsAtStep.map(function(x) { return x.map(function(y) { return y.f; }); }),
+ visitCounts: flowsAtStep.map(function(x) { return x.length; }),
+ visitorCounts: flowsAtStep.map(function(x) {
+ return _uniq(x.map(function(y) { return y.f.sessionKey; })).length
+ })
+ };
+}
+
+function makeHistogram(array) {
+ var counts = {};
+ for (var i = 0; i < array.length; ++i) {
+ var value = array[i]
+ if (! counts[value]) {
+ counts[value] = 0;
+ }
+ counts[value]++;
+ }
+ var histogram = [];
+ eachProperty(counts, function(k, v) {
+ histogram.push({value: k, count: v, fraction: (v / array.length)});
+ });
+ histogram.sort(function(a, b) { return b.count - a.count; });
+ return histogram;
+}
+
+function getOrigins(startDate, endDate, useReferer, shouldAggregatePads) {
+ var key = (useReferer ? "referer" : "url");
+ var flows = getFlows(startDate, endDate);
+
+ var sessionKeyFirsts = [];
+ var flowFirsts = [];
+ eachProperty(flows, function(k, flowArray) {
+ if (flowArray[0].visits[0] && flowArray[0].visits[0].data &&
+ flowArray[0].visits[0].data[key]) {
+ var path = flowArray[0].visits[0].data[key];
+ sessionKeyFirsts.push(
+ (shouldAggregatePads && ! useReferer && _isPadUrl(path) ?
+ "(pad)" : path));
+ }
+ flowArray.forEach(function(flow) {
+ if (flow.visits[0] && flow.visits[0].data &&
+ flow.visits[0].data[key]) {
+ var path = flow.visits[0].data[key];
+ flowFirsts.push(
+ (shouldAggregatePads && ! useReferer && _isPadUrl(path) ?
+ "(pad)" : path));
+ }
+ });
+ });
+
+ if (useReferer) {
+ flowFirsts = flowFirsts.filter(function(x) { return ! startsWith(x, "http://pad.spline.inf.fu-berlin.de"); });
+ sessionKeyFirsts = sessionKeyFirsts.filter(function(x) { return ! startsWith(x, "http://pad.spline.inf.fu-berlin.de"); });
+ }
+
+ return {
+ flowFirsts: makeHistogram(flowFirsts),
+ sessionKeyFirsts: makeHistogram(sessionKeyFirsts)
+ }
+}
+
+function getExits(startDate, endDate, src, shouldAggregatePads) {
+ var flows = getFlows(startDate, endDate);
+
+ var exits = [];
+
+ eachProperty(flows, function(k, flowArray) {
+ flowArray.forEach(function(flow) {
+ var indices = flow.visitIndices(src);
+ for (var i = 0; i < indices.length; ++i) {
+ if (indices[i]+1 < flow.visits.length) {
+ if (src != flow.visits[indices[i]+1].data.url) {
+ exits.push(flow.visits[indices[i]+1]);
+ }
+ } else {
+ exits.push("(nothing)");
+ }
+ }
+ });
+ });
+ return {
+ nextVisits: exits,
+ histogram: makeHistogram(exits.map(function(x) {
+ if (typeof(x) == 'string') return x;
+ return ((! shouldAggregatePads) || ! _isPadUrl(x.data.url) ?
+ x.data.url : "(pad)" )
+ }))
+ }
+}
+
+jimport("org.jfree.data.general.DefaultPieDataset");
+jimport("org.jfree.chart.plot.PiePlot");
+jimport("org.jfree.chart.ChartUtilities");
+jimport("org.jfree.chart.JFreeChart");
+
+function _fToPct(f) {
+ return Math.round(f*10000)/100;
+}
+
+function _shorten(str) {
+ if (startsWith(str, "http://")) {
+ str = str.substring("http://".length);
+ }
+ var len = 35;
+ if (str.length > len) {
+ return str.substring(0, len-3)+"..."
+ } else {
+ return str;
+ }
+}
+
+function respondWithPieChart(name, histogram) {
+ var width = 900;
+ var height = 300;
+
+ var ds = new DefaultPieDataset();
+
+ var cumulative = 0;
+ var other = 0;
+ var otherCount = 0;
+ histogram.forEach(function(x, i) {
+ cumulative += x.fraction;
+ if (cumulative < 0.98 && x.fraction > .01) {
+ ds.setValue(_shorten(x.value)+"\n ("+x.count+" visits - "+_fToPct(x.fraction)+"%)", x.fraction);
+ } else {
+ other += x.fraction;
+ otherCount += x.count;
+ }
+ });
+ if (other > 0) {
+ ds.setValue("Other ("+otherCount + " visits - "+_fToPct(other)+"%)", other);
+ }
+
+ var piePlot = new PiePlot(ds);
+
+ var chart = new JFreeChart(piePlot);
+ chart.setTitle(name);
+ chart.removeLegend();
+
+ var jos = new java.io.ByteArrayOutputStream();
+ ChartUtilities.writeChartAsJPEG(
+ jos, 1.0, chart, width, height);
+
+ response.setContentType('image/jpeg');
+ response.writeBytes(jos.toByteArray());
+}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etherpad/src/etherpad/pad/activepads.js b/etherpad/src/etherpad/pad/activepads.js
new file mode 100644
index 0000000..07f5e2e
--- /dev/null
+++ b/etherpad/src/etherpad/pad/activepads.js
@@ -0,0 +1,52 @@
+/**
+ * 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.
+ */
+
+import("jsutils.cmp");
+
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+var HISTORY_SIZE = 100;
+
+function _getMap() {
+ if (!appjet.cache['activepads']) {
+ appjet.cache['activepads'] = {
+ map: new LimitedSizeMapping(HISTORY_SIZE)
+ };
+ }
+ return appjet.cache['activepads'].map;
+}
+
+function touch(padId) {
+ _getMap().put(padId, +(new Date));
+}
+
+function getActivePads() {
+ var m = _getMap();
+ var a = m.listAllKeys().toArray();
+ var activePads = [];
+ for (var i = 0; i < a.length; i++) {
+ activePads.push({
+ padId: a[i],
+ timestamp: m.get(a[i])
+ });
+ }
+
+ activePads.sort(function(a,b) { return cmp(b.timestamp,a.timestamp); });
+ return activePads;
+}
+
+
+
diff --git a/etherpad/src/etherpad/pad/chatarchive.js b/etherpad/src/etherpad/pad/chatarchive.js
new file mode 100644
index 0000000..2f8e33a
--- /dev/null
+++ b/etherpad/src/etherpad/pad/chatarchive.js
@@ -0,0 +1,67 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("etherpad.log");
+
+jimport("java.lang.System.out.println");
+
+function onChatMessage(pad, senderUserInfo, msg) {
+ pad.appendChatMessage({
+ name: senderUserInfo.name,
+ userId: senderUserInfo.userId,
+ time: +(new Date),
+ lineText: msg.lineText
+ });
+}
+
+function getRecentChatBlock(pad, howMany) {
+ var numMessages = pad.getNumChatMessages();
+ var firstToGet = Math.max(0, numMessages - howMany);
+
+ return getChatBlock(pad, firstToGet, numMessages);
+}
+
+function getChatBlock(pad, start, end) {
+ if (start < 0) {
+ start = 0;
+ }
+ if (end > pad.getNumChatMessages()) {
+ end = pad.getNumChatMessages();
+ }
+
+ var historicalAuthorData = {};
+ var lines = [];
+ var block = {start: start, end: end,
+ historicalAuthorData: historicalAuthorData,
+ lines: lines};
+
+ for(var i=start; i<end; i++) {
+ var x = pad.getChatMessage(i);
+ var userId = x.userId;
+ if (! historicalAuthorData[userId]) {
+ historicalAuthorData[userId] = (pad.getAuthorData(userId) || {});
+ }
+ lines.push({
+ name: x.name,
+ time: x.time,
+ userId: x.userId,
+ lineText: x.lineText
+ });
+ }
+
+ return block;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pad/dbwriter.js b/etherpad/src/etherpad/pad/dbwriter.js
new file mode 100644
index 0000000..233622b
--- /dev/null
+++ b/etherpad/src/etherpad/pad/dbwriter.js
@@ -0,0 +1,338 @@
+/**
+ * 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.
+ */
+
+import("execution");
+import("profiler");
+
+import("etherpad.pad.model");
+import("etherpad.pad.model.accessPadGlobal");
+import("etherpad.log");
+import("etherpad.utils");
+
+jimport("net.appjet.oui.exceptionlog");
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("java.lang.System.out.println");
+
+var MIN_WRITE_INTERVAL_MS = 2000; // 2 seconds
+var MIN_WRITE_DELAY_NOTIFY_MS = 2000; // 2 seconds
+var AGE_FOR_PAD_FLUSH_MS = 5*60*1000; // 5 minutes
+var DBUNWRITABLE_WRITE_DELAY_MS = 30*1000; // 30 seconds
+
+// state is { constant: true }, { constant: false }, { trueAfter: timeInMs }
+function setWritableState(state) {
+ _dbwriter().dbWritable = state;
+}
+
+function getWritableState() {
+ return _dbwriter().dbWritable;
+}
+
+function isDBWritable() {
+ return _isDBWritable();
+}
+
+function _isDBWritable() {
+ var state = _dbwriter().dbWritable;
+ if (typeof state != "object") {
+ return true;
+ }
+ else if (state.constant !== undefined) {
+ return !! state.constant;
+ }
+ else if (state.trueAfter !== undefined) {
+ return (+new Date()) > state.trueAfter;
+ }
+ else return true;
+}
+
+function getWritableStateDescription(state) {
+ var v = _isDBWritable();
+ var restOfMessage = "";
+ if (state.trueAfter !== undefined) {
+ var now = +new Date();
+ var then = state.trueAfter;
+ var diffSeconds = java.lang.String.format("%.1f", Math.abs(now - then)/1000);
+ if (now < then) {
+ restOfMessage = " until "+diffSeconds+" seconds from now";
+ }
+ else {
+ restOfMessage = " since "+diffSeconds+" seconds ago";
+ }
+ }
+ return v+restOfMessage;
+}
+
+function _dbwriter() {
+ return appjet.cache.dbwriter;
+}
+
+function onStartup() {
+ appjet.cache.dbwriter = {};
+ var dbwriter = _dbwriter();
+ dbwriter.pendingWrites = new ConcurrentHashMap();
+ dbwriter.scheduledFor = new ConcurrentHashMap(); // padId --> long
+ dbwriter.dbWritable = { constant: true };
+
+ execution.initTaskThreadPool("dbwriter", 4);
+ // we don't wait for scheduled tasks in the infreq pool to run and complete
+ execution.initTaskThreadPool("dbwriter_infreq", 1);
+
+ _scheduleCheckForStalePads();
+}
+
+function _scheduleCheckForStalePads() {
+ execution.scheduleTask("dbwriter_infreq", "checkForStalePads", AGE_FOR_PAD_FLUSH_MS, []);
+}
+
+function onShutdown() {
+ log.info("Doing final DB writes before shutdown...");
+ var success = execution.shutdownAndWaitOnTaskThreadPool("dbwriter", 10000);
+ if (! success) {
+ log.warn("ERROR! DB WRITER COULD NOT SHUTDOWN THREAD POOL!");
+ }
+}
+
+function _logException(e) {
+ var exc = utils.toJavaException(e);
+ log.warn("writeAllToDB: Error writing to SQL! Written to exceptions.log: "+exc);
+ log.logException(exc);
+ exceptionlog.apply(exc);
+}
+
+function taskFlushPad(padId, reason) {
+ var dbwriter = _dbwriter();
+ if (! _isDBWritable()) {
+ // DB is unwritable, delay
+ execution.scheduleTask("dbwriter_infreq", "flushPad", DBUNWRITABLE_WRITE_DELAY_MS, [padId, reason]);
+ return;
+ }
+
+ model.accessPadGlobal(padId, function(pad) {
+ writePadNow(pad, true);
+ }, "r");
+
+ log.info("taskFlushPad: flushed "+padId+(reason?(" (reason: "+reason+")"):''));
+}
+
+function taskWritePad(padId) {
+ var dbwriter = _dbwriter();
+ if (! _isDBWritable()) {
+ // DB is unwritable, delay
+ dbwriter.scheduledFor.put(padId, (+(new Date)+DBUNWRITABLE_WRITE_DELAY_MS));
+ execution.scheduleTask("dbwriter", "writePad", DBUNWRITABLE_WRITE_DELAY_MS, [padId]);
+ return;
+ }
+
+ profiler.reset();
+ var t1 = profiler.rcb("lock wait");
+ model.accessPadGlobal(padId, function(pad) {
+ t1();
+ _dbwriter().pendingWrites.remove(padId); // do this first
+
+ var success = false;
+ try {
+ var t2 = profiler.rcb("write");
+ writePadNow(pad);
+ t2();
+
+ success = true;
+ }
+ finally {
+ if (! success) {
+ log.warn("DB WRITER FAILED TO WRITE PAD: "+padId);
+ }
+ profiler.print();
+ }
+ }, "r");
+}
+
+function taskCheckForStalePads() {
+ // do this first
+ _scheduleCheckForStalePads();
+
+ if (! _isDBWritable()) return;
+
+ // get "active" pads into an array
+ var padIter = appjet.cache.pads.meta.keySet().iterator();
+ var padList = [];
+ while (padIter.hasNext()) { padList.push(padIter.next()); }
+
+ var numStale = 0;
+
+ for (var i = 0; i < padList.length; i++) {
+ if (! _isDBWritable()) break;
+ var p = padList[i];
+ if (model.isPadLockHeld(p)) {
+ // skip it, don't want to lock up stale pad flusher
+ }
+ else {
+ accessPadGlobal(p, function(pad) {
+ if (pad.exists()) {
+ var padAge = (+new Date()) - pad._meta.status.lastAccess;
+ if (padAge > AGE_FOR_PAD_FLUSH_MS) {
+ writePadNow(pad, true);
+ numStale++;
+ }
+ }
+ }, "r");
+ }
+ }
+
+ log.info("taskCheckForStalePads: flushed "+numStale+" stale pads");
+}
+
+function notifyPadDirty(padId) {
+ var dbwriter = _dbwriter();
+ if (! dbwriter.pendingWrites.containsKey(padId)) {
+ dbwriter.pendingWrites.put(padId, "pending");
+ dbwriter.scheduledFor.put(padId, (+(new Date)+MIN_WRITE_INTERVAL_MS));
+ execution.scheduleTask("dbwriter", "writePad", MIN_WRITE_INTERVAL_MS, [padId]);
+ }
+}
+
+function scheduleFlushPad(padId, reason) {
+ execution.scheduleTask("dbwriter_infreq", "flushPad", 0, [padId, reason]);
+}
+
+/*function _dbwriterLoopBody(executor) {
+ try {
+ var info = writeAllToDB(executor);
+ if (!info.boring) {
+ log.info("DB writer: "+info.toSource());
+ }
+ java.lang.Thread.sleep(Math.max(0, MIN_WRITE_INTERVAL_MS - info.elapsed));
+ }
+ catch (e) {
+ _logException(e);
+ java.lang.Thread.sleep(MIN_WRITE_INTERVAL_MS);
+ }
+}
+
+function _startInThread(name, func) {
+ (new Thread(new Runnable({
+ run: function() {
+ func();
+ }
+ }), name)).start();
+}
+
+function killDBWriterThreadAndWait() {
+ appjet.cache.abortDBWriter = true;
+ while (appjet.cache.runningDBWriter) {
+ java.lang.Thread.sleep(100);
+ }
+}*/
+
+/*function writeAllToDB(executor, andFlush) {
+ if (!executor) {
+ executor = new ScheduledThreadPoolExecutor(NUM_WRITER_THREADS);
+ }
+
+ profiler.reset();
+ var startWriteTime = profiler.time();
+ var padCount = new AtomicInteger(0);
+ var writeCount = new AtomicInteger(0);
+ var removeCount = new AtomicInteger(0);
+
+ // get pads into an array
+ var padIter = appjet.cache.pads.meta.keySet().iterator();
+ var padList = [];
+ while (padIter.hasNext()) { padList.push(padIter.next()); }
+
+ var latch = new CountDownLatch(padList.length);
+
+ for (var i = 0; i < padList.length; i++) {
+ _spawnCall(executor, function(p) {
+ try {
+ var padWriteResult = {};
+ accessPadGlobal(p, function(pad) {
+ if (pad.exists()) {
+ padCount.getAndIncrement();
+ padWriteResult = writePad(pad, andFlush);
+ if (padWriteResult.didWrite) writeCount.getAndIncrement();
+ if (padWriteResult.didRemove) removeCount.getAndIncrement();
+ }
+ }, "r");
+ } catch (e) {
+ _logException(e);
+ } finally {
+ latch.countDown();
+ }
+ }, padList[i]);
+ }
+
+ // wait for them all to finish
+ latch.await();
+
+ var endWriteTime = profiler.time();
+ var elapsed = Math.round((endWriteTime - startWriteTime)/1000)/1000;
+ var interesting = (writeCount.get() > 0 || removeCount.get() > 0);
+
+ var obj = {padCount:padCount.get(), writeCount:writeCount.get(), elapsed:elapsed, removeCount:removeCount.get()};
+ if (! interesting) obj.boring = true;
+ if (interesting) {
+ profiler.record("writeAll", profiler.time()-startWriteTime);
+ profiler.print();
+ }
+
+ return obj;
+}*/
+
+function writePadNow(pad, andFlush) {
+ var didWrite = false;
+ var didRemove = false;
+
+ if (pad.exists()) {
+ var dbUpToDate = false;
+ if (pad._meta.status.dirty) {
+ /*log.info("Writing pad "+pad.getId());*/
+ pad._meta.status.dirty = false;
+ //var t1 = +new Date();
+ pad.writeToDB();
+ //var t2 = +new Date();
+ didWrite = true;
+
+ //log.info("Wrote pad "+pad.getId()+" in "+(t2-t1)+" ms.");
+
+ var now = +(new Date);
+ var sched = _dbwriter().scheduledFor.get(pad.getId());
+ if (sched) {
+ var delay = now - sched;
+ if (delay > MIN_WRITE_DELAY_NOTIFY_MS) {
+ log.warn("dbwriter["+pad.getId()+"] behind schedule by "+delay+"ms");
+ }
+ _dbwriter().scheduledFor.remove(pad.getId());
+ }
+ }
+ if (andFlush) {
+ // remove from cache
+ model.removeFromMemory(pad);
+ didRemove = true;
+ }
+ }
+ return {didWrite:didWrite, didRemove:didRemove};
+}
+
+/*function _spawnCall(executor, func, varargs) {
+ var args = Array.prototype.slice.call(arguments, 2);
+ var that = this;
+ executor.schedule(new Runnable({
+ run: function() {
+ func.apply(that, args);
+ }
+ }), 0, TimeUnit.MICROSECONDS);
+}*/
+
diff --git a/etherpad/src/etherpad/pad/easysync2migration.js b/etherpad/src/etherpad/pad/easysync2migration.js
new file mode 100644
index 0000000..c2a1523
--- /dev/null
+++ b/etherpad/src/etherpad/pad/easysync2migration.js
@@ -0,0 +1,675 @@
+/**
+ * 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.
+ */
+
+
+import("etherpad.collab.ace.easysync1");
+import("etherpad.collab.ace.easysync2");
+import("sqlbase.sqlbase");
+import("fastJSON");
+import("sqlbase.sqlcommon.*");
+import("etherpad.collab.ace.contentcollector.sanitizeUnicode");
+
+function _getPadStringArrayNumId(padId, arrayName) {
+ var stmnt = "SELECT NUMID FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " WHERE ("+btquote("ID")+" = ?)";
+
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setString(1, padId);
+ var resultSet = pstmnt.executeQuery();
+ return closing(resultSet, function() {
+ if (! resultSet.next()) {
+ return -1;
+ }
+ return resultSet.getInt(1);
+ });
+ });
+ });
+}
+
+function _getEntirePadStringArray(padId, arrayName) {
+ var numId = _getPadStringArrayNumId(padId, arrayName);
+ if (numId < 0) {
+ return [];
+ }
+
+ var stmnt = "SELECT PAGESTART, OFFSETS, DATA FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " WHERE ("+btquote("NUMID")+" = ?)";
+
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setInt(1, numId);
+ var resultSet = pstmnt.executeQuery();
+ return closing(resultSet, function() {
+ var array = [];
+ while (resultSet.next()) {
+ var pageStart = resultSet.getInt(1);
+ var lengthsString = resultSet.getString(2);
+ var dataString = resultSet.getString(3);
+ var dataIndex = 0;
+ var arrayIndex = pageStart;
+ lengthsString.split(',').forEach(function(len) {
+ if (len) {
+ len = Number(len);
+ array[arrayIndex] = dataString.substr(dataIndex, len);
+ dataIndex += len;
+ }
+ arrayIndex++;
+ });
+ }
+ return array;
+ });
+ });
+ });
+}
+
+function _overwriteEntirePadStringArray(padId, arrayName, array) {
+ var numId = _getPadStringArrayNumId(padId, arrayName);
+ if (numId < 0) {
+ // generate numId
+ withConnection(function(conn) {
+ var ps = conn.prepareStatement("INSERT INTO "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " ("+btquote("ID")+") VALUES (?)",
+ java.sql.Statement.RETURN_GENERATED_KEYS);
+ closing(ps, function() {
+ ps.setString(1, padId);
+ ps.executeUpdate();
+ var keys = ps.getGeneratedKeys();
+ if ((! keys) || (! keys.next())) {
+ throw new Error("Couldn't generate key for "+arrayName+" table for pad "+padId);
+ }
+ closing(keys, function() {
+ numId = keys.getInt(1);
+ });
+ });
+ });
+ }
+
+ withConnection(function(conn) {
+
+ var stmnt1 = "DELETE FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " WHERE ("+btquote("NUMID")+" = ?)";
+ var pstmnt1 = conn.prepareStatement(stmnt1);
+ closing(pstmnt1, function() {
+ pstmnt1.setInt(1, numId);
+ pstmnt1.executeUpdate();
+ });
+
+ var PAGE_SIZE = 20;
+ var numPages = Math.floor((array.length-1) / PAGE_SIZE + 1);
+
+ var PAGES_PER_BATCH = 20;
+ var curPage = 0;
+
+ while (curPage < numPages) {
+ var stmnt2 = "INSERT INTO "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " ("+btquote("NUMID")+", "+btquote("PAGESTART")+", "+btquote("OFFSETS")+
+ ", "+btquote("DATA")+") VALUES (?, ?, ?, ?)";
+ var pstmnt2 = conn.prepareStatement(stmnt2);
+ closing(pstmnt2, function() {
+ for(var n=0;n<PAGES_PER_BATCH && curPage < numPages;n++) {
+ var pageStart = curPage*PAGE_SIZE;
+ var r = pageStart;
+ var lengthPieces = [];
+ var dataPieces = [];
+ for(var i=0;i<PAGE_SIZE;i++) {
+ var str = (array[r] || '');
+ dataPieces.push(str);
+ lengthPieces.push(String(str.length || ''));
+ r++;
+ }
+ var lengthsString = lengthPieces.join(',');
+ var dataString = dataPieces.join('');
+ pstmnt2.setInt(1, numId);
+ pstmnt2.setInt(2, pageStart);
+ pstmnt2.setString(3, lengthsString);
+ pstmnt2.setString(4, dataString);
+ pstmnt2.addBatch();
+
+ curPage++;
+ }
+ pstmnt2.executeBatch();
+ });
+ }
+ });
+
+}
+
+function _getEntirePadJSONArray(padId, arrayName) {
+ var array = _getEntirePadStringArray(padId, arrayName);
+ for(var k in array) {
+ if (array[k]) {
+ array[k] = fastJSON.parse(array[k]);
+ }
+ }
+ return array;
+}
+
+function _overwriteEntirePadJSONArray(padId, arrayName, objArray) {
+ var array = [];
+ for(var k in objArray) {
+ if (objArray[k]) {
+ array[k] = fastJSON.stringify(objArray[k]);
+ }
+ }
+ _overwriteEntirePadStringArray(padId, arrayName, array);
+}
+
+function _getMigrationPad(padId) {
+ var oldRevs = _getEntirePadStringArray(padId, "revs");
+ var oldRevMeta = _getEntirePadJSONArray(padId, "revmeta");
+ var oldAuthors = _getEntirePadJSONArray(padId, "authors");
+ var oldMeta = sqlbase.getJSON("PAD_META", padId);
+
+ var oldPad = {
+ getHeadRevisionNumber: function() {
+ return oldMeta.head;
+ },
+ getRevisionChangesetString: function(r) {
+ return oldRevs[r];
+ },
+ getRevisionAuthor: function(r) {
+ return oldMeta.numToAuthor[oldRevMeta[r].a];
+ },
+ getId: function() { return padId; },
+ getKeyRevisionNumber: function(r) {
+ return Math.floor(r / oldMeta.keyRevInterval) * oldMeta.keyRevInterval;
+ },
+ getInternalRevisionText: function(r) {
+ if (r != oldPad.getKeyRevisionNumber(r)) {
+ throw new Error("Assertion error: "+r+" != "+oldPad.getKeyRevisionNumber(r));
+ }
+ return oldRevMeta[r].atext.text;
+ },
+ _meta: oldMeta,
+ getAuthorArrayEntry: function(n) {
+ return oldAuthors[n];
+ },
+ getRevMetaArrayEntry: function(r) {
+ return oldRevMeta[r];
+ }
+ };
+
+ var apool = new easysync2.AttribPool();
+ var newRevMeta = [];
+ var newAuthors = [];
+ var newRevs = [];
+ var metaPropsToDelete = [];
+
+ var newPad = {
+ pool: function() { return apool; },
+ setAuthorArrayEntry: function(n, obj) {
+ newAuthors[n] = obj;
+ },
+ setRevMetaArrayEntry: function(r, obj) {
+ newRevMeta[r] = obj;
+ },
+ setRevsArrayEntry: function(r, cs) {
+ newRevs[r] = cs;
+ },
+ deleteMetaProp: function(propName) {
+ metaPropsToDelete.push(propName);
+ }
+ };
+
+ function writeToDB() {
+ var newMeta = {};
+ for(var k in oldMeta) {
+ newMeta[k] = oldMeta[k];
+ }
+ metaPropsToDelete.forEach(function(p) {
+ delete newMeta[p];
+ });
+
+ sqlbase.putJSON("PAD_META", padId, newMeta);
+ sqlbase.putJSON("PAD_APOOL", padId, apool.toJsonable());
+
+ _overwriteEntirePadStringArray(padId, "revs", newRevs);
+ _overwriteEntirePadJSONArray(padId, "revmeta", newRevMeta);
+ _overwriteEntirePadJSONArray(padId, "authors", newAuthors);
+ }
+
+ return {oldPad:oldPad, newPad:newPad, writeToDB:writeToDB};
+}
+
+function migratePad(padId) {
+
+ var mpad = _getMigrationPad(padId);
+ var oldPad = mpad.oldPad;
+ var newPad = mpad.newPad;
+
+ var headRev = oldPad.getHeadRevisionNumber();
+ var txt = "\n";
+ var newChangesets = [];
+ var newChangesetAuthorNums = [];
+ var cumCs = easysync2.Changeset.identity(1);
+
+ var pool = newPad.pool();
+
+ var isExtraFinalNewline = false;
+
+ function authorToNewNum(author) {
+ return pool.putAttrib(['author',author||'']);
+ }
+
+ //S var oldTotalChangesetSize = 0;
+ //S var newTotalChangesetSize = 0;
+ //S function stringSize(str) {
+ //S return new java.lang.String(str).getBytes("UTF-8").length;
+ //S }
+
+ //P var diffTotals = [];
+ for(var r=0;r<=headRev;r++) {
+ //P var times = [];
+ //P times.push(+new Date);
+ var author = oldPad.getRevisionAuthor(r);
+ //P times.push(+new Date);
+ newChangesetAuthorNums.push(authorToNewNum(author));
+
+ var newCs, newText;
+ if (r == 0) {
+ newText = oldPad.getInternalRevisionText(0);
+ newCs = getInitialChangeset(newText, pool, author);
+ //S oldTotalChangesetSize += stringSize(pad.getRevisionChangesetString(0));
+ }
+ else {
+ var oldCsStr = oldPad.getRevisionChangesetString(r);
+ //S oldTotalChangesetSize += stringSize(oldCsStr);
+ //P times.push(+new Date);
+ var oldCs = easysync1.Changeset.decodeFromString(oldCsStr);
+ //P times.push(+new Date);
+
+ /*var newTextFromOldCs = oldCs.applyToText(txt);
+ if (newTextFromOldCs.charAt(newTextFromOldCs.length-1) != '\n') {
+ var e = new Error("Violation of final newline property at revision "+r);
+ e.finalNewlineMissing = true;
+ throw e;
+ }*/
+ //var newCsNewTxt1 = upgradeChangeset(oldCs, txt, pool, author);
+ var oldIsExtraFinalNewline = isExtraFinalNewline;
+ var newCsNewTxt2 = upgradeChangeset(oldCs, txt, pool, author, isExtraFinalNewline);
+ //P times.push(+new Date);
+ /*if (newCsNewTxt1[1] != newCsNewTxt2[1]) {
+ _putFile(newCsNewTxt1[1], "/tmp/file1");
+ _putFile(newCsNewTxt2[1], "/tmp/file2");
+ throw new Error("MISMATCH 1");
+ }
+ if (newCsNewTxt1[0] != newCsNewTxt2[0]) {
+ _putFile(newCsNewTxt1[0], "/tmp/file1");
+ _putFile(newCsNewTxt2[0], "/tmp/file2");
+ throw new Error("MISMATCH 0");
+ }*/
+ newCs = newCsNewTxt2[0];
+ newText = newCsNewTxt2[1];
+ isExtraFinalNewline = newCsNewTxt2[2];
+
+ /*if (oldIsExtraFinalNewline || isExtraFinalNewline) {
+ System.out.print("\nnewline fix for rev "+r+"/"+headRev+"... ");
+ }*/
+ }
+
+ var oldText = txt;
+ newChangesets.push(newCs);
+ txt = newText;
+ //System.out.println(easysync2.Changeset.toBaseTen(cumCs)+" * "+
+ //easysync2.Changeset.toBaseTen(newCs));
+ /*cumCs = easysync2.Changeset.checkRep(easysync2.Changeset.compose(cumCs, newCs));
+ if (easysync2.Changeset.applyToText(cumCs, "\n") != txt) {
+ throw new Error("cumCs mismatch");
+ }*/
+
+ //P times.push(+new Date);
+
+ easysync2.Changeset.checkRep(newCs);
+ //P times.push(+new Date);
+ var origText = txt;
+ if (isExtraFinalNewline) {
+ origText = origText.slice(0, -1);
+ }
+ if (r == oldPad.getKeyRevisionNumber(r)) {
+ // only check key revisions (and final outcome), for speed
+ if (oldPad.getInternalRevisionText(r) != origText) {
+ var expected = oldPad.getInternalRevisionText(r);
+ var actual = origText;
+ //_putFile(expected, "/tmp/file1");
+ //_putFile(actual, "/tmp/file2");
+ //_putFile(oldText, "/tmp/file3");
+ //java.lang.System.out.println(String(oldCs));
+ //java.lang.System.out.println(easysync2.Changeset.toBaseTen(newCs));
+ throw new Error("Migration mismatch, pad "+padId+", revision "+r);
+ }
+ }
+
+ //S newTotalChangesetSize += stringSize(newCs);
+
+ //P if (r > 0) {
+ //P var diffs = [];
+ //P for(var i=0;i<times.length-1;i++) {
+ //P diffs[i] = times[i+1] - times[i];
+ //P }
+ //P for(var i=0;i<diffs.length;i++) {
+ //P diffTotals[i] = (diffTotals[i] || 0) + diffs[i]*1000/headRev;
+ //P }
+ //P }
+ }
+ //P System.out.println(String(diffTotals));
+
+ //S System.out.println("New data is "+(newTotalChangesetSize/oldTotalChangesetSize*100)+
+ //S "% size of old data (average "+(newTotalChangesetSize/(headRev+1))+
+ //S " bytes instead of "+(oldTotalChangesetSize/(headRev+1))+")");
+
+ var atext = easysync2.Changeset.makeAText("\n");
+ for(var r=0; r<=headRev; r++) {
+ newPad.setRevsArrayEntry(r, newChangesets[r]);
+
+ atext = easysync2.Changeset.applyToAText(newChangesets[r], atext, pool);
+
+ var rm = oldPad.getRevMetaArrayEntry(r);
+ rm.a = newChangesetAuthorNums[r];
+ if (rm.atext) {
+ rm.atext = easysync2.Changeset.cloneAText(atext);
+ }
+ newPad.setRevMetaArrayEntry(r, rm);
+ }
+
+ var newAuthors = [];
+ var newAuthorDatas = [];
+ for(var k in oldPad._meta.numToAuthor) {
+ var n = Number(k);
+ var authorData = oldPad.getAuthorArrayEntry(n) || {};
+ var authorName = oldPad._meta.numToAuthor[n];
+ var newAuthorNum = pool.putAttrib(['author',authorName]);
+ newPad.setAuthorArrayEntry(newAuthorNum, authorData);
+ }
+
+ newPad.deleteMetaProp('numToAuthor');
+ newPad.deleteMetaProp('authorToNum');
+
+ mpad.writeToDB();
+}
+
+function getInitialChangeset(txt, pool, author) {
+ var txt2 = txt.substring(0, txt.length-1); // strip off final newline
+
+ var assem = easysync2.Changeset.smartOpAssembler();
+ assem.appendOpWithText('+', txt2, pool && author && [['author',author]], pool);
+ assem.endDocument();
+ return easysync2.Changeset.pack(1, txt2.length+1, assem.toString(), txt2);
+}
+
+function upgradeChangeset(cs, inputText, pool, author, isExtraNewlineInSource) {
+ var attribs = '';
+ if (pool && author) {
+ attribs = '*'+easysync2.Changeset.numToString(pool.putAttrib(['author', author]));
+ }
+
+ function keepLastCharacter(c) {
+ if (! c[c.length-1] && c[c.length-3] + c[c.length-2] >= (c.oldLen() - 1)) {
+ c[c.length-2] = c.oldLen() - c[c.length-3];
+ }
+ else {
+ c.push(c.oldLen() - 1, 1, "");
+ }
+ }
+
+ var isExtraNewlineInOutput = false;
+ if (isExtraNewlineInSource) {
+ cs[1] += 1; // oldLen ++
+ }
+ if ((cs[cs.length-1] && cs[cs.length-1].slice(-1) != '\n') ||
+ ((! cs[cs.length-1]) && inputText.charAt(cs[cs.length-3] + cs[cs.length-2] - 1) != '\n')) {
+ // new text won't end with newline!
+ if (isExtraNewlineInSource) {
+ keepLastCharacter(cs);
+ }
+ else {
+ cs[cs.length-1] += "\n";
+ }
+ cs[2] += 1; // newLen ++
+ isExtraNewlineInOutput = true;
+ }
+
+ var oldLen = cs.oldLen();
+ var newLen = cs.newLen();
+
+ // final-newline-preserving modifications to changeset {{{
+ // These fixes are required for changesets that don't respect the
+ // new rule that the final newline of the document not be touched,
+ // and also for changesets tweaked above. It is important that the
+ // fixed changesets obey all the constraints on version 1 changesets
+ // so that they may become valid version 2 changesets.
+ {
+ function collapsePotentialEmptyLastTake(c) {
+ if (c[c.length-2] == 0 && c.length > 6) {
+ if (! c[c.length-1]) {
+ // last strip doesn't take or insert now
+ c.length -= 3;
+ }
+ else {
+ // the last two strips should be merged
+ // e.g. fo\n -> rock\nbar\n: then in this block,
+ // "Changeset,3,9,0,0,r,1,1,ck,2,0,\nbar" becomes
+ // "Changeset,3,9,0,0,r,1,1,ck\nbar"
+ c[c.length-4] += c[c.length-1];
+ c.length -= 3;
+ }
+ }
+ }
+ var lastStripStart = cs[cs.length-3];
+ var lastStripTake = cs[cs.length-2];
+ var lastStripInsert = cs[cs.length-1];
+ if (lastStripStart + lastStripTake == oldLen && lastStripInsert) {
+ // an insert at end
+ // e.g. foo\n -> foo\nbar\n:
+ // "Changeset,4,8,0,4,bar\n" becomes "Changeset,4,8,0,3,\nbar,3,1,"
+ // first make the previous newline part of the insertion
+ cs[cs.length-2] -= 1;
+ cs[cs.length-1] = '\n'+cs[cs.length-1].slice(0,-1);
+ collapsePotentialEmptyLastTake(cs);
+ keepLastCharacter(cs);
+ }
+ else if (lastStripStart + lastStripTake < oldLen && ! lastStripInsert) {
+ // ends with pure deletion
+ cs[cs.length-2] -= 1;
+ collapsePotentialEmptyLastTake(cs);
+ keepLastCharacter(cs);
+ }
+ else if (lastStripStart + lastStripTake < oldLen) {
+ // ends with replacement
+ cs[cs.length-1] = cs[cs.length-1].slice(0,-1);
+ keepLastCharacter(cs);
+ }
+ }
+ // }}}
+
+ var ops = [];
+ var lastOpcode = '';
+ function appendOp(opcode, text, startChar, endChar) {
+ function num(n) {
+ return easysync2.Changeset.numToString(n);
+ }
+ var lines = 0;
+ var lastNewlineEnd = startChar;
+ for (;;) {
+ var index = text.indexOf('\n', lastNewlineEnd);
+ if (index < 0 || index >= endChar) {
+ break;
+ }
+ lines++;
+ lastNewlineEnd = index+1;
+ }
+ var a = (opcode == '+' ? attribs : '');
+ var multilineChars = (lastNewlineEnd - startChar);
+ var seqLength = endChar - startChar;
+ var op = '';
+ if (lines > 0) {
+ op = [a, '|', num(lines), opcode, num(multilineChars)].join('');
+ }
+ if (multilineChars < seqLength) {
+ op += [a, opcode, num(seqLength - multilineChars)].join('');
+ }
+ if (op) {
+ // we reorder a single - and a single +
+ if (opcode == '-' && lastOpcode == '+') {
+ ops.splice(ops.length-1, 0, op);
+ }
+ else {
+ ops.push(op);
+ lastOpcode = opcode;
+ }
+ }
+ }
+
+ var oldPos = 0;
+
+ var textPieces = [];
+ var charBankPieces = [];
+ cs.eachStrip(function(start, take, insert) {
+ if (start > oldPos) {
+ appendOp('-', inputText, oldPos, start);
+ }
+ if (take) {
+ if (start+take < oldLen || insert) {
+ appendOp('=', inputText, start, start+take);
+ }
+ textPieces.push(inputText.substring(start, start+take));
+ }
+ if (insert) {
+ appendOp('+', insert, 0, insert.length);
+ textPieces.push(insert);
+ charBankPieces.push(insert);
+ }
+ oldPos = start+take;
+ });
+ // ... and no final deletions after the newline fixing.
+
+ var newCs = easysync2.Changeset.pack(oldLen, newLen, ops.join(''),
+ sanitizeUnicode(charBankPieces.join('')));
+ var newText = textPieces.join('');
+
+ return [newCs, newText, isExtraNewlineInOutput];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// unicode issues: 5SaYQp7cKV
+
+// // hard-coded just for testing; any pad is allowed to have corruption.
+// var newlineCorruptedPads = [
+// '0OCGFKkjDv', '14dWjOiOxP', '1LL8XQCBjC', '1jMnjEEK6e', '21',
+// '23DytOPN7d', '32YzfdT2xS', '3E6GB7l7FZ', '3Un8qaCfJh', '3YAj3rC9em',
+// '3vY2eaHSw5', '4834RRTLlg', '4Fm1iVSTWI', '5NpTNqWHGC', '7FYNSdYQVa',
+// '7RZCbvgw1z', '8EVpyN6HyY', '8P5mPRxPVr', '8aHFRmLxKR', '8dsj9eGQfP',
+// 'BSoGobOJZZ', 'Bf0uVghKy0', 'C2f3umStKd', 'CHlu2CA8F3', 'D2WEwgvg1W',
+// 'DNLTpuP2wl', 'DwNpm2TDgu', 'EKPByZ3EGZ', 'FwQxu6UKQx', 'HUn9O34rFl',
+// 'JKZhxMo20E', 'JVjuukL42N', 'JVuBlWxaxL', 'Jmw5lPNYcl', 'KnZHz6jE2P',
+// 'Luyp6ylbgR', 'MB6lPoN1eI', 'McsCrQUM6c', 'NWIuVobIw9', 'OKERTLQCCn',
+// 'OchiOchi', 'OfhKHCB8jJ', 'OkM3Jv3XY9', 'PX5Z89mx29', 'PdmKQIvOEd',
+// 'R9NQNB66qt', 'RvULFSvCbV', 'RyLJC6Qo1x', 'SBlKLwr2Ag', 'SavD72Q9P7',
+// 'SfXyxseAeF', 'TTGZ4yO2PI', 'U3U7rT3d6w', 'UFmqpQIDAi', 'V7Or0QQk4m',
+// 'VPCM5ReAQm', 'VvIYHzIJUY', 'W0Ccc3BVGb', 'Wv3cGgSgjg', 'WwVPgaZUK5',
+// 'WyIFUJXfm5', 'XxESEsgQ6R', 'Yc5Yq3WCuU', 'ZRqCFaRx6h', 'ZepX6TLFbD',
+// 'bSeImT5po4', 'bqIlTkFDiH', 'btt9vNPSQ9', 'c97YJj8PSN', 'd9YV3sypKF',
+// 'eDzzkrwDRU', 'eFQJZWclzo', 'eaz44OhFDu', 'ehKkx1YpLA', 'ep',
+// 'foNq3v3e9T', 'form6rooma', 'fqhtIHG0Ii', 'fvZyCRZjv2', 'gZnadICPYV',
+// 'gvGXtMKhQk', 'h7AYuTxUOd', 'hc1UZSti3J', 'hrFQtae2jW', 'i8rENUZUMu',
+// 'iFW9dceEmh', 'iRNEc8SlOc', 'jEDsDgDlaK', 'jo8ngXlSJh', 'kgJrB9Gh2M',
+// 'klassennetz76da2661f8ceccfe74faf97d25a4b418',
+// 'klassennetzf06d4d8176d0804697d9650f836cb1f7', 'lDHgmfyiSu',
+// 'mA1cbvxFwA', 'mSJpW1th29', 'mXHAqv1Emu', 'monocles12', 'n0NhU3FxxT',
+// 'ng7AlzPb5b', 'ntbErnnuyz', 'oVnMO0dX80', 'omOTPVY3Gl', 'p5aNFCfYG9',
+// 'pYxjVCILuL', 'phylab', 'pjVBFmnhf1', 'qGohFW3Lbr', 'qYlbjeIHDs',
+// 'qgf4OwkFI6', 'qsi', 'rJQ09pRexM', 'snNjlS1aLC', 'tYKC53TDF9',
+// 'u1vZmL8Yjv', 'ur4sb7DBJB', 'vesti', 'w9NJegEAZt', 'wDwlSCby2s',
+// 'wGFJJRT514', 'wTgEoQGqng', 'xomMZGhius', 'yFEFYWBSvr', 'z7tGFKsGk6',
+// 'zIJWNK8Z4i', 'zNMGJYI7hq'];
+
+// function _time(f) {
+// var t1 = +(new Date);
+// f();
+// var t2 = +(new Date);
+// return t2 - t1;
+// }
+
+// function listAllRevisionCounts() {
+// var padList = sqlbase.getAllJSONKeys("PAD_META");
+// //padList.length = 10;
+// padList = padList.slice(68000, 68100);
+// padList.forEach(function(id) {
+// model.accessPadGlobal(id, function(pad) {
+// System.out.println((new java.lang.Integer(pad.getHeadRevisionNumber()).toString())+
+// " "+id);
+// dbwriter.writePadNow(pad, true);
+// }, 'r');
+// });
+// }
+
+// function verifyAllPads() {
+// //var padList = sqlbase.getAllJSONKeys("PAD_META");
+// //padList = newlineCorruptedPads;
+// var padList = ['0OCGFKkjDv'];
+// //padList = ['form6rooma'];
+// //padList.length = 10;
+// var numOks = 0;
+// var numErrors = 0;
+// var numNewlineBugs = 0;
+// var longestPad;
+// var longestPadTime = -1;
+// System.out.println(padList.length+" pads.");
+// var totalTime = _time(function() {
+// padList.forEach(function(id) {
+// model.accessPadGlobal(id, function(pad) {
+// var padTime = _time(function() {
+// System.out.print(id+"... ");
+// try {
+// verifyMigration(pad);
+// System.out.println("OK ("+(++numOks)+")");
+// }
+// catch (e) {
+// System.out.println("ERROR ("+(++numErrors)+")"+(e.finalNewlineMissing?" [newline]":""));
+// System.out.println(e.toString());
+// if (e.finalNewlineMissing) {
+// numNewlineBugs++;
+// }
+// }
+// });
+// if (padTime > longestPadTime) {
+// longestPadTime = padTime;
+// longestPad = id;
+// }
+// }, 'r');
+// });
+// });
+// System.out.println("finished verifyAllPads in "+(totalTime/1000)+" seconds.");
+// System.out.println(numOks+" OK");
+// System.out.println(numErrors+" ERROR");
+// System.out.println("Most time-consuming pad: "+longestPad+" / "+longestPadTime+" ms");
+// }
+
+// function _literal(v) {
+// if ((typeof v) == "string") {
+// return '"'+v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')+'"';
+// }
+// else return v.toSource();
+// }
+
+// function _putFile(str, path) {
+// var writer = new java.io.FileWriter(path);
+// writer.write(str);
+// writer.close();
+// }
diff --git a/etherpad/src/etherpad/pad/exporthtml.js b/etherpad/src/etherpad/pad/exporthtml.js
new file mode 100644
index 0000000..2512603
--- /dev/null
+++ b/etherpad/src/etherpad/pad/exporthtml.js
@@ -0,0 +1,383 @@
+/**
+ * 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.
+ */
+
+import("etherpad.collab.ace.easysync2.Changeset");
+
+function getPadPlainText(pad, revNum) {
+ var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) :
+ pad.atext());
+ var textLines = atext.text.slice(0,-1).split('\n');
+ var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
+ var apool = pad.pool();
+
+ var pieces = [];
+ for(var i=0;i<textLines.length;i++) {
+ var line = _analyzeLine(textLines[i], attribLines[i], apool);
+ if (line.listLevel) {
+ var numSpaces = line.listLevel*2-1;
+ var bullet = '*';
+ pieces.push(new Array(numSpaces+1).join(' '), bullet, ' ', line.text, '\n');
+ }
+ else {
+ pieces.push(line.text, '\n');
+ }
+ }
+
+ return pieces.join('');
+}
+
+function getPadHTML(pad, revNum) {
+ var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) :
+ pad.atext());
+ var textLines = atext.text.slice(0,-1).split('\n');
+ var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
+
+ var apool = pad.pool();
+
+ var tags = ['b','i','u','s','h1','h2','h3','h4','h5','h6'];
+ var props = ['bold','italic','underline','strikethrough','h1','h2','h3','h4','h5','h6'];
+ var anumMap = {};
+ props.forEach(function(propName, i) {
+ var propTrueNum = apool.putAttrib([propName,true], true);
+ if (propTrueNum >= 0) {
+ anumMap[propTrueNum] = i;
+ }
+ });
+
+ function getLineHTML(text, attribs) {
+ var propVals = [false, false, false];
+ var ENTER = 1;
+ var STAY = 2;
+ var LEAVE = 0;
+
+ // Use order of tags (b/i/u) as order of nesting, for simplicity
+ // and decent nesting. For example,
+ // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
+ // becomes
+ // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
+
+ var taker = Changeset.stringIterator(text);
+ var assem = Changeset.stringAssembler();
+
+ function emitOpenTag(i) {
+ assem.append('<');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
+ function emitCloseTag(i) {
+ assem.append('</');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
+
+ var urls = _findURLs(text);
+
+ var idx = 0;
+ function processNextChars(numChars) {
+ if (numChars <= 0) {
+ return;
+ }
+
+ var iter = Changeset.opIterator(Changeset.subattribution(attribs,
+ idx, idx+numChars));
+ idx += numChars;
+
+ while (iter.hasNext()) {
+ var o = iter.next();
+ var propChanged = false;
+ Changeset.eachAttribNumber(o.attribs, function(a) {
+ if (a in anumMap) {
+ var i = anumMap[a]; // i = 0 => bold, etc.
+ if (! propVals[i]) {
+ propVals[i] = ENTER;
+ propChanged = true;
+ }
+ else {
+ propVals[i] = STAY;
+ }
+ }
+ });
+ for(var i=0;i<propVals.length;i++) {
+ if (propVals[i] === true) {
+ propVals[i] = LEAVE;
+ propChanged = true;
+ }
+ else if (propVals[i] === STAY) {
+ propVals[i] = true; // set it back
+ }
+ }
+ // now each member of propVal is in {false,LEAVE,ENTER,true}
+ // according to what happens at start of span
+
+ if (propChanged) {
+ // leaving bold (e.g.) also leaves italics, etc.
+ var left = false;
+ for(var i=0;i<propVals.length;i++) {
+ var v = propVals[i];
+ if (! left) {
+ if (v === LEAVE) {
+ left = true;
+ }
+ }
+ else {
+ if (v === true) {
+ propVals[i] = STAY; // tag will be closed and re-opened
+ }
+ }
+ }
+
+ for(var i=propVals.length-1; i>=0; i--) {
+ if (propVals[i] === LEAVE) {
+ emitCloseTag(i);
+ propVals[i] = false;
+ }
+ else if (propVals[i] === STAY) {
+ emitCloseTag(i);
+ }
+ }
+ for(var i=0; i<propVals.length; i++) {
+ if (propVals[i] === ENTER || propVals[i] === STAY) {
+ emitOpenTag(i);
+ propVals[i] = true;
+ }
+ }
+ // propVals is now all {true,false} again
+ } // end if (propChanged)
+
+ var chars = o.chars;
+ if (o.lines) {
+ chars--; // exclude newline at end of line, if present
+ }
+ var s = taker.take(chars);
+
+ assem.append(_escapeHTML(s));
+ } // end iteration over spans in line
+
+ for(var i=propVals.length-1; i>=0; i--) {
+ if (propVals[i]) {
+ emitCloseTag(i);
+ propVals[i] = false;
+ }
+ }
+ } // end processNextChars
+
+ if (urls) {
+ urls.forEach(function(urlData) {
+ var startIndex = urlData[0];
+ var url = urlData[1];
+ var urlLength = url.length;
+ processNextChars(startIndex - idx);
+ assem.append('<a href="'+url.replace(/\"/g, '&quot;')+'">');
+ processNextChars(urlLength);
+ assem.append('</a>');
+ });
+ }
+ processNextChars(text.length - idx);
+
+ return _processSpaces(assem.toString());
+ } // end getLineHTML
+
+ var pieces = [];
+
+ // Need to deal with constraints imposed on HTML lists; can
+ // only gain one level of nesting at once, can't change type
+ // mid-list, etc.
+ // People might use weird indenting, e.g. skip a level,
+ // so we want to do something reasonable there. We also
+ // want to deal gracefully with blank lines.
+ var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...]
+ for(var i=0;i<textLines.length;i++) {
+ var line = _analyzeLine(textLines[i], attribLines[i], apool);
+ var lineContent = getLineHTML(line.text, line.aline);
+
+ if (line.listLevel || lists.length > 0) {
+ // do list stuff
+ var whichList = -1; // index into lists or -1
+ if (line.listLevel) {
+ whichList = lists.length;
+ for(var j=lists.length-1;j>=0;j--) {
+ if (line.listLevel <= lists[j][0]) {
+ whichList = j;
+ }
+ }
+ }
+
+ if (whichList >= lists.length) {
+ lists.push([line.listLevel, line.listTypeName]);
+ pieces.push('<ul><li>', lineContent || '<br/>');
+ }
+ else if (whichList == -1) {
+ if (line.text) {
+ // non-blank line, end all lists
+ pieces.push(new Array(lists.length+1).join('</li></ul\n>'));
+ lists.length = 0;
+ pieces.push(lineContent, '<br\n/>');
+ }
+ else {
+ pieces.push('<br/><br\n/>');
+ }
+ }
+ else {
+ while (whichList < lists.length-1) {
+ pieces.push('</li></ul\n>');
+ lists.length--;
+ }
+ pieces.push('</li\n><li>', lineContent || '<br/>');
+ }
+ }
+ else {
+ pieces.push(lineContent, '<br\n/>');
+ }
+ }
+ pieces.push(new Array(lists.length+1).join('</li></ul\n>'));
+
+ return pieces.join('');
+}
+
+function _analyzeLine(text, aline, apool) {
+ var line = {};
+
+ // identify list
+ var lineMarker = 0;
+ line.listLevel = 0;
+ if (aline) {
+ var opIter = Changeset.opIterator(aline);
+ if (opIter.hasNext()) {
+ var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
+ if (listType) {
+ lineMarker = 1;
+ listType = /([a-z]+)([12345678])/.exec(listType);
+ if (listType) {
+ line.listTypeName = listType[1];
+ line.listLevel = Number(listType[2]);
+ }
+ }
+ }
+ }
+ if (lineMarker) {
+ line.text = text.substring(1);
+ line.aline = Changeset.subattribution(aline, 1);
+ }
+ else {
+ line.text = text;
+ line.aline = aline;
+ }
+
+ return line;
+}
+
+function getPadHTMLDocument(pad, revNum, noDocType) {
+ var head = (noDocType?'':'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '+
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')+
+ '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">\n'+
+ (noDocType?'':
+ '<head>\n'+
+ '<meta http-equiv="Content-type" content="text/html; charset=utf-8" />\n'+
+ '<meta http-equiv="Content-Language" content="en-us" />\n'+
+ '<title>'+'/'+pad.getId()+'</title>\n'+
+ '<style type="text/css">h1,h2,h3,h4,h5,h6 { display: inline; }</style>\n' +
+ '</head>\n')+
+ '<body>';
+
+ var foot = '</body>\n</html>\n';
+
+ return head + getPadHTML(pad, revNum) + foot;
+}
+
+function _escapeHTML(s) {
+ var re = /[&<>]/g;
+ if (! re.MAP) {
+ // persisted across function calls!
+ re.MAP = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ };
+ }
+ return s.replace(re, function(c) { return re.MAP[c]; });
+}
+
+// copied from ACE
+function _processSpaces(s) {
+ var doesWrap = true;
+ if (s.indexOf("<") < 0 && ! doesWrap) {
+ // short-cut
+ return s.replace(/ /g, '&nbsp;');
+ }
+ var parts = [];
+ s.replace(/<[^>]*>?| |[^ <]+/g, function(m) { parts.push(m); });
+ if (doesWrap) {
+ var endOfLine = true;
+ var beforeSpace = false;
+ // last space in a run is normal, others are nbsp,
+ // end of line is nbsp
+ for(var i=parts.length-1;i>=0;i--) {
+ var p = parts[i];
+ if (p == " ") {
+ if (endOfLine || beforeSpace)
+ parts[i] = '&nbsp;';
+ endOfLine = false;
+ beforeSpace = true;
+ }
+ else if (p.charAt(0) != "<") {
+ endOfLine = false;
+ beforeSpace = false;
+ }
+ }
+ // beginning of line is nbsp
+ for(var i=0;i<parts.length;i++) {
+ var p = parts[i];
+ if (p == " ") {
+ parts[i] = '&nbsp;';
+ break;
+ }
+ else if (p.charAt(0) != "<") {
+ break;
+ }
+ }
+ }
+ else {
+ for(var i=0;i<parts.length;i++) {
+ var p = parts[i];
+ if (p == " ") {
+ parts[i] = '&nbsp;';
+ }
+ }
+ }
+ return parts.join('');
+}
+
+
+// copied from ACE
+var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+var _REGEX_SPACE = /\s/;
+var _REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+_REGEX_WORDCHAR.source+')');
+var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+_REGEX_URLCHAR.source+'*(?![:.,;])'+_REGEX_URLCHAR.source, 'g');
+
+// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
+function _findURLs(text) {
+ _REGEX_URL.lastIndex = 0;
+ var urls = null;
+ var execResult;
+ while ((execResult = _REGEX_URL.exec(text))) {
+ urls = (urls || []);
+ var startIndex = execResult.index;
+ var url = execResult[0];
+ urls.push([startIndex, url]);
+ }
+
+ return urls;
+}
diff --git a/etherpad/src/etherpad/pad/importhtml.js b/etherpad/src/etherpad/pad/importhtml.js
new file mode 100644
index 0000000..4a48c6f
--- /dev/null
+++ b/etherpad/src/etherpad/pad/importhtml.js
@@ -0,0 +1,230 @@
+/**
+ * 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.
+ */
+
+
+jimport("org.ccil.cowan.tagsoup.Parser");
+jimport("org.ccil.cowan.tagsoup.PYXWriter");
+jimport("java.io.StringReader");
+jimport("java.io.StringWriter");
+jimport("org.xml.sax.InputSource");
+
+import("etherpad.collab.ace.easysync2.{Changeset,AttribPool}");
+import("etherpad.collab.ace.contentcollector.makeContentCollector");
+import("etherpad.collab.collab_server");
+
+function setPadHTML(pad, html) {
+ var atext = htmlToAText(html, pad.pool());
+ collab_server.setPadAText(pad, atext);
+}
+
+function _html2pyx(html) {
+ var p = new Parser();
+ var w = new StringWriter();
+ var h = new PYXWriter(w);
+ p.setContentHandler(h);
+ var s = new InputSource();
+ s.setCharacterStream(new StringReader(html));
+ p.parse(s);
+ return w.toString().replace(/\r\n|\r|\n/g, '\n');
+}
+
+function _htmlBody2js(html) {
+ var pyx = _html2pyx(html);
+ var plines = pyx.split("\n");
+
+ function pyxUnescape(s) {
+ return s.replace(/\\t/g, '\t').replace(/\\/g, '\\');
+ }
+ var inAttrs = false;
+
+ var nodeStack = [];
+ var topNode = {};
+
+ var bodyNode = {name:"body"};
+
+ plines.forEach(function(pline) {
+ var t = pline.charAt(0);
+ var v = pline.substring(1);
+ if (inAttrs && t != 'A') {
+ inAttrs = false;
+ }
+ if (t == '?') { /* ignore */ }
+ else if (t == '(') {
+ var newNode = {name: v};
+ if (v.toLowerCase() == "body") {
+ bodyNode = newNode;
+ }
+ topNode.children = (topNode.children || []);
+ topNode.children.push(newNode);
+ nodeStack.push(topNode);
+ topNode = newNode;
+ inAttrs = true;
+ }
+ else if (t == 'A') {
+ var spaceIndex = v.indexOf(' ');
+ var key = v.substring(0, spaceIndex);
+ var value = pyxUnescape(v.substring(spaceIndex+1));
+ topNode.attrs = (topNode.attrs || {});
+ topNode.attrs['$'+key] = value;
+ }
+ else if (t == '-') {
+ if (v == "\\n") {
+ v = '\n';
+ }
+ else {
+ v = pyxUnescape(v);
+ }
+ if (v) {
+ topNode.children = (topNode.children || []);
+ if (topNode.children.length > 0 &&
+ ((typeof topNode.children[topNode.children.length-1]) == "string")) {
+ // coallesce
+ topNode.children.push(topNode.children.pop() + v);
+ }
+ else {
+ topNode.children.push(v);
+ }
+ }
+ }
+ else if (t == ')') {
+ topNode = nodeStack.pop();
+ }
+ });
+
+ return bodyNode;
+}
+
+function _trimDomNode(n) {
+ function isWhitespace(str) {
+ return /^\s*$/.test(str);
+ }
+ function trimBeginningOrEnd(n, endNotBeginning) {
+ var cc = n.children;
+ var backwards = endNotBeginning;
+ if (cc) {
+ var i = (backwards ? cc.length-1 : 0);
+ var done = false;
+ var hitActualText = false;
+ while (! done) {
+ if (! (backwards ? (i >= 0) : (i < cc.length-1))) {
+ done = true;
+ }
+ else {
+ var c = cc[i];
+ if ((typeof c) == "string") {
+ if (! isWhitespace(c)) {
+ // actual text
+ hitActualText = true;
+ break;
+ }
+ else {
+ // whitespace
+ cc[i] = '';
+ }
+ }
+ else {
+ // recurse
+ if (trimBeginningOrEnd(cc[i], endNotBeginning)) {
+ hitActualText = true;
+ break;
+ }
+ }
+ i += (backwards ? -1 : 1);
+ }
+ }
+ n.children = n.children.filter(function(x) { return !!x; });
+ return hitActualText;
+ }
+ return false;
+ }
+ trimBeginningOrEnd(n, false);
+ trimBeginningOrEnd(n, true);
+}
+
+function htmlToAText(html, apool) {
+ var body = _htmlBody2js(html);
+ _trimDomNode(body);
+
+ var dom = {
+ isNodeText: function(n) {
+ return (typeof n) == "string";
+ },
+ nodeTagName: function(n) {
+ return ((typeof n) == "object") && n.name;
+ },
+ nodeValue: function(n) {
+ return String(n);
+ },
+ nodeNumChildren: function(n) {
+ return (((typeof n) == "object") && n.children && n.children.length) || 0;
+ },
+ nodeChild: function(n, i) {
+ return (((typeof n) == "object") && n.children && n.children[i]) || null;
+ },
+ nodeProp: function(n, p) {
+ return (((typeof n) == "object") && n.attrs && n.attrs[p]) || null;
+ },
+ nodeAttr: function(n, a) {
+ return (((typeof n) == "object") && n.attrs && n.attrs[a]) || null;
+ },
+ optNodeInnerHTML: function(n) {
+ return null;
+ }
+ }
+
+ var cc = makeContentCollector(true, null, apool, dom);
+ for(var i=0; i<dom.nodeNumChildren(body); i++) {
+ var n = dom.nodeChild(body, i);
+ cc.collectContent(n);
+ }
+ cc.notifyNextNode(null);
+ var ccData = cc.finish();
+
+ var textLines = ccData.lines;
+ var attLines = ccData.lineAttribs;
+ for(var i=0;i<textLines.length;i++) {
+ var txt = textLines[i];
+ if (txt == " " || txt == "\xa0") {
+ // space or nbsp all alone on a line, remove
+ textLines[i] = "";
+ attLines[i] = "";
+ }
+ }
+
+ var text = textLines.join('\n')+'\n';
+ var attribs = _joinLineAttribs(attLines);
+ var atext = Changeset.makeAText(text, attribs);
+
+ return atext;
+}
+
+function _joinLineAttribs(lineAttribs) {
+ var assem = Changeset.smartOpAssembler();
+
+ var newline = Changeset.newOp('+');
+ newline.chars = 1;
+ newline.lines = 1;
+
+ lineAttribs.forEach(function(aline) {
+ var iter = Changeset.opIterator(aline);
+ while (iter.hasNext()) {
+ assem.append(iter.next());
+ }
+ assem.append(newline);
+ });
+
+ return assem.toString();
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pad/model.js b/etherpad/src/etherpad/pad/model.js
new file mode 100644
index 0000000..3f44dfa
--- /dev/null
+++ b/etherpad/src/etherpad/pad/model.js
@@ -0,0 +1,655 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("timer");
+import("sync");
+
+import("etherpad.collab.ace.easysync2.{Changeset,AttribPool}");
+import("etherpad.log");
+import("etherpad.pad.padevents");
+import("etherpad.pad.padutils");
+import("etherpad.pad.dbwriter");
+import("etherpad.pad.pad_migrations");
+import("etherpad.pad.pad_security");
+import("etherpad.collab.collab_server");
+import("cache_utils.syncedWithCache");
+import("etherpad.admin.plugins");
+
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+jimport("java.lang.System.out.println");
+
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("net.appjet.oui.GlobalSynchronizer");
+jimport("net.appjet.oui.exceptionlog");
+
+function onStartup() {
+ appjet.cache.pads = {};
+ appjet.cache.pads.meta = new ConcurrentHashMap();
+ appjet.cache.pads.temp = new ConcurrentHashMap();
+ appjet.cache.pads.revs = new ConcurrentHashMap();
+ appjet.cache.pads.revs10 = new ConcurrentHashMap();
+ appjet.cache.pads.revs100 = new ConcurrentHashMap();
+ appjet.cache.pads.revs1000 = new ConcurrentHashMap();
+ appjet.cache.pads.chat = new ConcurrentHashMap();
+ appjet.cache.pads.revmeta = new ConcurrentHashMap();
+ appjet.cache.pads.authors = new ConcurrentHashMap();
+ appjet.cache.pads.apool = new ConcurrentHashMap();
+}
+
+var _JSON_CACHE_SIZE = 10000;
+
+// to clear: appjet.cache.padmodel.modelcache.map.clear()
+function _getModelCache() {
+ return syncedWithCache('padmodel.modelcache', function(cache) {
+ if (! cache.map) {
+ cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE);
+ }
+ return cache.map;
+ });
+}
+
+function cleanText(txt) {
+ return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
+}
+
+/**
+ * Access a pad object, which is passed as an argument to
+ * the given padFunc, which is executed inside an exclusive lock,
+ * and return the result. If the pad doesn't exist, a wrapper
+ * object is still created and passed to padFunc, and it can
+ * be used to check whether the pad exists and create it.
+ *
+ * Note: padId is a GLOBAL id.
+ */
+function accessPadGlobal(padId, padFunc, rwMode) {
+ // this may make a nested call to accessPadGlobal, so do it first
+ pad_security.checkAccessControl(padId, rwMode);
+
+ // pad is never loaded into memory (made "active") unless it has been migrated.
+ // Migrations do not use accessPad, but instead access the database directly.
+ pad_migrations.ensureMigrated(padId);
+
+ var mode = (rwMode || "rw").toLowerCase();
+
+ if (! appjet.requestCache.padsAccessing) {
+ appjet.requestCache.padsAccessing = {};
+ }
+ if (appjet.requestCache.padsAccessing[padId]) {
+ // nested access to same pad
+ var p = appjet.requestCache.padsAccessing[padId];
+ var m = p._meta;
+ if (m && mode != "r") {
+ m.status.lastAccess = +new Date();
+ m.status.dirty = true;
+ }
+ return padFunc(p);
+ }
+
+ return doWithPadLock(padId, function() {
+ return sqlcommon.inTransaction(function() {
+ var meta = _getPadMetaData(padId); // null if pad doesn't exist yet
+
+ if (meta && ! meta.status) {
+ meta.status = { validated: false };
+ }
+
+ if (meta && mode != "r") {
+ meta.status.lastAccess = +new Date();
+ }
+
+ function getCurrentAText() {
+ var tempObj = pad.tempObj();
+ if (! tempObj.atext) {
+ tempObj.atext = pad.getInternalRevisionAText(meta.head);
+ }
+ return tempObj.atext;
+ }
+ function addRevision(theChangeset, author, optDatestamp) {
+ var atext = getCurrentAText();
+ var newAText = Changeset.applyToAText(theChangeset, atext, pad.pool());
+ Changeset.copyAText(newAText, atext); // updates pad.tempObj().atext!
+
+ var newRev = ++meta.head;
+
+ var revs = _getPadStringArray(padId, "revs");
+ revs.setEntry(newRev, theChangeset);
+
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ var thisRevMeta = {t: (optDatestamp || (+new Date())),
+ a: getNumForAuthor(author)};
+ if ((newRev % meta.keyRevInterval) == 0) {
+ thisRevMeta.atext = atext;
+ }
+ revmeta.setJSONEntry(newRev, thisRevMeta);
+
+ updateCoarseChangesets(true);
+ }
+ function getNumForAuthor(author, dontAddIfAbsent) {
+ return pad.pool().putAttrib(['author',author||''], dontAddIfAbsent);
+ }
+ function getAuthorForNum(n) {
+ // must return null if n is an attrib number that isn't an author
+ var pair = pad.pool().getAttrib(n);
+ if (pair && pair[0] == 'author') {
+ return pair[1];
+ }
+ return null;
+ }
+
+ function updateCoarseChangesets(onlyIfPresent) {
+ // this is fast to run if the coarse changesets
+ // are up-to-date or almost up-to-date;
+ // if there's no coarse changeset data,
+ // it may take a while.
+
+ if (! meta.coarseHeads) {
+ if (onlyIfPresent) {
+ return;
+ }
+ else {
+ meta.coarseHeads = {10:-1, 100:-1, 1000:-1};
+ }
+ }
+ var head = meta.head;
+ // once we reach head==9, coarseHeads[10] moves
+ // from -1 up to 0; at head==19 it moves up to 1
+ var desiredCoarseHeads = {
+ 10: Math.floor((head-9)/10),
+ 100: Math.floor((head-99)/100),
+ 1000: Math.floor((head-999)/1000)
+ };
+ var revs = _getPadStringArray(padId, "revs");
+ var revs10 = _getPadStringArray(padId, "revs10");
+ var revs100 = _getPadStringArray(padId, "revs100");
+ var revs1000 = _getPadStringArray(padId, "revs1000");
+ var fineArrays = [revs, revs10, revs100];
+ var coarseArrays = [revs10, revs100, revs1000];
+ var levels = [10, 100, 1000];
+ var dirty = false;
+ for(var z=0;z<3;z++) {
+ var level = levels[z];
+ var coarseArray = coarseArrays[z];
+ var fineArray = fineArrays[z];
+ while (meta.coarseHeads[level] < desiredCoarseHeads[level]) {
+ dirty = true;
+ // for example, if the current coarse head is -1,
+ // compose 0-9 inclusive of the finer level and call it 0
+ var x = meta.coarseHeads[level] + 1;
+ var cs = fineArray.getEntry(10 * x);
+ for(var i=1;i<=9;i++) {
+ cs = Changeset.compose(cs, fineArray.getEntry(10*x + i),
+ pad.pool());
+ }
+ coarseArray.setEntry(x, cs);
+ meta.coarseHeads[level] = x;
+ }
+ }
+ if (dirty) {
+ meta.status.dirty = true;
+ }
+ }
+
+ /////////////////// "Public" API starts here (functions used by collab_server or other modules)
+ var pad = {
+ // Operations that write to the data structure should
+ // set meta.dirty = true. Any pad access that isn't
+ // done in "read" mode also sets dirty = true.
+ getId: function() { return padId; },
+ exists: function() { return !!meta; },
+ create: function(optText) {
+ meta = {};
+ meta.head = -1; // incremented below by addRevision
+ pad.tempObj().atext = Changeset.makeAText("\n");
+ meta.padId = padId,
+ meta.keyRevInterval = 100;
+ meta.numChatMessages = 0;
+ var t = +new Date();
+ meta.status = { validated: true };
+ meta.status.lastAccess = t;
+ meta.status.dirty = true;
+ meta.supportsTimeSlider = true;
+
+ var firstChangeset = Changeset.makeSplice("\n", 0, 0,
+ cleanText(optText || ''));
+ addRevision(firstChangeset, '');
+
+ _insertPadMetaData(padId, meta);
+
+ sqlobj.insert("PAD_SQLMETA", {
+ id: padId, version: 2, creationTime: new Date(t), lastWriteTime: new Date(),
+ headRev: meta.head }); // headRev is not authoritative, just for info
+
+ padevents.onNewPad(pad);
+ },
+ destroy: function() { // you may want to collab_server.bootAllUsers first
+ padevents.onDestroyPad(pad);
+
+ _destroyPadStringArray(padId, "revs");
+ _destroyPadStringArray(padId, "revs10");
+ _destroyPadStringArray(padId, "revs100");
+ _destroyPadStringArray(padId, "revs1000");
+ _destroyPadStringArray(padId, "revmeta");
+ _destroyPadStringArray(padId, "chat");
+ _destroyPadStringArray(padId, "authors");
+ _removePadMetaData(padId);
+ _removePadAPool(padId);
+ sqlobj.deleteRows("PAD_SQLMETA", { id: padId });
+ meta = null;
+ },
+ writeToDB: function() {
+ var meta2 = {};
+ for(var k in meta) meta2[k] = meta[k];
+ delete meta2.status;
+ sqlbase.putJSON("PAD_META", padId, meta2);
+
+ plugins.callHook("padModelWriteToDB", {pad:pad, padId:padId});
+
+ _getPadStringArray(padId, "revs").writeToDB();
+ _getPadStringArray(padId, "revs10").writeToDB();
+ _getPadStringArray(padId, "revs100").writeToDB();
+ _getPadStringArray(padId, "revs1000").writeToDB();
+ _getPadStringArray(padId, "revmeta").writeToDB();
+ _getPadStringArray(padId, "chat").writeToDB();
+ _getPadStringArray(padId, "authors").writeToDB();
+ sqlbase.putJSON("PAD_APOOL", padId, pad.pool().toJsonable());
+
+ var props = { headRev: meta.head, lastWriteTime: new Date() };
+ _writePadSqlMeta(padId, props);
+ },
+ pool: function() {
+ return _getPadAPool(padId);
+ },
+ getHeadRevisionNumber: function() { return meta.head; },
+ getRevisionAuthor: function(r) {
+ var n = _getPadStringArray(padId, "revmeta").getJSONEntry(r).a;
+ return getAuthorForNum(Number(n));
+ },
+ getRevisionChangeset: function(r) {
+ return _getPadStringArray(padId, "revs").getEntry(r);
+ },
+ tempObj: function() { return _getPadTemp(padId); },
+ getKeyRevisionNumber: function(r) {
+ return Math.floor(r / meta.keyRevInterval) * meta.keyRevInterval;
+ },
+ getInternalRevisionAText: function(r) {
+ var cacheKey = "atext/C/"+r+"/"+padId;
+ var modelCache = _getModelCache();
+ var cachedValue = modelCache.get(cacheKey);
+ if (cachedValue) {
+ modelCache.touch(cacheKey);
+ //java.lang.System.out.println("HIT! "+cacheKey);
+ return Changeset.cloneAText(cachedValue);
+ }
+ //java.lang.System.out.println("MISS! "+cacheKey);
+
+ var revs = _getPadStringArray(padId, "revs");
+ var keyRev = pad.getKeyRevisionNumber(r);
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ var atext = revmeta.getJSONEntry(keyRev).atext;
+ var curRev = keyRev;
+ var targetRev = r;
+ var apool = pad.pool();
+ while (curRev < targetRev) {
+ curRev++;
+ var cs = pad.getRevisionChangeset(curRev);
+ atext = Changeset.applyToAText(cs, atext, apool);
+ }
+ modelCache.put(cacheKey, Changeset.cloneAText(atext));
+ return atext;
+ },
+ getInternalRevisionText: function(r, optInfoObj) {
+ var atext = pad.getInternalRevisionAText(r);
+ var text = atext.text;
+ if (optInfoObj) {
+ if (text.slice(-1) != "\n") {
+ optInfoObj.badLastChar = text.slice(-1);
+ }
+ }
+ return text;
+ },
+ getRevisionText: function(r, optInfoObj) {
+ var internalText = pad.getInternalRevisionText(r, optInfoObj);
+ return internalText.slice(0, -1);
+ },
+ atext: function() { return Changeset.cloneAText(getCurrentAText()); },
+ text: function() { return pad.atext().text; },
+ getRevisionDate: function(r) {
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ return new Date(revmeta.getJSONEntry(r).t);
+ },
+ // note: calls like appendRevision will NOT notify clients of the change!
+ // you must go through collab_server.
+ // Also, be sure to run cleanText() on any text to strip out carriage returns
+ // and other stuff.
+ appendRevision: function(theChangeset, author, optDatestamp) {
+ addRevision(theChangeset, author || '', optDatestamp);
+ },
+ appendChatMessage: function(obj) {
+ var index = meta.numChatMessages;
+ meta.numChatMessages++;
+ var chat = _getPadStringArray(padId, "chat");
+ chat.setJSONEntry(index, obj);
+ },
+ getNumChatMessages: function() {
+ return meta.numChatMessages;
+ },
+ getChatMessage: function(i) {
+ var chat = _getPadStringArray(padId, "chat");
+ return chat.getJSONEntry(i);
+ },
+ getPadOptionsObj: function() {
+ var data = pad.getDataRoot();
+ if (! data.padOptions) {
+ data.padOptions = {};
+ }
+ if ((! data.padOptions.guestPolicy) ||
+ (data.padOptions.guestPolicy == 'ask')) {
+ data.padOptions.guestPolicy = 'deny';
+ }
+ return data.padOptions;
+ },
+ getGuestPolicy: function() {
+ // allow/ask/deny
+ return pad.getPadOptionsObj().guestPolicy;
+ },
+ setGuestPolicy: function(policy) {
+ pad.getPadOptionsObj().guestPolicy = policy;
+ },
+ getDataRoot: function() {
+ var dataRoot = meta.dataRoot;
+ if (! dataRoot) {
+ dataRoot = {};
+ meta.dataRoot = dataRoot;
+ }
+ return dataRoot;
+ },
+ // returns an object, changes to which are not reflected
+ // in the DB; use setAuthorData for mutation
+ getAuthorData: function(author) {
+ var authors = _getPadStringArray(padId, "authors");
+ var n = getNumForAuthor(author, true);
+ if (n < 0) {
+ return null;
+ }
+ else {
+ return authors.getJSONEntry(n);
+ }
+ },
+ setAuthorData: function(author, data) {
+ var authors = _getPadStringArray(padId, "authors");
+ var n = getNumForAuthor(author);
+ authors.setJSONEntry(n, data);
+ },
+ adoptChangesetAttribs: function(cs, oldPool) {
+ return Changeset.moveOpsToNewPool(cs, oldPool, pad.pool());
+ },
+ eachATextAuthor: function(atext, func) {
+ var seenNums = {};
+ Changeset.eachAttribNumber(atext.attribs, function(n) {
+ if (! seenNums[n]) {
+ seenNums[n] = true;
+ var author = getAuthorForNum(n);
+ if (author) {
+ func(author, n);
+ }
+ }
+ });
+ },
+ getCoarseChangeset: function(start, numChangesets) {
+ updateCoarseChangesets();
+
+ if (!(numChangesets == 10 || numChangesets == 100 ||
+ numChangesets == 1000)) {
+ return null;
+ }
+ var level = numChangesets;
+ var x = Math.floor(start / level);
+ if (!(x >= 0 && x*level == start)) {
+ return null;
+ }
+
+ var cs = _getPadStringArray(padId, "revs"+level).getEntry(x);
+
+ if (! cs) {
+ return null;
+ }
+
+ return cs;
+ },
+ getSupportsTimeSlider: function() {
+ if (! ('supportsTimeSlider' in meta)) {
+ if (padutils.isProPadId(padId)) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ else {
+ return !! meta.supportsTimeSlider;
+ }
+ },
+ setSupportsTimeSlider: function(v) {
+ meta.supportsTimeSlider = v;
+ },
+ get _meta() { return meta; }
+ };
+
+ try {
+ padutils.setCurrentPad(padId);
+ appjet.requestCache.padsAccessing[padId] = pad;
+ return padFunc(pad);
+ }
+ finally {
+ padutils.clearCurrentPad();
+ delete appjet.requestCache.padsAccessing[padId];
+ if (meta) {
+ if (mode != "r") {
+ meta.status.dirty = true;
+ }
+ if (meta.status.dirty) {
+ dbwriter.notifyPadDirty(padId);
+ }
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Call an arbitrary function with no arguments inside an exclusive
+ * lock on a padId, and return the result.
+ */
+function doWithPadLock(padId, func) {
+ var lockName = "document/"+padId;
+ return sync.doWithStringLock(lockName, func);
+}
+
+function isPadLockHeld(padId) {
+ var lockName = "document/"+padId;
+ return GlobalSynchronizer.isHeld(lockName);
+}
+
+/**
+ * Get pad meta-data object, which is stored in SQL as JSON
+ * but cached in appjet.cache. Returns null if pad doesn't
+ * exist at all (does NOT create it). Requires pad lock.
+ */
+function _getPadMetaData(padId) {
+ var padMeta = appjet.cache.pads.meta.get(padId);
+ if (! padMeta) {
+ // not in cache
+ padMeta = sqlbase.getJSON("PAD_META", padId);
+ if (! padMeta) {
+ // not in SQL
+ padMeta = null;
+ }
+ else {
+ appjet.cache.pads.meta.put(padId, padMeta);
+ }
+ }
+ return padMeta;
+}
+
+/**
+ * Sets a pad's meta-data object, such as when creating
+ * a pad for the first time. Requires pad lock.
+ */
+function _insertPadMetaData(padId, obj) {
+ appjet.cache.pads.meta.put(padId, obj);
+}
+
+/**
+ * Removes a pad's meta data, writing through to the database.
+ * Used for the rare case of deleting a pad.
+ */
+function _removePadMetaData(padId) {
+ appjet.cache.pads.meta.remove(padId);
+ sqlbase.deleteJSON("PAD_META", padId);
+}
+
+function _getPadAPool(padId) {
+ var padAPool = appjet.cache.pads.apool.get(padId);
+ if (! padAPool) {
+ // not in cache
+ padAPool = new AttribPool();
+ padAPoolJson = sqlbase.getJSON("PAD_APOOL", padId);
+ if (padAPoolJson) {
+ // in SQL
+ padAPool.fromJsonable(padAPoolJson);
+ }
+ appjet.cache.pads.apool.put(padId, padAPool);
+ }
+ return padAPool;
+}
+
+/**
+ * Removes a pad's apool data, writing through to the database.
+ * Used for the rare case of deleting a pad.
+ */
+function _removePadAPool(padId) {
+ appjet.cache.pads.apool.remove(padId);
+ sqlbase.deleteJSON("PAD_APOOL", padId);
+}
+
+/**
+ * Get an object for a pad that's not persisted in storage,
+ * e.g. for tracking open connections. Creates object
+ * if necessary. Requires pad lock.
+ */
+function _getPadTemp(padId) {
+ var padTemp = appjet.cache.pads.temp.get(padId);
+ if (! padTemp) {
+ padTemp = {};
+ appjet.cache.pads.temp.put(padId, padTemp);
+ }
+ return padTemp;
+}
+
+/**
+ * Returns an object with methods for manipulating a string array, where name
+ * is something like "revs" or "chat". The object must be acquired and used
+ * all within a pad lock.
+ */
+function _getPadStringArray(padId, name) {
+ var padFoo = appjet.cache.pads[name].get(padId);
+ if (! padFoo) {
+ padFoo = {};
+ // writes go into writeCache, which is authoritative for reads;
+ // reads cause pages to be read into readCache
+ padFoo.readCache = {};
+ padFoo.writeCache = {};
+ appjet.cache.pads[name].put(padId, padFoo);
+ }
+ var tableName = "PAD_"+name.toUpperCase();
+ var self = {
+ getEntry: function(idx) {
+ var n = Number(idx);
+ if (padFoo.writeCache[n]) return padFoo.writeCache[n];
+ if (padFoo.readCache[n]) return padFoo.readCache[n];
+ sqlbase.getPageStringArrayElements(tableName, padId, n, padFoo.readCache);
+ return padFoo.readCache[n]; // null if not present in SQL
+ },
+ setEntry: function(idx, value) {
+ var n = Number(idx);
+ var v = String(value);
+ padFoo.writeCache[n] = v;
+ },
+ getJSONEntry: function(idx) {
+ var result = self.getEntry(idx);
+ if (! result) return result;
+ return fastJSON.parse(String(result));
+ },
+ setJSONEntry: function(idx, valueObj) {
+ self.setEntry(idx, fastJSON.stringify(valueObj));
+ },
+ writeToDB: function() {
+ sqlbase.putDictStringArrayElements(tableName, padId, padFoo.writeCache);
+ // copy key-vals of writeCache into readCache
+ var readCache = padFoo.readCache;
+ var writeCache = padFoo.writeCache;
+ for(var p in writeCache) {
+ readCache[p] = writeCache[p];
+ }
+ padFoo.writeCache = {};
+ }
+ };
+ return self;
+}
+
+/**
+ * Destroy a string array; writes through to the database. Must be
+ * called within a pad lock.
+ */
+function _destroyPadStringArray(padId, name) {
+ appjet.cache.pads[name].remove(padId);
+ var tableName = "PAD_"+name.toUpperCase();
+ sqlbase.clearStringArray(tableName, padId);
+}
+
+/**
+ * SELECT the row of PAD_SQLMETA for the given pad. Requires pad lock.
+ */
+function _getPadSqlMeta(padId) {
+ return sqlobj.selectSingle("PAD_SQLMETA", { id: padId });
+}
+
+function _writePadSqlMeta(padId, updates) {
+ sqlobj.update("PAD_SQLMETA", { id: padId }, updates);
+}
+
+
+// called from dbwriter
+function removeFromMemory(pad) {
+ // safe to call if all data is written to SQL, otherwise will lose data;
+ var padId = pad.getId();
+ appjet.cache.pads.meta.remove(padId);
+ appjet.cache.pads.revs.remove(padId);
+ appjet.cache.pads.revs10.remove(padId);
+ appjet.cache.pads.revs100.remove(padId);
+ appjet.cache.pads.revs1000.remove(padId);
+ appjet.cache.pads.chat.remove(padId);
+ appjet.cache.pads.revmeta.remove(padId);
+ appjet.cache.pads.apool.remove(padId);
+ collab_server.removeFromMemory(pad);
+}
+
+
diff --git a/etherpad/src/etherpad/pad/noprowatcher.js b/etherpad/src/etherpad/pad/noprowatcher.js
new file mode 100644
index 0000000..8eb2a92
--- /dev/null
+++ b/etherpad/src/etherpad/pad/noprowatcher.js
@@ -0,0 +1,110 @@
+/**
+ * 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.
+ */
+
+/*
+ * noprowatcher keeps track of when a pad has had no pro user
+ * in it for a certain period of time, after which all guests
+ * are booted.
+ */
+
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+import("etherpad.pad.padusers");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.model");
+import("cache_utils.syncedWithCache");
+import("execution");
+import("etherpad.sessions");
+
+function onStartup() {
+ execution.initTaskThreadPool("noprowatcher", 1);
+}
+
+function getNumProUsers(pad) {
+ var n = 0;
+ collab_server.getConnectedUsers(pad).forEach(function(info) {
+ if (! padusers.isGuest(info.userId)) {
+ n++; // found a non-guest
+ }
+ });
+ return n;
+}
+
+var _EMPTY_TIME = 60000;
+
+function checkPad(padOrPadId) {
+ if ((typeof padOrPadId) == "string") {
+ return model.accessPadGlobal(padOrPadId, function(pad) {
+ return checkPad(pad);
+ });
+ }
+ var pad = padOrPadId;
+
+ if (! padutils.isProPad(pad)) {
+ return; // public pad
+ }
+
+ if (pad.getGuestPolicy() == 'allow') {
+ return; // public access
+ }
+
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+
+ var globalPadId = pad.getId();
+
+ var numConnections = collab_server.getNumConnections(pad);
+ var numProUsers = getNumProUsers(pad);
+ syncedWithCache('noprowatcher.no_pros_since', function(noProsSince) {
+ if (! numConnections) {
+ // no connections, clear state and we're done
+ delete noProsSince[globalPadId];
+ }
+ else if (numProUsers) {
+ // pro users in pad, so we're not in a span of time with
+ // no pro users
+ delete noProsSince[globalPadId];
+ }
+ else {
+ // no pro users in pad
+ var since = noProsSince[globalPadId];
+ if (! since) {
+ // no entry in cache, that means last time we checked
+ // there were still pro users, but now there aren't
+ noProsSince[globalPadId] = +new Date;
+ execution.scheduleTask("noprowatcher", "noProWatcherCheckPad",
+ _EMPTY_TIME+1000, [globalPadId]);
+ }
+ else {
+ // already in a span of time with no pro users
+ if ((+new Date) - since > _EMPTY_TIME) {
+ // _EMPTY_TIME milliseconds since we first noticed no pro users
+ collab_server.bootAllUsersFromPad(pad, "unauth");
+ pad_security.revokeAllPadAccess(globalPadId);
+ }
+ }
+ }
+ });
+}
+
+function onUserJoin(pad, userInfo) {
+ checkPad(pad);
+}
+
+function onUserLeave(pad, userInfo) {
+ checkPad(pad);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pad/pad_migrations.js b/etherpad/src/etherpad/pad/pad_migrations.js
new file mode 100644
index 0000000..e81cf63
--- /dev/null
+++ b/etherpad/src/etherpad/pad/pad_migrations.js
@@ -0,0 +1,206 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.pad.model");
+import("etherpad.pad.easysync2migration");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("java.lang.System");
+jimport("java.util.ArrayList");
+jimport("java.util.Collections");
+
+function onStartup() {
+ if (! appjet.cache.pad_migrations) {
+ appjet.cache.pad_migrations = {};
+ }
+
+ // this part can be removed when all pads are migrated on pad.spline.inf.fu-berlin.de
+ //if (! pne_utils.isPNE()) {
+ // System.out.println("Building cache for live migrations...");
+ // initLiveMigration();
+ //}
+}
+
+function initLiveMigration() {
+
+ if (! appjet.cache.pad_migrations) {
+ appjet.cache.pad_migrations = {};
+ }
+ appjet.cache.pad_migrations.doingAnyLiveMigrations = true;
+ appjet.cache.pad_migrations.doingBackgroundLiveMigrations = true;
+ appjet.cache.pad_migrations.padMap = new ConcurrentHashMap();
+
+ // presence of a pad in padMap indicates migration is needed
+ var padMap = _padMap();
+ var migrationsNeeded = sqlobj.selectMulti("PAD_SQLMETA", {version: 1});
+ migrationsNeeded.forEach(function(obj) {
+ padMap.put(String(obj.id), {from: obj.version});
+ });
+}
+
+function _padMap() {
+ return appjet.cache.pad_migrations.padMap;
+}
+
+function _doingItLive() {
+ return !! appjet.cache.pad_migrations.doingAnyLiveMigrations;
+}
+
+function checkPadStatus(padId) {
+ if (! _doingItLive()) {
+ return "ready";
+ }
+ var info = _padMap().get(padId);
+ if (! info) {
+ return "ready";
+ }
+ else if (info.migrating) {
+ return "migrating";
+ }
+ else {
+ return "oldversion";
+ }
+}
+
+function ensureMigrated(padId, async) {
+ if (! _doingItLive()) {
+ return false;
+ }
+
+ var info = _padMap().get(padId);
+ if (! info) {
+ // pad is up-to-date
+ return false;
+ }
+ else if (async && info.migrating) {
+ // pad is already being migrated, don't wait on the lock
+ return false;
+ }
+
+ return model.doWithPadLock(padId, function() {
+ // inside pad lock...
+ var info = _padMap().get(padId);
+ if (!info) {
+ return false;
+ }
+ // migrate from version 1 to version 2 in a transaction
+ var migrateSucceeded = false;
+ try {
+ info.migrating = true;
+ log.info("Migrating pad "+padId+" from version 1 to version 2...");
+
+ var success = false;
+ var whichTry = 1;
+ while ((! success) && whichTry <= 3) {
+ success = sqlcommon.inTransaction(function() {
+ try {
+ easysync2migration.migratePad(padId);
+ sqlobj.update("PAD_SQLMETA", {id: padId}, {version: 2});
+ return true;
+ }
+ catch (e if (e.toString().indexOf("try restarting transaction") >= 0)) {
+ whichTry++;
+ return false;
+ }
+ });
+ if (! success) {
+ java.lang.Thread.sleep(Math.floor(Math.random()*200));
+ }
+ }
+ if (! success) {
+ throw new Error("too many retries");
+ }
+
+ migrateSucceeded = true;
+ log.info("Migrated pad "+padId+".");
+ _padMap().remove(padId);
+ }
+ finally {
+ info.migrating = false;
+ if (! migrateSucceeded) {
+ log.info("Migration failed for pad "+padId+".");
+ throw new Error("Migration failed for pad "+padId+".");
+ }
+ }
+ return true;
+ });
+}
+
+function numUnmigratedPads() {
+ if (! _doingItLive()) {
+ return 0;
+ }
+
+ return _padMap().size();
+}
+
+////////// BACKGROUND MIGRATIONS
+
+function _logPadMigration(runnerId, padNumber, padTotal, timeMs, fourCharResult, padId) {
+ log.custom("pad_migrations", {
+ runnerId: runnerId,
+ padNumber: Math.round(padNumber+1),
+ padTotal: Math.round(padTotal),
+ timeMs: Math.round(timeMs),
+ fourCharResult: fourCharResult,
+ padId: padId});
+}
+
+function _getNeededMigrationsArrayList(filter) {
+ var L = new ArrayList(_padMap().keySet());
+ for(var i=L.size()-1; i>=0; i--) {
+ if (! filter(String(L.get(i)))) {
+ L.remove(i);
+ }
+ }
+ return L;
+}
+
+function runBackgroundMigration(residue, modulus, runnerId) {
+ var L = _getNeededMigrationsArrayList(function(padId) {
+ return (padId.charCodeAt(0) % modulus) == residue;
+ });
+ Collections.shuffle(L);
+
+ var totalPads = L.size();
+ for(var i=0;i<totalPads;i++) {
+ if (! appjet.cache.pad_migrations.doingBackgroundLiveMigrations) {
+ break;
+ }
+ var padId = L.get(i);
+ var result = "FAIL";
+ var t1 = System.currentTimeMillis();
+ try {
+ if (ensureMigrated(padId, true)) {
+ result = " OK "; // migrated successfully
+ }
+ else {
+ result = " -- "; // no migration needed after all
+ }
+ }
+ catch (e) {
+ // e just says "migration failed", but presumably
+ // inTransaction() printed a stack trace.
+ // result == "FAIL", do nothing.
+ }
+ var t2 = System.currentTimeMillis();
+ _logPadMigration(runnerId, i, totalPads, t2 - t1, result, padId);
+ }
+}
diff --git a/etherpad/src/etherpad/pad/pad_security.js b/etherpad/src/etherpad/pad/pad_security.js
new file mode 100644
index 0000000..0ff8783
--- /dev/null
+++ b/etherpad/src/etherpad/pad/pad_security.js
@@ -0,0 +1,237 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.sessions.getSession");
+import("etherpad.sessions");
+
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_utils.isProDomainRequest");
+import("etherpad.pad.noprowatcher");
+
+//--------------------------------------------------------------------------------
+// granting session permanent access to pads (for the session)
+//--------------------------------------------------------------------------------
+
+function _grantSessionAccessTo(globalPadId) {
+ var userId = padusers.getUserId();
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ c[userId] = true;
+ });
+}
+
+function _doesSessionHaveAccessTo(globalPadId) {
+ var userId = padusers.getUserId();
+ return syncedWithCache("pad-auth."+globalPadId, function(c) {
+ return c[userId];
+ });
+}
+
+function revokePadUserAccess(globalPadId, userId) {
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ delete c[userId];
+ });
+}
+
+function revokeAllPadAccess(globalPadId) {
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ for (k in c) {
+ delete c[k];
+ }
+ });
+}
+
+//--------------------------------------------------------------------------------
+// knock/answer
+//--------------------------------------------------------------------------------
+
+function clearKnockStatus(userId, globalPadId) {
+ syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ delete c[userId];
+ });
+}
+
+// called by collab_server when accountholders approve or deny
+function answerKnock(userId, globalPadId, status) {
+ // status is either "approved" or "denied"
+ syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ // If two account-holders respond to the knock, keep the first one.
+ if (!c[userId]) {
+ c[userId] = status;
+ }
+ });
+}
+
+// returns "approved", "denied", or undefined
+function getKnockAnswer(userId, globalPadId) {
+ return syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ return c[userId];
+ });
+}
+
+//--------------------------------------------------------------------------------
+// main entrypoint called for every accessPad()
+//--------------------------------------------------------------------------------
+
+var _insideCheckAccessControl = false;
+
+function checkAccessControl(globalPadId, rwMode) {
+ if (!request.isDefined) {
+ return; // TODO: is this the right thing to do here?
+ // Empirical evidence indicates request.isDefined during comet requests,
+ // but not during tasks, which is the behavior we want.
+ }
+
+ if (_insideCheckAccessControl) {
+ // checkAccessControl is always allowed to access pads itself
+ return;
+ }
+ if (isProDomainRequest() && (request.path == "/ep/account/guest-knock")) {
+ return;
+ }
+ if (!isProDomainRequest() && (request.path == "/ep/admin/padinspector")) {
+ return;
+ }
+ if (isProDomainRequest() && (request.path == "/ep/padlist/all-pads.zip")) {
+ return;
+ }
+ try {
+ _insideCheckAccessControl = true;
+
+ if (!padutils.isProPadId(globalPadId)) {
+ // no access control on non-pro pads yet.
+ return;
+ }
+
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+ if (_doesSessionHaveAccessTo(globalPadId)) {
+ return;
+ }
+ _checkDomainSecurity(globalPadId);
+ _checkGuestSecurity(globalPadId);
+ _checkPasswordSecurity(globalPadId);
+
+ // remember that this user has access
+ _grantSessionAccessTo(globalPadId);
+ }
+ finally {
+ // this always runs, even on error or stop
+ _insideCheckAccessControl = false;
+ }
+}
+
+function _checkDomainSecurity(globalPadId) {
+ var padDomainId = padutils.getDomainId(globalPadId);
+ if (!padDomainId) {
+ return; // global pad
+ }
+ if (pro_utils.isProDomainRequest()) {
+ var requestDomainId = domains.getRequestDomainId();
+ if (requestDomainId != padDomainId) {
+ throw Error("Request cross-domain pad access not allowed.");
+ }
+ }
+}
+
+function _checkGuestSecurity(globalPadId) {
+ if (!getSession().guestPadAccess) {
+ getSession().guestPadAccess = {};
+ }
+
+ var padDomainId = padutils.getDomainId(globalPadId);
+ var isAccountHolder = pro_accounts.isAccountSignedIn();
+ if (isAccountHolder) {
+ if (getSessionProAccount().domainId != padDomainId) {
+ throw Error("Account cross-domain pad access not allowed.");
+ }
+ return; // OK
+ }
+
+ // Not an account holder ==> Guest
+
+ // returns either "allow", "ask", or "deny"
+ var guestPolicy = model.accessPadGlobal(globalPadId, function(p) {
+ if (!p.exists()) {
+ return "deny";
+ } else {
+ return p.getGuestPolicy();
+ }
+ });
+
+ var numProUsers = model.accessPadGlobal(globalPadId, function(pad) {
+ return noprowatcher.getNumProUsers(pad);
+ });
+
+ if (guestPolicy == "allow") {
+ return;
+ }
+ if (guestPolicy == "deny") {
+ pro_accounts.requireAccount("Guests are not allowed to join that pad. Please sign in.");
+ }
+ if (guestPolicy == "ask") {
+ if (numProUsers < 1) {
+ pro_accounts.requireAccount("This pad's security policy does not allow guests to join unless an account-holder is connected to the pad.");
+ }
+ var userId = padusers.getUserId();
+
+ // one of {"approved", "denied", undefined}
+ var knockAnswer = getKnockAnswer(userId, globalPadId);
+ if (knockAnswer == "approved") {
+ return;
+ } else {
+ var localPadId = padutils.globalToLocalId(globalPadId);
+ response.redirect('/ep/account/guest-sign-in?padId='+encodeURIComponent(localPadId));
+ }
+ }
+}
+
+function _checkPasswordSecurity(globalPadId) {
+ if (!getSession().padPasswordAuth) {
+ getSession().padPasswordAuth = {};
+ }
+ if (getSession().padPasswordAuth[globalPadId] == true) {
+ return;
+ }
+ var domainId = padutils.getDomainId(globalPadId);
+ var localPadId = globalPadId.split("$")[1];
+
+ if (stringutils.startsWith(request.path, "/ep/admin/recover-padtext")) {
+ return;
+ }
+
+ var p = pro_padmeta.accessProPad(globalPadId, function(propad) {
+ if (propad.exists()) {
+ return propad.getPassword();
+ } else {
+ return null;
+ }
+ });
+ if (p) {
+ response.redirect('/ep/pad/auth/'+localPadId+'?cont='+encodeURIComponent(request.url));
+ }
+}
+
diff --git a/etherpad/src/etherpad/pad/padevents.js b/etherpad/src/etherpad/pad/padevents.js
new file mode 100644
index 0000000..52b303c
--- /dev/null
+++ b/etherpad/src/etherpad/pad/padevents.js
@@ -0,0 +1,170 @@
+/**
+ * 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.
+ */
+
+// src/etherpad/events.js
+
+import("etherpad.licensing");
+import("etherpad.log");
+import("etherpad.pad.chatarchive");
+import("etherpad.pad.activepads");
+import("etherpad.pad.padutils");
+import("etherpad.sessions");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pad.padusers");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.noprowatcher");
+import("etherpad.collab.collab_server");
+jimport("java.lang.System.out.println");
+
+function onNewPad(pad) {
+ log.custom("padevents", {
+ type: "newpad",
+ padId: pad.getId()
+ });
+ pro_pad_db.onCreatePad(pad);
+}
+
+function onDestroyPad(pad) {
+ log.custom("padevents", {
+ type: "destroypad",
+ padId: pad.getId()
+ });
+ pro_pad_db.onDestroyPad(pad);
+}
+
+function onUserJoin(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ var name = userInfo.name || "unnamed";
+ log.custom("padevents", {
+ type: "userjoin",
+ padId: pad.getId(),
+ username: name,
+ ip: userInfo.ip,
+ userId: userInfo.userId
+ });
+ activepads.touch(pad.getId());
+ licensing.onUserJoin(userInfo);
+ log.onUserJoin(userInfo.userId);
+ padusers.notifyActive();
+ noprowatcher.onUserJoin(pad, userInfo);
+
+ });
+}
+
+function onUserLeave(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ var name = userInfo.name || "unnamed";
+ log.custom("padevents", {
+ type: "userleave",
+ padId: pad.getId(),
+ username: name,
+ ip: userInfo.ip,
+ userId: userInfo.userId
+ });
+ activepads.touch(pad.getId());
+ licensing.onUserLeave(userInfo);
+ noprowatcher.onUserLeave(pad, userInfo);
+
+ });
+}
+
+function onUserInfoChange(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ activepads.touch(pad.getId());
+
+ });
+}
+
+function onClientMessage(pad, senderUserInfo, msg) {
+ var padId = pad.getId();
+ activepads.touch(padId);
+
+ if (msg.type == "chat") {
+
+ chatarchive.onChatMessage(pad, senderUserInfo, msg);
+
+ var name = "unnamed";
+ if (senderUserInfo.name) {
+ name = senderUserInfo.name;
+ }
+
+ log.custom("chat", {
+ padId: padId,
+ userId: senderUserInfo.userId,
+ username: name,
+ text: msg.lineText
+ });
+ }
+ else if (msg.type == "padtitle") {
+ if (msg.title && padutils.isProPadId(pad.getId())) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ propad.setTitle(String(msg.title).substring(0, 80));
+ });
+ }
+ }
+ else if (msg.type == "padpassword") {
+ if (padutils.isProPadId(pad.getId())) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ propad.setPassword(msg.password || null);
+ });
+ }
+ }
+ else if (msg.type == "padoptions") {
+ // options object is a full set of options or just
+ // some options to change
+ var opts = msg.options;
+ var padOptions = pad.getPadOptionsObj();
+ if (opts.view) {
+ if (! padOptions.view) {
+ padOptions.view = {};
+ }
+ for(var k in opts.view) {
+ padOptions.view[k] = opts.view[k];
+ }
+ }
+ if (opts.guestPolicy) {
+ padOptions.guestPolicy = opts.guestPolicy;
+ if (opts.guestPolicy == 'deny') {
+ // boot guests!
+ collab_server.bootUsersFromPad(pad, "unauth", function(userInfo) {
+ return padusers.isGuest(userInfo.userId); }).forEach(function(userInfo) {
+ pad_security.revokePadUserAccess(padId, userInfo.userId); });
+ }
+ }
+ }
+ else if (msg.type == "guestanswer") {
+ if ((! msg.authId) || padusers.isGuest(msg.authId)) {
+ // not a pro user, forbid.
+ }
+ else {
+ pad_security.answerKnock(msg.guestId, padId, msg.answer);
+ }
+ }
+}
+
+function onEditPad(pad, authorId) {
+ log.callCatchingExceptions(function() {
+
+ pro_pad_db.onEditPad(pad, authorId);
+
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/pad/padusers.js b/etherpad/src/etherpad/pad/padusers.js
new file mode 100644
index 0000000..f04f0eb
--- /dev/null
+++ b/etherpad/src/etherpad/pad/padusers.js
@@ -0,0 +1,397 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("fastJSON");
+import("stringutils");
+import("jsutils.eachProperty");
+import("sync");
+import("etherpad.sessions");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("stringutils.randomHash");
+
+var _table = cachedSqlTable('pad_guests', 'pad_guests',
+ ['id', 'privateKey', 'userId'], processGuestRow);
+function processGuestRow(row) {
+ row.data = fastJSON.parse(row.data);
+}
+
+function notifySignIn() {
+ /*if (pro_accounts.isAccountSignedIn()) {
+ var proId = getUserId();
+ var guestId = _getGuestUserId();
+
+ var guestUser = _getGuestByKey('userId', guestId);
+ if (guestUser) {
+ var mods = {};
+ mods.data = guestUser.data;
+ // associate guest with proId
+ mods.data.replacement = proId;
+ // de-associate ET cookie with guest, otherwise
+ // the ET cookie would provide a semi-permanent way
+ // to effect changes under the pro account's name!
+ mods.privateKey = "replaced$"+_randomString(20);
+ _updateGuest('userId', guestId, mods);
+ }
+ }*/
+}
+
+function notifyActive() {
+ if (isGuest(getUserId())) {
+ _updateGuest('userId', getUserId(), {});
+ }
+}
+
+function notifyUserData(userData) {
+ var uid = getUserId();
+ if (isGuest(uid)) {
+ var data = _getGuestByKey('userId', uid).data;
+ if (userData.name) {
+ data.name = userData.name;
+ }
+ _updateGuest('userId', uid, {data: data});
+ }
+}
+
+function getUserId() {
+ if (pro_accounts.isAccountSignedIn()) {
+ return "p."+(getSessionProAccount().id);
+ }
+ else {
+ return getGuestUserId();
+ }
+}
+
+function getUserName() {
+ var uid = getUserId();
+ if (isGuest(uid)) {
+ var fromSession = sessions.getSession().guestDisplayName;
+ return fromSession || _getGuestByKey('userId', uid).data.name || null;
+ }
+ else {
+ return getSessionProAccount().fullName;
+ }
+}
+
+function getAccountIdForProAuthor(uid) {
+ if (uid.indexOf("p.") == 0) {
+ return Number(uid.substring(2));
+ }
+ else {
+ return -1;
+ }
+}
+
+function getNameForUserId(uid) {
+ if (isGuest(uid)) {
+ return _getGuestByKey('userId', uid).data.name || null;
+ }
+ else {
+ var accountNum = getAccountIdForProAuthor(uid);
+ if (accountNum < 0) {
+ return null;
+ }
+ else {
+ return pro_accounts.getAccountById(accountNum).fullName;
+ }
+ }
+}
+
+function isGuest(userId) {
+ return /^g/.test(userId);
+}
+
+function getGuestUserId() {
+ // cache the userId in the requestCache,
+ // for efficiency and consistency
+ var c = appjet.requestCache;
+ if (c.padGuestUserId === undefined) {
+ c.padGuestUserId = _computeGuestUserId();
+ }
+ return c.padGuestUserId;
+}
+
+function _getGuestTrackerId() {
+ // get ET cookie
+ var tid = sessions.getTrackingId();
+ if (tid == '-') {
+ // no tracking cookie? not a normal request?
+ return null;
+ }
+
+ // get domain ID
+ var domain = "-";
+ if (pro_utils.isProDomainRequest()) {
+ // e.g. "3"
+ domain = String(domains.getRequestDomainId());
+ }
+
+ // combine them
+ return domain+"$"+tid;
+}
+
+function _insertGuest(obj) {
+ // only requires 'userId' in obj
+
+ obj.createdDate = new Date;
+ obj.lastActiveDate = new Date;
+ if (! obj.data) {
+ obj.data = {};
+ }
+ if ((typeof obj.data) == "object") {
+ obj.data = fastJSON.stringify(obj.data);
+ }
+ if (! obj.privateKey) {
+ // private keys must be unique
+ obj.privateKey = "notracker$"+_randomString(20);
+ }
+
+ return _table.insert(obj);
+}
+
+function _getGuestByKey(keyColumn, value) {
+ return _table.getByKey(keyColumn, value);
+}
+
+function _updateGuest(keyColumn, value, obj) {
+ var obj2 = {};
+ eachProperty(obj, function(k,v) {
+ if (k == "data" && (typeof v) == "object") {
+ obj2.data = fastJSON.stringify(v);
+ }
+ else {
+ obj2[k] = v;
+ }
+ });
+
+ obj2.lastActiveDate = new Date;
+
+ _table.updateByKey(keyColumn, value, obj2);
+}
+
+function _newGuestUserId() {
+ return "g."+_randomString(16);
+}
+
+function _computeGuestUserId() {
+ // always returns some userId
+
+ var privateKey = _getGuestTrackerId();
+
+ if (! privateKey) {
+ // no tracking cookie, pretend there is one
+ privateKey = randomHash(16);
+ }
+
+ var userFromTracker = _table.getByKey('privateKey', privateKey);
+ if (userFromTracker) {
+ // we know this guy
+ return userFromTracker.userId;
+ }
+
+ // generate userId
+ var userId = _newGuestUserId();
+ var guest = {userId:userId, privateKey:privateKey};
+ var data = {};
+ guest.data = data;
+
+ var prefsCookieData = _getPrefsCookieData();
+ if (prefsCookieData) {
+ // found an old prefs cookie with an old userId
+ var oldUserId = prefsCookieData.userId;
+ // take the name and preferences
+ if ('name' in prefsCookieData) {
+ data.name = prefsCookieData.name;
+ }
+ /*['fullWidth','viewZoom'].forEach(function(pref) {
+ if (pref in prefsCookieData) {
+ data.prefs[pref] = prefsCookieData[pref];
+ }
+ });*/
+ }
+
+ _insertGuest(guest);
+ return userId;
+}
+
+function _getPrefsCookieData() {
+ // get userId from old prefs cookie if possible,
+ // but don't allow modern usernames
+
+ var prefsCookie = request.cookies['prefs'];
+ if (! prefsCookie) {
+ return null;
+ }
+ if (prefsCookie.charAt(0) != '%') {
+ return null;
+ }
+ try {
+ var cookieData = fastJSON.parse(unescape(prefsCookie));
+ // require one to three digits followed by dot at beginning of userId
+ if (/^[0-9]{1,3}\./.test(String(cookieData.userId))) {
+ return cookieData;
+ }
+ }
+ catch (e) {
+ return null;
+ }
+
+ return null;
+}
+
+function _randomString(len) {
+ // use only numbers and lowercase letters
+ var pieces = [];
+ for(var i=0;i<len;i++) {
+ pieces.push(Math.floor(Math.random()*36).toString(36).slice(-1));
+ }
+ return pieces.join('');
+}
+
+
+function cachedSqlTable(cacheName, tableName, keyColumns, processFetched) {
+ // Keeps a cache of sqlobj rows for the case where
+ // you want to select one row at a time by a single column
+ // at a time, taken from some set of key columns.
+ // The cache maps (keyColumn, value), e.g. ("id", 4) or
+ // ("secondaryKey", "foo123"), to an object, and each
+ // object is either present for all keyColumns
+ // (e.g. "id", "secondaryKey") or none.
+
+ if ((typeof keyColumns) == "string") {
+ keyColumns = [keyColumns];
+ }
+ processFetched = processFetched || (function(o) {});
+
+ function getCache() {
+ // this function is normally fast, only slow when cache
+ // needs to be created for the first time
+ var cache = appjet.cache[cacheName];
+ if (cache) {
+ return cache;
+ }
+ else {
+ // initialize in a synchronized block (double-checked locking);
+ // uses same lock as cache_utils.syncedWithCache would use.
+ sync.doWithStringLock("cache/"+cacheName, function() {
+ if (! appjet.cache[cacheName]) {
+ // values expire after 10 minutes
+ appjet.cache[cacheName] =
+ new net.appjet.common.util.ExpiringMapping(10*60*1000);
+ }
+ });
+ return appjet.cache[cacheName];
+ }
+ }
+
+ function cacheKey(keyColumn, value) {
+ // e.g. "id$4"
+ return keyColumn+"$"+String(value);
+ }
+
+ function getFromCache(keyColumn, value) {
+ return getCache().get(cacheKey(keyColumn, value));
+ }
+ function putInCache(obj) {
+ var cache = getCache();
+ // put in cache, keyed on all keyColumns we care about
+ keyColumns.forEach(function(keyColumn) {
+ cache.put(cacheKey(keyColumn, obj[keyColumn]), obj);
+ });
+ }
+ function touchInCache(obj) {
+ var cache = getCache();
+ keyColumns.forEach(function(keyColumn) {
+ cache.touch(cacheKey(keyColumn, obj[keyColumn]));
+ });
+ }
+ function removeObjFromCache(obj) {
+ var cache = getCache();
+ keyColumns.forEach(function(keyColumn) {
+ cache.remove(cacheKey(keyColumn, obj[keyColumn]));
+ });
+ }
+ function removeFromCache(keyColumn, value) {
+ var cached = getFromCache(keyColumn, value);
+ if (cached) {
+ removeObjFromCache(cached);
+ }
+ }
+
+ var self = {
+ clearCache: function() {
+ getCache().clear();
+ },
+ getByKey: function(keyColumn, value) {
+ // get cached object, if any
+ var cached = getFromCache(keyColumn, value);
+ if (! cached) {
+ // nothing in cache for this query, fetch from SQL
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ var fetched = sqlobj.selectSingle(tableName, keyToValue);
+ if (fetched) {
+ processFetched(fetched);
+ // fetched something, stick it in the cache
+ putInCache(fetched);
+ }
+ return fetched;
+ }
+ else {
+ // touch cached object and return
+ touchInCache(cached);
+ return cached;
+ }
+ },
+ updateByKey: function(keyColumn, value, obj) {
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ sqlobj.updateSingle(tableName, keyToValue, obj);
+ // remove old object from caches but
+ // don't put obj in cache, because it
+ // is likely a partial object
+ removeFromCache(keyColumn, value);
+ },
+ insert: function(obj) {
+ var returnVal = sqlobj.insert(tableName, obj);
+ // remove old object from caches but
+ // don't put obj in the cache; it doesn't
+ // have all values, e.g. for auto-generated ids
+ removeObjFromCache(obj);
+ return returnVal;
+ },
+ deleteByKey: function(keyColumn, value) {
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ sqlobj.deleteRows(tableName, keyToValue);
+ removeFromCache(keyColumn, value);
+ }
+ };
+ return self;
+}
+
+function _getClientIp() {
+ return (request.isDefined && request.clientIp) || '';
+}
+
+function getUserIdCreatedDate(userId) {
+ var record = sqlobj.selectSingle('pad_cookie_userids', {id: userId});
+ if (! record) { return; } // hm. weird case.
+ return record.createdDate;
+}
diff --git a/etherpad/src/etherpad/pad/padutils.js b/etherpad/src/etherpad/pad/padutils.js
new file mode 100644
index 0000000..b53de11
--- /dev/null
+++ b/etherpad/src/etherpad/pad/padutils.js
@@ -0,0 +1,191 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("stringutils");
+
+import("etherpad.control.pro.account_control");
+
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pad.model");
+import("etherpad.sessions.getSession");
+import("etherpad.helpers");
+
+jimport("java.lang.System.out.println");
+
+
+function setCurrentPad(p) {
+ appjet.context.attributes().update("currentPadId", p);
+}
+
+function clearCurrentPad() {
+ appjet.context.attributes()['$minus$eq']("currentPadId");
+}
+
+function getCurrentPad() {
+ var padOpt = appjet.context.attributes().get("currentPadId");
+ if (padOpt.isEmpty()) return null;
+ return padOpt.get();
+}
+
+function _parseCookie(text) {
+ try {
+ var cookieData = fastJSON.parse(unescape(text));
+ return cookieData;
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function getPrefsCookieData() {
+ var prefsCookie = request.cookies['prefs'];
+ if (!prefsCookie) {
+ return null;
+ }
+
+ return _parseCookie(prefsCookie);
+}
+
+function getPrefsCookieUserId() {
+ var cookieData = getPrefsCookieData();
+ if (! cookieData) {
+ return null;
+ }
+ return cookieData.userId || null;
+}
+
+/**
+ * Not valid to call this function outisde a HTTP request.
+ */
+function accessPadLocal(localPadId, fn, rwMode) {
+ if (!request.isDefined) {
+ throw Error("accessPadLocal() cannot run outside an HTTP request.");
+ }
+ var globalPadId = getGlobalPadId(localPadId);
+ var fnwrap = function(pad) {
+ pad.getLocalId = function() {
+ return getLocalPadId(pad);
+ };
+ return fn(pad);
+ }
+ return model.accessPadGlobal(globalPadId, fnwrap, rwMode);
+}
+
+/**
+ * Not valid to call this function outisde a HTTP request.
+ */
+function getGlobalPadId(localPadId) {
+ if (!request.isDefined) {
+ throw Error("getGlobalPadId() cannot run outside an HTTP request.");
+ }
+ if (pro_utils.isProDomainRequest()) {
+ return makeGlobalId(domains.getRequestDomainId(), localPadId);
+ } else {
+ // pad.spline.inf.fu-berlin.de pads
+ return localPadId;
+ }
+}
+
+function makeGlobalId(domainId, localPadId) {
+ return [domainId, localPadId].map(String).join('$');
+}
+
+function globalToLocalId(globalId) {
+ var parts = globalId.split('$');
+ if (parts.length == 1) {
+ return parts[0];
+ } else {
+ return parts[1];
+ }
+}
+
+function getLocalPadId(pad) {
+ var globalId = pad.getId();
+ return globalToLocalId(globalId);
+}
+
+function isProPadId(globalPadId) {
+ return (globalPadId.indexOf("$") > 0);
+}
+
+function isProPad(pad) {
+ return isProPadId(pad.getId());
+}
+
+function getDomainId(globalPadId) {
+ var parts = globalPadId.split("$");
+ if (parts.length < 2) {
+ return null;
+ } else {
+ return Number(parts[0]);
+ }
+}
+
+function makeValidLocalPadId(str) {
+ return str.replace(/[^a-zA-Z0-9\-]/g, '-');
+}
+
+function getProDisplayTitle(localPadId, title) {
+ if (title) {
+ return title;
+ }
+ if (stringutils.isNumeric(localPadId)) {
+ return ("Untitled "+localPadId);
+ } else {
+ return (localPadId);
+ }
+}
+
+
+function setOptsAndCookiePrefs(request) {
+ opts = {};
+ if (request.params.fullScreen) { // tokbox, embedding
+ opts.fullScreen = true;
+ }
+ if (request.params.tokbox) {
+ opts.tokbox = true;
+ }
+ if (request.params.sidebar) {
+ opts.sidebar = Boolean(Number(request.params.sidebar));
+ }
+ helpers.addClientVars({opts: opts});
+
+
+ var prefs = getPrefsCookieData();
+
+ var prefsToSet = {
+ fullWidth:false,
+ hideSidebar:false
+ };
+ if (prefs) {
+ prefsToSet.isFullWidth = !! prefs.fullWidth;
+ prefsToSet.hideSidebar = !! prefs.hideSidebar;
+ }
+ if (opts.fullScreen) {
+ prefsToSet.isFullWidth = true;
+ if (opts.tokbox) {
+ prefsToSet.hideSidebar = true;
+ }
+ }
+ if ('sidebar' in opts) {
+ prefsToSet.hideSidebar = ! opts.sidebar;
+ }
+ helpers.addClientVars({cookiePrefsToSet: prefsToSet});
+}
diff --git a/etherpad/src/etherpad/pad/revisions.js b/etherpad/src/etherpad/pad/revisions.js
new file mode 100644
index 0000000..c7c84e8
--- /dev/null
+++ b/etherpad/src/etherpad/pad/revisions.js
@@ -0,0 +1,103 @@
+/**
+ * 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.
+ */
+
+import("jsutils.cmp");
+import("stringutils");
+
+import("etherpad.utils.*");
+
+jimport("java.lang.System.out.println");
+
+/* revisionList is an array of revisionInfo structures.
+ *
+ * Each revisionInfo structure looks like:
+ *
+ * {
+ * timestamp: a number unix timestamp
+ * label: string
+ * savedBy: string of author name
+ * savedById: string id of the author
+ * revNum: revision number in the edit history
+ * id: the view id of the (formerly the id of the StorableObject)
+ * }
+ */
+
+/* returns array */
+function _getRevisionsArray(pad) {
+ var dataRoot = pad.getDataRoot();
+ if (!dataRoot.savedRevisions) {
+ dataRoot.savedRevisions = [];
+ }
+ dataRoot.savedRevisions.sort(function(a,b) {
+ return cmp(b.timestamp, a.timestamp);
+ });
+ return dataRoot.savedRevisions;
+}
+
+function _getPadRevisionById(pad, savedRevId) {
+ var revs = _getRevisionsArray(pad);
+ var rev;
+ for(var i=0;i<revs.length;i++) {
+ if (revs[i].id == savedRevId) {
+ rev = revs[i];
+ break;
+ }
+ }
+ return rev || null;
+}
+
+/*----------------------------------------------------------------*/
+/* public functions */
+/*----------------------------------------------------------------*/
+
+function getRevisionList(pad) {
+ return _getRevisionsArray(pad);
+}
+
+function saveNewRevision(pad, savedBy, savedById, revisionNumber, optIP, optTimestamp, optId) {
+ var revArray = _getRevisionsArray(pad);
+ var rev = {
+ timestamp: (optTimestamp || (+(new Date))),
+ label: null,
+ savedBy: savedBy,
+ savedById: savedById,
+ revNum: revisionNumber,
+ ip: (optIP || request.clientAddr),
+ id: (optId || stringutils.randomString(10)) // *probably* unique
+ };
+ revArray.push(rev);
+ rev.label = "Revision "+revArray.length;
+ return rev;
+}
+
+function setLabel(pad, savedRevId, userId, newLabel) {
+ var rev = _getPadRevisionById(pad, savedRevId);
+ if (!rev) {
+ throw new Error("revision does not exist: "+savedRevId);
+ }
+ /*if (rev.savedById != userId) {
+ throw new Error("cannot label someone else's revision.");
+ }
+ if (((+new Date) - rev.timestamp) > (24*60*60*1000)) {
+ throw new Error("revision is too old to label: "+savedRevId);
+ }*/
+ rev.label = newLabel;
+}
+
+function getStoredRevision(pad, savedRevId) {
+ return _getPadRevisionById(pad, savedRevId);
+}
+
diff --git a/etherpad/src/etherpad/pne/pne_utils.js b/etherpad/src/etherpad/pne/pne_utils.js
new file mode 100644
index 0000000..073ad2a
--- /dev/null
+++ b/etherpad/src/etherpad/pne/pne_utils.js
@@ -0,0 +1,149 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("stringutils.md5");
+import("sqlbase.persistent_vars");
+
+import("etherpad.licensing");
+
+jimport("java.lang.System.out.println");
+jimport("java.lang.System");
+
+
+function isPNE() {
+ if (appjet.cache.fakePNE || appjet.config['etherpad.fakePNE']) {
+ return true;
+ }
+ if (getVersionString()) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Versioning scheme: we basically just use the apache scheme of MAJOR.MINOR.PATCH:
+ *
+ * Versions are denoted using a standard triplet of integers: MAJOR.MINOR.PATCH. The
+ * basic intent is that MAJOR versions are incompatible, large-scale upgrades of the API.
+ * MINOR versions retain source and binary compatibility with older minor versions, and
+ * changes in the PATCH level are perfectly compatible, forwards and backwards.
+ */
+
+function getVersionString() {
+ return appjet.config['etherpad.pneVersion'];
+}
+
+function parseVersionString(x) {
+ var parts = x.split('.');
+ return {
+ major: Number(parts[0] || 0),
+ minor: Number(parts[1] || 0),
+ patch: Number(parts[2] || 0)
+ };
+}
+
+/* returns {major: int, minor: int, patch: int} */
+function getVersionNumbers() {
+ return parseVersionString(getVersionString());
+}
+
+function checkDbVersionUpgrade() {
+ var dbVersionString = persistent_vars.get("db_pne_version");
+ var runningVersionString = getVersionString();
+
+ if (!dbVersionString) {
+ println("Upgrading to Private Network Edition, version: "+runningVersionString);
+ return;
+ }
+
+ var dbVersion = parseVersionString(dbVersionString);
+ var runningVersion = getVersionNumbers();
+ var trueRegex = /\s*true\s*/i;
+ var force = trueRegex.test(appjet.config['etherpad.forceDbUpgrade']);
+
+ if (!force && (runningVersion.major != dbVersion.major)) {
+ println("Error: you are attempting to update an EtherPad["+dbVersionString+
+ "] database to version ["+runningVersionString+"]. This is not possible.");
+ println("Exiting...");
+ System.exit(1);
+ }
+ if (!force && (runningVersion.minor < dbVersion.minor)) {
+ println("Error: your etherpad database is at a newer version ["+dbVersionString+"] than"+
+ " the current running etherpad ["+runningVersionString+"]. Please upgrade to the "+
+ " latest version.");
+ println("Exiting...");
+ System.exit(1);
+ }
+ if (!force && (runningVersion.minor > (dbVersion.minor + 1))) {
+ println("\n\nWARNING: you are attempting to upgrade from version "+dbVersionString+" to version "+
+ runningVersionString+". It is recommended that you upgrade one minor version at a time."+
+ " (The \"minor\" version number is the second number separated by dots. For example,"+
+ " if you are running version 1.2, it is recommended that you upgrade to 1.3 and then 1.4 "+
+ " instead of going directly from 1.2 to 1.4.");
+ println("\n\nIf you really want to do this, you can force us to attempt the upgrade with "+
+ " the --etherpad.forceDbUpgrade=true flag.");
+ println("\n\nExiting...");
+ System.exit(1);
+ }
+ if (runningVersion.minor > dbVersion.minor) {
+ println("Upgrading database to version "+runningVersionString);
+ }
+}
+
+function saveDbVersion() {
+ var dbVersionString = persistent_vars.get("db_pne_version");
+ if (getVersionString() != dbVersionString) {
+ persistent_vars.put('db_pne_version', getVersionString());
+ println("Upgraded Private Network Edition version to ["+getVersionString()+"]");
+ }
+}
+
+// These are a list of some of the config vars documented in the PNE manual. They are here
+// temporarily, until we move them to the PNE config UI.
+
+var _eepneAllowedConfigVars = [
+ 'configFile',
+ 'etherpad.soffice',
+ 'etherpad.useMySQL',
+ 'etherpad.SQL_JDBC_DRIVER',
+ 'etherpad.SQL_JDBC_URL',
+ 'etherpad.SQL_PASSWORD',
+ 'etherpad.SQL_USERNAME',
+ 'etherpad.adminPass',
+ 'etherpad.licenseKey',
+ 'listen',
+ 'listenSecure',
+ 'smtpPass',
+ 'smtpServer',
+ 'smtpUser',
+ 'sslKeyPassword',
+ 'sslKeyStore'
+];
+
+function isServerLicensed() {
+ return true;
+}
+
+function enableTrackingAgain() {
+}
+
+function pneTrackerHtml() {
+ appjet.cache.noMorePneTracking = true;
+}
+
+
+
diff --git a/etherpad/src/etherpad/pro/domains.js b/etherpad/src/etherpad/pro/domains.js
new file mode 100644
index 0000000..e56a408
--- /dev/null
+++ b/etherpad/src/etherpad/pro/domains.js
@@ -0,0 +1,141 @@
+/**
+ * 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.
+ */
+
+// Library for managing subDomains
+
+import("jsutils.*");
+import("sqlbase.sqlobj");
+
+import("etherpad.pro.pro_utils");
+import("etherpad.pne.pne_utils");
+import("etherpad.licensing");
+
+jimport("java.lang.System.out.println");
+
+// reserved domains
+var reservedSubdomains = {
+ 'alpha': 1,
+ 'beta': 1,
+ 'blog': 1,
+ 'comet': 1,
+ 'diagnostic': 1,
+ 'forums': 1,
+ 'forumsdev': 1,
+ 'staging': 1,
+ 'web': 1,
+ 'www': 1
+};
+
+function _getCache() {
+ if (!appjet.cache.pro_domains) {
+ appjet.cache.pro_domains = {
+ records: {id: {}, subDomain: {}}
+ };
+ }
+ return appjet.cache.pro_domains;
+}
+
+function doesSubdomainExist(subDomain) {
+ if (reservedSubdomains[subDomain]) {
+ return true;
+ }
+ if (getDomainRecordFromSubdomain(subDomain) != null) {
+ return true;
+ }
+ return false;
+}
+
+function _updateCache(locator) {
+ var record = sqlobj.selectSingle('pro_domains', locator);
+ var recordCache = _getCache().records;
+
+ if (record) {
+ // update both maps: recordCache.id, recordCache.subDomain
+ keys(recordCache).forEach(function(key) {
+ recordCache[key][record[key]] = record;
+ });
+ } else {
+ // write false for whatever hit with this locator
+ keys(locator).forEach(function(key) {
+ recordCache[key][locator[key]] = false;
+ });
+ }
+}
+
+function getDomainRecord(domainId) {
+ if (!(domainId in _getCache().records.id)) {
+ _updateCache({id: domainId});
+ }
+ var record = _getCache().records.id[domainId];
+ return (record ? record : null);
+}
+
+function getDomainRecordFromSubdomain(subDomain) {
+ subDomain = subDomain.toLowerCase();
+ if (!(subDomain in _getCache().records.subDomain)) {
+ _updateCache({subDomain: subDomain});
+ }
+ var record = _getCache().records.subDomain[subDomain];
+ return (record ? record : null);
+}
+
+/** returns id of newly created subDomain */
+function createNewSubdomain(subDomain, orgName) {
+ var id = sqlobj.insert('pro_domains', {subDomain: subDomain, orgName: orgName});
+ _updateCache({id: id});
+ return id;
+}
+
+function getPrivateNetworkDomainId() {
+ var r = getDomainRecordFromSubdomain('<<private-network>>');
+ if (!r) {
+ throw Error("<<private-network>> does not exist in the domains table!");
+ }
+ return r.id;
+}
+
+/** returns null if not found. */
+function getRequestDomainRecord() {
+ if (pne_utils.isPNE()) {
+ var r = getDomainRecord(getPrivateNetworkDomainId());
+ if (appjet.cache.fakePNE) {
+ r.orgName = "fake";
+ } else {
+ var licenseInfo = licensing.getLicense();
+ if (licenseInfo) {
+ r.orgName = licenseInfo.organizationName;
+ } else {
+ r.orgName = "Private Network Edition TRIAL";
+ }
+ }
+ return r;
+ } else {
+ var subDomain = pro_utils.getProRequestSubdomain();
+ var r = getDomainRecordFromSubdomain(subDomain);
+ return r;
+ }
+}
+
+/* throws exception if not pro domain request. */
+function getRequestDomainId() {
+ var r = getRequestDomainRecord();
+ if (!r) {
+ throw Error("Error getting request domain id.");
+ }
+ return r.id;
+}
+
+
diff --git a/etherpad/src/etherpad/pro/pro_account_auto_signin.js b/etherpad/src/etherpad/pro/pro_account_auto_signin.js
new file mode 100644
index 0000000..ebcd227
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_account_auto_signin.js
@@ -0,0 +1,101 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlobj");
+import("stringutils");
+
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.lang.System.out.println");
+
+var _COOKIE_NAME = "PUAS";
+
+function dmesg(m) {
+ if (false) {
+ println("[pro-account-auto-sign-in]: "+m);
+ }
+}
+
+function checkAutoSignin() {
+ dmesg("checking auto sign-in...");
+ if (pro_accounts.isAccountSignedIn()) {
+ dmesg("account already signed in...");
+ // don't mess with already signed-in account
+ return;
+ }
+ var cookie = request.cookies[_COOKIE_NAME];
+ if (!cookie) {
+ dmesg("no auto-sign-in cookie found...");
+ return;
+ }
+ var record = sqlobj.selectSingle('pro_accounts_auto_signin', {cookie: cookie}, {});
+ if (!record) {
+ return;
+ }
+
+ var now = +(new Date);
+ if (+record.expires < now) {
+ sqlobj.deleteRows('pro_accounts_auto_signin', {id: record.id});
+ response.deleteCookie(_COOKIE_NAME);
+ dmesg("deleted expired record...");
+ return;
+ }
+ // do auto-signin (bypasses normal security)
+ dmesg("Doing auto sign in...");
+ var account = pro_accounts.getAccountById(record.accountId);
+ pro_accounts.signInSession(account);
+ response.redirect('/ep/account/sign-in?cont='+encodeURIComponent(request.url));
+}
+
+function setAutoSigninCookie(rememberMe) {
+ if (!pro_accounts.isAccountSignedIn()) {
+ return; // only call this function after account is already signed in.
+ }
+
+ var accountId = getSessionProAccount().id;
+ // delete any existing auto-signins for this account.
+ sqlobj.deleteRows('pro_accounts_auto_signin', {accountId: accountId});
+
+ // set this insecure cookie just to indicate that account is auto-sign-in-able
+ response.setCookie({
+ name: "ASIE",
+ value: (rememberMe ? "T" : "F"),
+ path: "/",
+ domain: request.domain,
+ expires: new Date(32503708800000), // year 3000
+ });
+
+ if (!rememberMe) {
+ return;
+ }
+
+ var cookie = stringutils.randomHash(16);
+ var now = +(new Date);
+ var expires = new Date(now + 1000*60*60*24*30); // 30 days
+ //var expires = new Date(now + 1000 * 60 * 5); // 2 minutes
+
+ sqlobj.insert('pro_accounts_auto_signin', {cookie: cookie, accountId: accountId, expires: expires});
+ response.setCookie({
+ name: _COOKIE_NAME,
+ value: cookie,
+ path: "/ep/account/",
+ domain: request.domain,
+ expires: new Date(32503708800000), // year 3000
+ secure: true
+ });
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_accounts.js b/etherpad/src/etherpad/pro/pro_accounts.js
new file mode 100644
index 0000000..98df6bb
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_accounts.js
@@ -0,0 +1,592 @@
+/**
+ * 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.
+ */
+
+// library for pro accounts
+
+import("funhtml.*");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+import("email.sendEmail");
+import("cache_utils.syncedWithCache");
+import("stringutils.*");
+
+import("etherpad.globals.*");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.pro.domains");
+import("etherpad.control.pro.account_control");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_ldap_support.*");
+import("etherpad.pro.pro_quotas");
+import("etherpad.pad.padusers");
+import("etherpad.log");
+import("etherpad.billing.team_billing");
+
+import("process.*");
+import("fastJSON")
+
+jimport("org.mindrot.BCrypt");
+jimport("java.lang.System.out.println");
+
+function _dmesg(m) {
+ if (!isProduction()) {
+ println(m);
+ }
+}
+
+function _computePasswordHash(p) {
+ var pwh;
+ pwh = BCrypt.hashpw(p, BCrypt.gensalt(10));
+ return pwh;
+}
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro_accounts.'+name, fn);
+}
+
+//----------------------------------------------------------------
+// validation
+//----------------------------------------------------------------
+
+function validateEmail(email) {
+ if (!email) { return "Email is required."; }
+ if (!isValidEmail(email)) { return "\""+email+"\" does not look like a valid email address."; }
+ return null;
+}
+
+function validateFullName(name) {
+ if (!name) { return "Full name is required."; }
+ if (name.length < 2) { return "Full name must be at least 2 characters."; }
+ return null;
+}
+
+function validatePassword(p) {
+ if (!p) { return "Password is required."; }
+ if (p.length < 6) { return "Passwords must be at least 6 characters."; }
+ return null;
+}
+
+function validateEmailDomainPair(email, domainId) {
+ // TODO: make sure the same email address cannot exist more than once within
+ // the same domainid.
+}
+
+/* if domainId is null, then use domainId of current request. */
+function createNewAccount(domainId, fullName, email, password, isAdmin, skipValidation) {
+ if (!domainId) {
+ domainId = domains.getRequestDomainId();
+ }
+ if (!skipValidation) {
+ skipValidation = false;
+ }
+ email = trim(email);
+ isAdmin = !!isAdmin; // convert to bool
+
+ // validation
+ if (!skipValidation) {
+ var e;
+ e = validateEmail(email); if (e) { throw Error(e); }
+ e = validateFullName(fullName); if (e) { throw Error(e); }
+ e = validatePassword(password); if (e) { throw Error(e); }
+ }
+
+ // xss normalization
+ fullName = toHTML(fullName);
+
+ // make sure account does not already exist on this domain.
+ var ret = inTransaction(function() {
+ var existingAccount = getAccountByEmail(email, domainId);
+ if (existingAccount) {
+ throw Error("There is already an account with that email address.");
+ }
+ // No existing account. Proceed.
+ var now = new Date();
+ var account = {
+ domainId: domainId,
+ fullName: fullName,
+ email: email,
+ passwordHash: _computePasswordHash(password),
+ createdDate: now,
+ isAdmin: isAdmin
+ };
+ return sqlobj.insert('pro_accounts', account);
+ });
+
+ _withCache('does-domain-admin-exist', function(cache) {
+ delete cache[domainId];
+ });
+
+ pro_quotas.updateAccountUsageCount(domainId);
+ updateCachedActiveCount(domainId);
+
+ if (ret) {
+ log.custom('pro-accounts',
+ {type: "account-created",
+ accountId: ret,
+ domainId: domainId,
+ name: fullName,
+ email: email,
+ admin: isAdmin});
+ }
+
+ return ret;
+}
+
+function _checkAccess(account) {
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+ if (account.domainId != domains.getRequestDomainId()) {
+ throw Error("access denied");
+ }
+}
+
+function setPassword(account, newPass) {
+ _checkAccess(account);
+ var passHash = _computePasswordHash(newPass);
+ sqlobj.update('pro_accounts', {id: account.id}, {passwordHash: passHash});
+ markDirtySessionAccount(account.id);
+}
+
+function setTempPassword(account, tempPass) {
+ _checkAccess(account);
+ var tempPassHash = _computePasswordHash(tempPass);
+ sqlobj.update('pro_accounts', {id: account.id}, {tempPassHash: tempPassHash});
+ markDirtySessionAccount(account.id);
+}
+
+function setEmail(account, newEmail) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {email: newEmail});
+ markDirtySessionAccount(account.id);
+}
+
+function setFullName(account, newName) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {fullName: newName});
+ markDirtySessionAccount(account.id);
+}
+
+function setIsAdmin(account, newVal) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {isAdmin: newVal});
+ markDirtySessionAccount(account.id);
+}
+
+function setDeleted(account) {
+ _checkAccess(account);
+ if (!isNumeric(account.id)) {
+ throw new Error("Invalid account id: "+account.id);
+ }
+ sqlobj.update('pro_accounts', {id: account.id}, {isDeleted: true});
+ markDirtySessionAccount(account.id);
+ pro_quotas.updateAccountUsageCount(account.domainId);
+ updateCachedActiveCount(account.domainId);
+
+ log.custom('pro-accounts',
+ {type: "account-deleted",
+ accountId: account.id,
+ domainId: account.domainId,
+ name: account.fullName,
+ email: account.email,
+ admin: account.isAdmin,
+ createdDate: account.createdDate.getTime()});
+}
+
+//----------------------------------------------------------------
+
+function doesAdminExist() {
+ var domainId = domains.getRequestDomainId();
+ return _withCache('does-domain-admin-exist', function(cache) {
+ if (cache[domainId] === undefined) {
+ _dmesg("cache miss for doesAdminExist (domainId="+domainId+")");
+ var admins = sqlobj.selectMulti('pro_accounts', {domainId: domainId, isAdmin: true}, {});
+ cache[domainId] = (admins.length > 0);
+ }
+ return cache[domainId]
+ });
+}
+
+function attemptSingleSignOn() {
+ if(!appjet.config['etherpad.SSOScript']) return null;
+
+ // pass request.cookies to a small user script
+ var file = appjet.config['etherpad.SSOScript'];
+
+ var cmd = exec(file);
+
+ // note that this will block until script execution returns
+ var result = cmd.write(fastJSON.stringify(request.cookies)).result();
+ var val = false;
+
+ // we try to parse the result as a JSON string, if not, return null.
+ try {
+ if(!!(val=fastJSON.parse(result))) {
+ return val;
+ }
+ } catch(e) {}
+ return null;
+}
+
+function getSessionProAccount() {
+ if (sessions.isAnEtherpadAdmin()) {
+ return getEtherpadAdminAccount();
+ }
+ var account = getSession().proAccount;
+ if (!account) {
+ return null;
+ }
+ if (account.isDeleted) {
+ delete getSession().proAccount;
+ return null;
+ }
+ return account;
+}
+
+function isAccountSignedIn() {
+ if (getSessionProAccount()) {
+ return true;
+ } else {
+ // if the user is not signed in, check to see if he should be signed in
+ // by calling an external script.
+ if(appjet.config['etherpad.SSOScript']) {
+ var ssoResult = attemptSingleSignOn();
+ if(ssoResult && ('email' in ssoResult)) {
+ var user = getAccountByEmail(ssoResult['email']);
+ if (!user) {
+ var email = ssoResult['email'];
+ var pass = ssoResult['password'] || "";
+ var name = ssoResult['fullname'] || "unnamed";
+ createNewAccount(null, name, email, pass, false, true);
+ user = getAccountByEmail(email, null);
+ }
+
+ signInSession(user);
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+function isAdminSignedIn() {
+ return isAccountSignedIn() && getSessionProAccount().isAdmin;
+}
+
+function requireAccount(message) {
+ if ((request.path == "/ep/account/sign-in") ||
+ (request.path == "/ep/account/sign-out") ||
+ (request.path == "/ep/account/guest-sign-in") ||
+ (request.path == "/ep/account/guest-knock") ||
+ (request.path == "/ep/account/forgot-password")) {
+ return;
+ }
+
+ function checkSessionAccount() {
+ if (!getSessionProAccount()) {
+ if (message) {
+ account_control.setSigninNotice(message);
+ }
+ response.redirect('/ep/account/sign-in?cont='+encodeURIComponent(request.url));
+ }
+ }
+
+ checkSessionAccount();
+
+ if (getSessionProAccount().domainId != domains.getRequestDomainId()) {
+ // This should theoretically never happen unless the account is spoofing cookies / trying to
+ // hack the site.
+ pro_utils.renderFramedMessage("Permission denied.");
+ response.stop();
+ }
+ // update dirty session account if necessary
+ _withCache('dirty-session-accounts', function(cache) {
+ var uid = getSessionProAccount().id;
+ if (cache[uid]) {
+ reloadSessionAccountData(uid);
+ cache[uid] = false;
+ }
+ });
+
+ // need to check again in case dirty update caused account to be marked
+ // deleted.
+ checkSessionAccount();
+}
+
+function requireAdminAccount() {
+ requireAccount();
+ if (!getSessionProAccount().isAdmin) {
+ pro_utils.renderFramedMessage("Permission denied.");
+ response.stop();
+ }
+}
+
+/* returns undefined on success, error string otherise. */
+function authenticateSignIn(email, password) {
+ // blank passwords are not allowed to sign in.
+ if (password == "") return "Please provide a password.";
+
+ // If the email ends with our ldap suffix...
+ var isLdapSuffix = getLDAP() && getLDAP().isLDAPSuffix(email);
+
+ if(isLdapSuffix && !getLDAP()) {
+ return "LDAP not yet configured. Please contact your system admininstrator.";
+ }
+
+ // if there is an error in the LDAP configuration, return the error message
+ if(getLDAP() && getLDAP().error) {
+ return getLDAP().error + " Please contact your system administrator.";
+ }
+
+ if(isLdapSuffix && getLDAP()) {
+ var ldapuser = email.substr(0, email.indexOf(getLDAP().getLDAPSuffix()));
+ var ldapResult = getLDAP().login(ldapuser, password);
+
+ if (ldapResult.error == true) {
+ return ldapResult.message + "";
+ }
+
+ var accountRecord = getAccountByEmail(email, null);
+
+ // if this is the first time this user has logged in, create a user
+ // for him/her
+ if (!accountRecord) {
+ // password to store in database -- a blank password means the user
+ // cannot authenticate normally (e.g. must go through SSO or LDAP)
+ var ldapPass = "";
+
+ // create a new user (skipping validation of email/users/passes)
+ createNewAccount(null, ldapResult.getFullName(), email, ldapPass, false, true);
+ accountRecord = getAccountByEmail(email, null);
+ }
+
+ signInSession(accountRecord);
+ return undefined; // success
+ }
+
+ var accountRecord = getAccountByEmail(email, null);
+ if (!accountRecord) {
+ return "Account not found: "+email;
+ }
+
+ if (BCrypt.checkpw(password, accountRecord.passwordHash) != true) {
+ return "Incorrect password. Please try again.";
+ }
+
+ signInSession(accountRecord);
+
+ return undefined; // success
+}
+
+function signOut() {
+ delete getSession().proAccount;
+}
+
+function authenticateTempSignIn(uid, tempPass) {
+ var emsg = "That password reset link that is no longer valid.";
+
+ var account = getAccountById(uid);
+ if (!account) {
+ return emsg+" (Account not found.)";
+ }
+ if (account.domainId != domains.getRequestDomainId()) {
+ return emsg+" (Wrong domain.)";
+ }
+ if (!account.tempPassHash) {
+ return emsg+" (Expired.)";
+ }
+ if (BCrypt.checkpw(tempPass, account.tempPassHash) != true) {
+ return emsg+" (Bad temp pass.)";
+ }
+
+ signInSession(account);
+
+ getSession().accountMessage = "Please choose a new password";
+ getSession().changePass = true;
+
+ response.redirect("/ep/account/");
+}
+
+function signInSession(account) {
+ account.lastLoginDate = new Date();
+ account.tempPassHash = null;
+ sqlobj.updateSingle('pro_accounts', {id: account.id}, account);
+ reloadSessionAccountData(account.id);
+ padusers.notifySignIn();
+}
+
+function listAllDomainAccounts(domainId) {
+ if (domainId === undefined) {
+ domainId = domains.getRequestDomainId();
+ }
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false}, {});
+ return records;
+}
+
+function listAllDomainAdmins(domainId) {
+ if (domainId === undefined) {
+ domainId = domains.getRequestDomainId();
+ }
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false, isAdmin: true},
+ {});
+ return records;
+}
+
+function getActiveCount(domainId) {
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false}, {});
+ return records.length;
+}
+
+/* getAccountById works for deleted and non-deleted accounts.
+ * The assumption is that cases whewre you look up an account by ID, you
+ * want the account info even if the account has been deleted. For
+ * example, when asking who created a pad.
+ */
+function getAccountById(accountId) {
+ var r = sqlobj.selectSingle('pro_accounts', {id: accountId});
+ if (r) {
+ return r;
+ } else {
+ return undefined;
+ }
+}
+
+/* getting an account by email only returns the account if it is
+ * not deleted. The assumption is that when you look up an account by
+ * email address, you only want active accounts. Furthermore, some
+ * deleted accounts may match a given email, but only one non-deleted
+ * account should ever match a single (email,domainId) pair.
+ */
+function getAccountByEmail(email, domainId) {
+ if (!domainId) {
+ domainId = domains.getRequestDomainId();
+ }
+ var r = sqlobj.selectSingle('pro_accounts', {domainId: domainId, email: email, isDeleted: false});
+ if (r) {
+ return r;
+ } else {
+ return undefined;
+ }
+}
+
+function getFullNameById(id) {
+ if (!id) {
+ return null;
+ }
+
+ return _withCache('names-by-id', function(cache) {
+ if (cache[id] === undefined) {
+ _dmesg("cache miss for getFullNameById (accountId="+id+")");
+ var r = getAccountById(id);
+ if (r) {
+ cache[id] = r.fullName;
+ } else {
+ cache[id] = false;
+ }
+ }
+ if (cache[id]) {
+ return cache[id];
+ } else {
+ return null;
+ }
+ });
+}
+
+function getTempSigninUrl(account, tempPass) {
+ if(appjet.config.listenSecurePort != 0 || appjet.config.useHttpsUrls)
+ return [
+ 'https://', httpsHost(pro_utils.getFullProHost()), '/ep/account/sign-in?',
+ 'uid=', account.id, '&tp=', tempPass
+ ].join('');
+ else
+ return [
+ 'http://', httpHost(pro_utils.getFullProHost()), '/ep/account/sign-in?',
+ 'uid=', account.id, '&tp=', tempPass
+ ].join('');
+}
+
+
+// TODO: this session account object storage / dirty cache is a
+// ridiculous hack. What we should really do is have a caching/access
+// layer for accounts similar to accessPad() and accessProPadMeta(), and
+// have that abstraction take care of caching and marking accounts as
+// dirty. This can be incorporated into getSessionProAccount(), and we
+// should actually refactor that into accessSessionProAccount().
+
+/* will force session data for this account to be updated next time that
+ * account requests a page. */
+function markDirtySessionAccount(uid) {
+ var domainId = domains.getRequestDomainId();
+
+ _withCache('dirty-session-accounts', function(cache) {
+ cache[uid] = true;
+ });
+ _withCache('names-by-id', function(cache) {
+ delete cache[uid];
+ });
+ _withCache('does-domain-admin-exist', function(cache) {
+ delete cache[domainId];
+ });
+}
+
+function reloadSessionAccountData(uid) {
+ if (!uid) {
+ uid = getSessionProAccount().id;
+ }
+ getSession().proAccount = getAccountById(uid);
+}
+
+function getAllAccountsWithEmail(email) {
+ var accountRecords = sqlobj.selectMulti('pro_accounts', {email: email, isDeleted: false}, {});
+ return accountRecords;
+}
+
+function getEtherpadAdminAccount() {
+ return {
+ id: 0,
+ isAdmin: true,
+ fullName: "ETHERPAD ADMIN",
+ email: "support@pad.spline.inf.fu-berlin.de",
+ domainId: domains.getRequestDomainId(),
+ isDeleted: false
+ };
+}
+
+function getCachedActiveCount(domainId) {
+ return _withCache('user-counts.'+domainId, function(c) {
+ if (!c.count) {
+ c.count = getActiveCount(domainId);
+ }
+ return c.count;
+ });
+}
+
+function updateCachedActiveCount(domainId) {
+ _withCache('user-counts.'+domainId, function(c) {
+ c.count = getActiveCount(domainId);
+ });
+}
+
+
+
+
+
+
diff --git a/etherpad/src/etherpad/pro/pro_config.js b/etherpad/src/etherpad/pro/pro_config.js
new file mode 100644
index 0000000..d2d119f
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_config.js
@@ -0,0 +1,92 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_utils");
+
+function _guessSiteName() {
+ var x = request.host.split('.')[0];
+ x = (x.charAt(0).toUpperCase() + x.slice(1));
+ return x;
+}
+
+function _getDefaultConfig() {
+ return {
+ siteName: _guessSiteName(),
+ alwaysHttps: false,
+ defaultPadText: renderTemplateAsString("misc/pad_default.ejs")
+ };
+}
+
+// must be fast! gets called per request, on every request.
+function getConfig() {
+ if (!pro_utils.isProDomainRequest()) {
+ return null;
+ }
+
+ if (!appjet.cache.pro_config) {
+ appjet.cache.pro_config = {};
+ }
+
+ var domainId = domains.getRequestDomainId();
+ if (!appjet.cache.pro_config[domainId]) {
+ reloadConfig();
+ }
+
+ return appjet.cache.pro_config[domainId];
+}
+
+function reloadConfig() {
+ var domainId = domains.getRequestDomainId();
+ var config = _getDefaultConfig();
+ var records = sqlobj.selectMulti('pro_config', {domainId: domainId}, {});
+
+ records.forEach(function(r) {
+ var name = r.name;
+ var val = fastJSON.parse(r.jsonVal).x;
+ config[name] = val;
+ });
+
+ if (!appjet.cache.pro_config) {
+ appjet.cache.pro_config = {};
+ }
+
+ appjet.cache.pro_config[domainId] = config;
+}
+
+function setConfigVal(name, val) {
+ var domainId = domains.getRequestDomainId();
+ var jsonVal = fastJSON.stringify({x: val});
+
+ var r = sqlobj.selectSingle('pro_config', {domainId: domainId, name: name});
+ if (!r) {
+ sqlobj.insert('pro_config',
+ {domainId: domainId, name: name, jsonVal: jsonVal});
+ } else {
+ sqlobj.update('pro_config',
+ {name: name, domainId: domainId},
+ {jsonVal: jsonVal});
+ }
+
+ reloadConfig();
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_ldap_support.js b/etherpad/src/etherpad/pro/pro_ldap_support.js
new file mode 100644
index 0000000..a657af1
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_ldap_support.js
@@ -0,0 +1,217 @@
+import("fastJSON");
+
+jimport("net.appjet.common.util.BetterFile")
+
+jimport("java.lang.System.out.println");
+jimport("javax.naming.directory.DirContext");
+jimport("javax.naming.directory.SearchControls");
+jimport("javax.naming.directory.InitialDirContext");
+jimport("javax.naming.directory.SearchResult");
+jimport("javax.naming.NamingEnumeration");
+jimport("javax.naming.Context");
+jimport("java.util.Hashtable");
+
+function LDAP(config, errortext) {
+ if(!config)
+ this.error = errortext;
+ else
+ this.error = false;
+
+ this.ldapConfig = config;
+}
+
+function _dmesg(m) {
+ // if (!isProduction()) {
+ println(new String(m));
+ // }
+}
+
+/**
+ * an ldap result object
+ *
+ * will either have error = true, with a corrisponding error message,
+ * or will have error = false, with a corrisponding results object message
+ */
+function LDAPResult(msg, error, ldap) {
+ if(!ldap) ldap = getLDAP();
+ if(!error) error = false;
+ this.message = msg;
+ this.ldap = ldap;
+ this.error = error;
+}
+
+/**
+ * returns the full name attribute, as specified by the 'nameAttribute' config
+ * value.
+ */
+LDAPResult.prototype.getFullName = function() {
+ return this.message[this.ldap.ldapConfig['nameAttribute']][0];
+}
+
+/**
+ * Handy function for creating an LDAPResult object
+ */
+function ldapMessage(success, msg) {
+ var message = msg;
+ if(typeof(msg) == String) {
+ message = "LDAP " +
+ (success ? "Success" : "Error") + ": " + msg;
+ }
+
+ var result = new LDAPResult(message);
+ result.error = !success;
+ return result;
+}
+
+// returns the associated ldap results object, with an error flag of false
+var ldapSuccess =
+ function(msg) { return ldapMessage.apply(this, [true, msg]); };
+
+// returns a helpful error message
+var ldapError =
+ function(msg) { return ldapMessage.apply(this, [false, msg]); };
+
+/* build an LDAP Query (searches for an objectClass and uid) */
+LDAP.prototype.buildLDAPQuery = function(queryUser) {
+ if(queryUser && queryUser.match(/[\w_-]+/)) {
+ return "(&(objectClass=" +
+ this.ldapConfig['userClass'] + ")(uid=" +
+ queryUser + "))"
+ } else return null;
+}
+
+LDAP.prototype.login = function(queryUser, queryPass) {
+ var query = this.buildLDAPQuery(queryUser);
+ if(!query) { return ldapError("invalid LDAP username"); }
+
+ try {
+ var context = LDAP.authenticate(this.ldapConfig['url'],
+ this.ldapConfig['principal'],
+ this.ldapConfig['password']);
+
+ if(!context) {
+ return ldapError("could not authenticate principle user.");
+ }
+
+ var ctrl = new SearchControls();
+ ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ var results = context.search(this.ldapConfig['rootPath'], query, ctrl);
+
+ // if the user is found
+ if(results.hasMore()) {
+ var result = results.next();
+
+ // grab the absolute path to the user
+ var userResult = result.getNameInNamespace();
+ var authed = !!LDAP.authenticate(this.ldapConfig['url'],
+ userResult,
+ queryPass)
+
+ // return the LDAP info on the user upon success
+ return authed ?
+ ldapSuccess(LDAP.parse(result)) :
+ ldapError("Incorrect password. Please try again.");
+ } else {
+ return ldapError("User "+queryUser+" not found in LDAP.");
+ }
+
+ // if there are errors in the search, log them and return "unknown error"
+ } catch (e) {
+ _dmesg(e);
+ return ldapError(new String(e))
+ }
+};
+
+LDAP.prototype.isLDAPSuffix = function(email) {
+ return email.indexOf(this.ldapConfig['ldapSuffix']) ==
+ (email.length-this.ldapConfig['ldapSuffix'].length);
+}
+
+LDAP.prototype.getLDAPSuffix = function() {
+ return this.ldapConfig['ldapSuffix'];
+}
+
+/* static function returns a DirContext, or undefined upon authentation err */
+LDAP.authenticate = function(url, user, pass) {
+ var context = null;
+ try {
+ var env = new Hashtable();
+ env.put(Context.INITIAL_CONTEXT_FACTORY,
+ "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put( Context.SECURITY_PRINCIPAL, user );
+ env.put( Context.SECURITY_CREDENTIALS, pass );
+ env.put(Context.PROVIDER_URL, url);
+ context = new InitialDirContext(env);
+ } catch (e) {
+ // bind failed.
+ }
+ return context;
+}
+
+/* turn a res */
+LDAP.parse = function(result) {
+ var resultobj = {};
+ try {
+ var attrs = result.getAttributes();
+ var ids = attrs.getIDs();
+
+ while(ids.hasMore()) {
+ var id = ids.next().toString();
+ resultobj[id] = [];
+
+ var attr = attrs.get(id);
+
+ for(var i=0; i<attr.size(); i++) {
+ resultobj[id].push(attr.get(i).toString());
+ }
+ }
+ } catch (e) {
+ // naming error
+ return {'keys': e}
+ }
+
+ return resultobj;
+}
+
+LDAP.ldapSingleton = false;
+
+// load in ldap configuration from a file...
+function readLdapConfig(file) {
+ var fileContents = BetterFile.getFileContents(file);
+
+ if(fileContents == null)
+ return "File not found.";
+
+ var configObject = fastJSON.parse(fileContents);
+ if(configObject['ldapSuffix']) {
+ LDAP.ldapSuffix = configObject['ldapSuffix'];
+ }
+ return configObject;
+}
+
+// Sample Configuration file:
+// {
+// "userClass" : "person",
+// "url" : "ldap://localhost:10389",
+// "principal" : "uid=admin,ou=system",
+// "password" : "secret",
+// "rootPath" : "ou=users,ou=system",
+// "nameAttribute": "displayname",
+// "ldapSuffix" : "@ldap"
+// }
+
+// appjet.config['etherpad.useLdapConfiguration'] = "/Users/kroo/Documents/Projects/active/AppJet/ldapConfig.json";
+function getLDAP() {
+ if (! LDAP.ldapSingleton &&
+ appjet.config['etherpad.useLdapConfiguration']) {
+ var config = readLdapConfig(appjet.config['etherpad.useLdapConfiguration']);
+ var error = null;
+ if(!config) {
+ config = null;
+ error = "Error reading LDAP configuration file."
+ }
+ LDAP.ldapSingleton = new LDAP(config, error);
+ }
+
+ return LDAP.ldapSingleton;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pro/pro_pad_db.js b/etherpad/src/etherpad/pro/pro_pad_db.js
new file mode 100644
index 0000000..dbb412c
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_pad_db.js
@@ -0,0 +1,232 @@
+/**
+ * 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.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("cache_utils.syncedWithCache");
+import("stringutils");
+
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_pad_editors");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.lang.System.out.println");
+
+
+// TODO: actually implement the cache part
+
+// NOTE: must return a deep-CLONE of the actual record, because caller
+// may proceed to mutate the returned record.
+
+function _makeRecord(r) {
+ if (!r) {
+ return null;
+ }
+ r.proAttrs = {};
+ if (r.proAttrsJson) {
+ r.proAttrs = fastJSON.parse(r.proAttrsJson);
+ }
+ if (!r.proAttrs.editors) {
+ r.proAttrs.editors = [];
+ }
+ r.proAttrs.editors.sort();
+ return r;
+}
+
+function getSingleRecord(domainId, localPadId) {
+ // TODO: make clone
+ // TODO: use cache
+ var record = sqlobj.selectSingle('pro_padmeta', {domainId: domainId, localPadId: localPadId});
+ return _makeRecord(record);
+}
+
+function update(padRecord) {
+ // TODO: use cache
+
+ padRecord.proAttrsJson = fastJSON.stringify(padRecord.proAttrs);
+ delete padRecord.proAttrs;
+
+ sqlobj.update('pro_padmeta', {id: padRecord.id}, padRecord);
+}
+
+
+//--------------------------------------------------------------------------------
+// create/edit/destory events
+//--------------------------------------------------------------------------------
+
+function onCreatePad(pad) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ var data = {
+ domainId: padutils.getDomainId(pad.getId()),
+ localPadId: padutils.getLocalPadId(pad),
+ createdDate: new Date(),
+ };
+
+ if (getSessionProAccount()) {
+ data.creatorId = getSessionProAccount().id;
+ }
+
+ sqlobj.insert('pro_padmeta', data);
+}
+
+// Not a normal part of the UI. This is only called from admin interface,
+// and thus should actually destroy all record of the pad.
+function onDestroyPad(pad) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ sqlobj.deleteRows('pro_padmeta', {
+ domainId: padutils.getDomainId(pad.getId()),
+ localPadId: padutils.getLocalPadId(pad)
+ });
+}
+
+// Called within the context of a comet post.
+function onEditPad(pad, padAuthorId) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ var editorId = undefined;
+ if (getSessionProAccount()) {
+ editorId = getSessionProAccount().id;
+ }
+
+ if (!(editorId && (editorId > 0))) {
+ return; // etherpad admins
+ }
+
+ pro_pad_editors.notifyEdit(
+ padutils.getDomainId(pad.getId()),
+ padutils.getLocalPadId(pad),
+ editorId,
+ new Date()
+ );
+}
+
+//--------------------------------------------------------------------------------
+// accessing the pad list.
+//--------------------------------------------------------------------------------
+
+function _makeRecordList(lis) {
+ lis.forEach(function(r) {
+ r = _makeRecord(r);
+ });
+ return lis;
+}
+
+function listMyPads() {
+ var domainId = domains.getRequestDomainId();
+ var accountId = getSessionProAccount().id;
+
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, creatorId: accountId, isDeleted: false, isArchived: false});
+ return _makeRecordList(padlist);
+}
+
+function listAllDomainPads() {
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: false});
+ return _makeRecordList(padlist);
+}
+
+function listArchivedPads() {
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: true});
+ return _makeRecordList(padlist);
+}
+
+function listPadsByEditor(editorId) {
+ editorId = Number(editorId);
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: false});
+ padlist = _makeRecordList(padlist);
+ padlist = padlist.filter(function(p) {
+ // NOTE: could replace with binary search to speed things up,
+ // since we know that editors array is sorted.
+ return (p.proAttrs.editors.indexOf(editorId) >= 0);
+ });
+ return padlist;
+}
+
+function listLiveDomainPads() {
+ var thisDomainId = domains.getRequestDomainId();
+ var allLivePadIds = collab_server.getAllPadsWithConnections();
+ var livePadMap = {};
+
+ allLivePadIds.forEach(function(globalId) {
+ if (padutils.isProPadId(globalId)) {
+ var domainId = padutils.getDomainId(globalId);
+ var localId = padutils.globalToLocalId(globalId);
+ if (domainId == thisDomainId) {
+ livePadMap[localId] = true;
+ }
+ }
+ });
+
+ var padList = listAllDomainPads();
+ padList = padList.filter(function(p) {
+ return (!!livePadMap[p.localPadId]);
+ });
+
+ return padList;
+}
+
+//--------------------------------------------------------------------------------
+// misc utils
+//--------------------------------------------------------------------------------
+
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro-padmeta.'+name, fn);
+}
+
+function _withDomainCache(domainId, name, fn) {
+ return _withCache(name+"."+domainId, fn);
+}
+
+
+
+// returns the next pad ID to use for a newly-created pad on this domain.
+function getNextPadId() {
+ var domainId = domains.getRequestDomainId();
+ return _withDomainCache(domainId, 'padcounters', function(c) {
+ var ret;
+ if (c.x === undefined) {
+ c.x = _getLargestNumericPadId(domainId) + 1;
+ }
+ while (sqlobj.selectSingle('pro_padmeta', {domainId: domainId, localPadId: String(c.x)})) {
+ c.x++;
+ }
+ ret = c.x;
+ c.x++;
+ return ret;
+ });
+}
+
+function _getLargestNumericPadId(domainId) {
+ var max = 0;
+ var allPads = listAllDomainPads();
+ allPads.forEach(function(p) {
+ if (stringutils.isNumeric(p.localPadId)) {
+ max = Math.max(max, Number(p.localPadId));
+ }
+ });
+ return max;
+}
+
+
+
diff --git a/etherpad/src/etherpad/pro/pro_pad_editors.js b/etherpad/src/etherpad/pro/pro_pad_editors.js
new file mode 100644
index 0000000..a90f05b
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_pad_editors.js
@@ -0,0 +1,104 @@
+/**
+ * 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.
+ */
+
+import("execution");
+import("jsutils.*");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.log");
+
+var _DOMAIN_EDIT_WRITE_INTERVAL = 2000; // 2 seconds
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro-padmeta.'+name, fn);
+}
+
+function _withDomainCache(domainId, name, fn) {
+ return _withCache(name+"."+domainId, fn);
+}
+
+
+function onStartup() {
+ execution.initTaskThreadPool("pro-padmeta-edits", 1);
+}
+
+function onShutdown() {
+ var success = execution.shutdownAndWaitOnTaskThreadPool("pro-padmeta-edits", 4000);
+ if (!success) {
+ log.warn("Warning: pro.padmeta failed to flush pad edits on shutdown.");
+ }
+}
+
+function notifyEdit(domainId, localPadId, editorId, editTime) {
+ if (!editorId) {
+ // guest editors
+ return;
+ }
+ _withDomainCache(domainId, "edits", function(c) {
+ if (!c[localPadId]) {
+ c[localPadId] = {
+ lastEditorId: editorId,
+ lastEditTime: editTime,
+ recentEditors: []
+ };
+ }
+ var info = c[localPadId];
+ if (info.recentEditors.indexOf(editorId) < 0) {
+ info.recentEditors.push(editorId);
+ }
+ });
+ _flushPadEditsEventually(domainId);
+}
+
+
+function _flushPadEditsEventually(domainId) {
+ // Make sure there is a recurring edit-writer for this domain
+ _withDomainCache(domainId, "recurring-edit-writers", function(c) {
+ if (!c[domainId]) {
+ flushEditsNow(domainId);
+ c[domainId] = true;
+ }
+ });
+}
+
+function flushEditsNow(domainId) {
+ if (!appjet.cache.shutdownHandlerIsRunning) {
+ execution.scheduleTask("pro-padmeta-edits", "proPadmetaFlushEdits",
+ _DOMAIN_EDIT_WRITE_INTERVAL, [domainId]);
+ }
+
+ _withDomainCache(domainId, "edits", function(edits) {
+ var padIdList = keys(edits);
+ padIdList.forEach(function(localPadId) {
+ _writePadEditsToDbNow(domainId, localPadId, edits[localPadId]);
+ delete edits[localPadId];
+ });
+ });
+}
+
+function _writePadEditsToDbNow(domainId, localPadId, editInfo) {
+ var globalPadId = padutils.makeGlobalId(domainId, localPadId);
+ pro_padmeta.accessProPad(globalPadId, function(propad) {
+ propad.setLastEditedDate(editInfo.lastEditTime);
+ propad.setLastEditor(editInfo.lastEditorId);
+ editInfo.recentEditors.forEach(function(eid) {
+ propad.addEditor(eid);
+ });
+ });
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_padlist.js b/etherpad/src/etherpad/pro/pro_padlist.js
new file mode 100644
index 0000000..73b179c
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_padlist.js
@@ -0,0 +1,289 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("jsutils.*");
+import("stringutils");
+
+import("etherpad.utils.*");
+import("etherpad.helpers");
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_accounts");
+
+function _getColumnMeta() {
+ // returns map of {id --> {
+ // title,
+ // sortFn(a,b),
+ // render(p)
+ // }
+
+ function _dateNum(d) {
+ if (!d) {
+ return 0;
+ }
+ return -1 * (+d);
+ }
+
+
+ var cols = {};
+
+ function addAvailableColumn(id, cdata) {
+ if (!cdata.render) {
+ cdata.render = function(p) {
+ return p[id];
+ };
+ }
+ if (!cdata.cmpFn) {
+ cdata.cmpFn = function(a,b) {
+ return cmp(a[id], b[id]);
+ };
+ }
+ cdata.id = id;
+ cols[id] = cdata;
+ }
+
+ addAvailableColumn('public', {
+ title: "",
+ render: function(p) {
+ // TODO: implement an icon with hover text that says public vs.
+ // private
+ return "";
+ },
+ cmpFn: function(a,b) {
+ return 0; // not sort-able
+ }
+ });
+ addAvailableColumn('secure', {
+ title: "",
+ render: function(p) {
+ if (p.password) {
+ return IMG({src: '/static/img/may09/padlock.gif'});
+ } else {
+ return "";
+ }
+ },
+ cmpFn: function(a,b) {
+ return cmp(a.password, b.password);
+ }
+ });
+ addAvailableColumn('title', {
+ title: "Title",
+ render: function(p) {
+ var t = padutils.getProDisplayTitle(p.localPadId, p.title);
+ return A({href: "/"+p.localPadId}, t);
+ },
+ sortFn: function(a, b) {
+ return cmp(padutils.getProDisplayTitle(a.localPadId, a.title),
+ padutils.getProDisplayTitle(b.localPadId, b.title));
+ }
+ });
+ addAvailableColumn('creatorId', {
+ title: "Creator",
+ render: function(p) {
+ return pro_accounts.getFullNameById(p.creatorId);
+ },
+ sortFn: function(a, b) {
+ return cmp(pro_accounts.getFullNameById(a.creatorId),
+ pro_accounts.getFullNameById(b.creatorId));
+ }
+ });
+ addAvailableColumn('createdDate', {
+ title: "Created",
+ render: function(p) {
+ return timeAgo(p.createdDate);
+ },
+ sortFn: function(a, b) {
+ return cmp(_dateNum(a.createdDate), _dateNum(b.createdDate));
+ }
+ });
+ addAvailableColumn('lastEditorId', {
+ title: "Last Editor",
+ render: function(p) {
+ if (p.lastEditorId) {
+ return pro_accounts.getFullNameById(p.lastEditorId);
+ } else {
+ return "";
+ }
+ },
+ sortFn: function(a, b) {
+ var a_ = a.lastEditorId ? pro_accounts.getFullNameById(a.lastEditorId) : "ZZZZZZZZZZ";
+ var b_ = b.lastEditorId ? pro_accounts.getFullNameById(b.lastEditorId) : "ZZZZZZZZZZ";
+ return cmp(a_, b_);
+ }
+ });
+
+ addAvailableColumn('editors', {
+ title: "Editors",
+ render: function(p) {
+ var editors = [];
+ p.proAttrs.editors.forEach(function(editorId) {
+ editors.push([editorId, pro_accounts.getFullNameById(editorId)]);
+ });
+ editors.sort(function(a,b) { return cmp(a[1], b[1]); });
+ var sp = SPAN();
+ for (var i = 0; i < editors.length; i++) {
+ if (i > 0) {
+ sp.push(", ");
+ }
+ sp.push(A({href: "/ep/padlist/edited-by?editorId="+editors[i][0]}, editors[i][1]));
+ }
+ return sp;
+ }
+ });
+
+ addAvailableColumn('lastEditedDate', {
+ title: "Last Edited",
+ render: function(p) {
+ if (p.lastEditedDate) {
+ return timeAgo(p.lastEditedDate);
+ } else {
+ return "never";
+ }
+ },
+ sortFn: function(a,b) {
+ return cmp(_dateNum(a.lastEditedDate), _dateNum(b.lastEditedDate));
+ }
+ });
+ addAvailableColumn('localPadId', {
+ title: "Path",
+ });
+ addAvailableColumn('actions', {
+ title: "",
+ render: function(p) {
+ return DIV({className: "gear-drop", id: "pad-gear-"+p.id}, " ");
+ }
+ });
+
+ addAvailableColumn('connectedUsers', {
+ title: "Connected Users",
+ render: function(p) {
+ var names = [];
+ padutils.accessPadLocal(p.localPadId, function(pad) {
+ var userList = collab_server.getConnectedUsers(pad);
+ userList.forEach(function(u) {
+ if (collab_server.translateSpecialKey(u.specialKey) != 'invisible') {
+ // excludes etherpad admin user
+ names.push(u.name);
+ }
+ });
+ });
+ return names.join(", ");
+ }
+ });
+
+ return cols;
+}
+
+function _sortPads(padList) {
+ var meta = _getColumnMeta();
+ var sortId = _getCurrentSortId();
+ var reverse = false;
+ if (sortId.charAt(0) == '-') {
+ reverse = true;
+ sortId = sortId.slice(1);
+ }
+ padList.sort(function(a,b) { return cmp(a.localPadId, b.localPadId); });
+ padList.sort(function(a,b) { return meta[sortId].sortFn(a, b); });
+ if (reverse) { padList.reverse(); }
+}
+
+function _addClientVars(padList) {
+ var padTitles = {}; // maps localPadId -> title
+ var localPadIds = {}; // maps padmetaId -> localPadId
+ padList.forEach(function(p) {
+ padTitles[p.localPadId] = stringutils.toHTML(padutils.getProDisplayTitle(p.localPadId, p.title));
+ localPadIds[p.id] = p.localPadId;
+ });
+ helpers.addClientVars({
+ padTitles: padTitles,
+ localPadIds: localPadIds
+ });
+}
+
+function _getCurrentSortId() {
+ return request.params.sortBy || "lastEditedDate";
+}
+
+function _renderColumnHeader(m) {
+ var sp = SPAN();
+ var sortBy = _getCurrentSortId();
+ if (m.sortFn) {
+ var d = {sortBy: m.id};
+ var arrow = "";
+ if (sortBy == m.id) {
+ d.sortBy = ("-"+m.id);
+ arrow = html("&#8595;");
+ }
+ if (sortBy == ("-"+m.id)) {
+ arrow = html("&#8593;");
+ }
+ sp.push(arrow, " ", A({href: qpath(d)}, m.title));
+ } else {
+ sp.push(m.title);
+ }
+ return sp;
+}
+
+function renderPadList(padList, columnIds, limit) {
+ _sortPads(padList);
+ _addClientVars(padList);
+
+ if (limit && (limit < padList.length)) {
+ padList = padList.slice(0,limit);
+ }
+
+ var showSecurityInfo = false;
+ padList.forEach(function(p) {
+ if (p.password && p.password.length > 0) { showSecurityInfo = true; }
+ });
+ if (!showSecurityInfo && (columnIds[0] == 'secure')) {
+ columnIds.shift();
+ }
+
+ var columnMeta = _getColumnMeta();
+
+ var t = TABLE({id: "padtable", cellspacing:"0", cellpadding:"0"});
+ var toprow = TR({className: "toprow"});
+ columnIds.forEach(function(cid) {
+ toprow.push(TH(_renderColumnHeader(columnMeta[cid])));
+ });
+ t.push(toprow);
+
+ padList.forEach(function(p) {
+ // Note that this id is always numeric, and is the actual
+ // canonical padmeta id.
+ var row = TR({id: 'padmeta-'+p.id});
+ var first = true;
+ for (var i = 0; i < columnIds.length; i++) {
+ var cid = columnIds[i];
+ var m = columnMeta[cid];
+ var classes = cid;
+ if (i == 0) {
+ classes += (" first");
+ }
+ if (i == (columnIds.length - 1)) {
+ classes += (" last");
+ }
+ row.push(TD({className: classes}, m.render(p)));
+ }
+ t.push(row);
+ });
+
+ return t;
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_padmeta.js b/etherpad/src/etherpad/pro/pro_padmeta.js
new file mode 100644
index 0000000..6f911b2
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_padmeta.js
@@ -0,0 +1,111 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("cache_utils.syncedWithCache");
+import("sync");
+
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_pad_db");
+
+function _doWithProPadLock(domainId, localPadId, func) {
+ var lockName = ["pro-pad", domainId, localPadId].join("/");
+ return sync.doWithStringLock(lockName, func);
+}
+
+function accessProPad(globalPadId, fn) {
+ // retrieve pad from cache
+ var domainId = padutils.getDomainId(globalPadId);
+ if (!domainId) {
+ throw Error("not a pro pad: "+globalPadId);
+ }
+ var localPadId = padutils.globalToLocalId(globalPadId);
+ var padRecord = pro_pad_db.getSingleRecord(domainId, localPadId);
+
+ return _doWithProPadLock(domainId, localPadId, function() {
+ var isDirty = false;
+
+ var proPad = {
+ exists: function() { return !!padRecord; },
+ getDomainId: function() { return domainId; },
+ getLocalPadId: function() { return localPadId; },
+ getGlobalId: function() { return globalPadId; },
+ getDisplayTitle: function() { return padutils.getProDisplayTitle(localPadId, padRecord.title); },
+ setTitle: function(newTitle) {
+ padRecord.title = newTitle;
+ isDirty = true;
+ },
+ isDeleted: function() { return padRecord.isDeleted; },
+ markDeleted: function() {
+ padRecord.isDeleted = true;
+ isDirty = true;
+ },
+ getPassword: function() { return padRecord.password; },
+ setPassword: function(newPass) {
+ if (newPass == "") {
+ newPass = null;
+ }
+ padRecord.password = newPass;
+ isDirty = true;
+ },
+ isArchived: function() { return padRecord.isArchived; },
+ markArchived: function() {
+ padRecord.isArchived = true;
+ isDirty = true;
+ },
+ unmarkArchived: function() {
+ padRecord.isArchived = false;
+ isDirty = true;
+ },
+ setLastEditedDate: function(d) {
+ padRecord.lastEditedDate = d;
+ isDirty = true;
+ },
+ addEditor: function(editorId) {
+ var es = String(editorId);
+ if (es && es.length > 0 && stringutils.isNumeric(editorId)) {
+ if (padRecord.proAttrs.editors.indexOf(editorId) < 0) {
+ padRecord.proAttrs.editors.push(editorId);
+ padRecord.proAttrs.editors.sort();
+ }
+ isDirty = true;
+ }
+ },
+ setLastEditor: function(editorId) {
+ var es = String(editorId);
+ if (es && es.length > 0 && stringutils.isNumeric(editorId)) {
+ padRecord.lastEditorId = editorId;
+ this.addEditor(editorId);
+ isDirty = true;
+ }
+ }
+ };
+
+ var ret = fn(proPad);
+
+ if (isDirty) {
+ pro_pad_db.update(padRecord);
+ }
+
+ return ret;
+ });
+}
+
+function accessProPadLocal(localPadId, fn) {
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ return accessProPad(globalPadId, fn);
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_quotas.js b/etherpad/src/etherpad/pro/pro_quotas.js
new file mode 100644
index 0000000..ed69e1c
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_quotas.js
@@ -0,0 +1,141 @@
+/**
+ * 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.
+ */
+
+import("funhtml.*");
+import("stringutils.startsWith");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+
+import("etherpad.billing.team_billing");
+import("etherpad.globals.*");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.domains");
+import("etherpad.sessions.getSession");
+import("etherpad.store.checkout");
+
+function _createRecordIfNecessary(domainId) {
+ inTransaction(function() {
+ var r = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ if (!r) {
+ var count = pro_accounts.getActiveCount(domainId);
+ sqlobj.insert('pro_account_usage', {
+ domainId: domainId,
+ count: count,
+ lastReset: (new Date),
+ lastUpdated: (new Date)
+ });
+ }
+ });
+}
+
+/**
+ * Called after a successful payment has been made.
+ * Effect: counts the current number of domain accounts and stores that
+ * as the current account usage count.
+ */
+function resetAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var newCount = pro_accounts.getActiveCount(domainId);
+ sqlobj.update(
+ 'pro_account_usage',
+ {domainId: domainId},
+ {count: newCount, lastUpdated: (new Date), lastReset: (new Date)}
+ );
+}
+
+/**
+ * Returns the max number of accounts that have existed simultaneously
+ * since the last reset.
+ */
+function getAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var record = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ return record.count;
+}
+
+
+/**
+ * Updates the current account usage count by computing:
+ * usage_count = max(current_accounts, usage_count)
+ */
+function updateAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var record = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ var currentCount = pro_accounts.getActiveCount(domainId);
+ var newCount = Math.max(record.count, currentCount);
+ sqlobj.update(
+ 'pro_account_usage',
+ {domainId: domainId},
+ {count: newCount, lastUpdated: (new Date)}
+ );
+}
+
+// called per request
+
+function _generateGlobalBillingNotice(status) {
+ if (status == team_billing.CURRENT) {
+ return;
+ }
+ var notice = SPAN();
+ if (status == team_billing.PAST_DUE) {
+ var suspensionDate = checkout.formatDate(team_billing.getDomainSuspensionDate(domains.getRequestDomainId()));
+ notice.push(
+ "Warning: your account is past due and will be suspended on ",
+ suspensionDate, ".");
+ }
+ if (status == team_billing.SUSPENDED) {
+ notice.push(
+ "Warning: your account is suspended because it is more than ",
+ team_billing.GRACE_PERIOD_DAYS, " days past due.");
+ }
+
+ if (pro_accounts.isAdminSignedIn()) {
+ notice.push(" ", A({href: "/ep/admin/billing/"}, "Manage billing"), ".");
+ } else {
+ getSession().billingProblem = "Payment is required for sites with more than "+PRO_FREE_ACCOUNTS+" accounts.";
+ notice.push(" ", "Please ",
+ A({href: "/ep/payment-required"}, "contact a site administrator"), ".");
+ }
+ request.cache.globalProNotice = notice;
+}
+
+function perRequestBillingCheck() {
+ // Do nothing if under the free account limit.
+ var activeAccounts = pro_accounts.getCachedActiveCount(domains.getRequestDomainId());
+ if (activeAccounts <= PRO_FREE_ACCOUNTS) {
+ return;
+ }
+
+ var status = team_billing.getDomainStatus(domains.getRequestDomainId());
+ _generateGlobalBillingNotice(status);
+
+ // now see if we need to block the request because of account
+ // suspension
+ if (status != team_billing.SUSPENDED) {
+ return;
+ }
+ // These path sare still OK if a suspension is on.
+ if ((startsWith(request.path, "/ep/account/") ||
+ startsWith(request.path, "/ep/admin/") ||
+ startsWith(request.path, "/ep/pro-help/") ||
+ startsWith(request.path, "/ep/payment-required"))) {
+ return;
+ }
+
+ getSession().billingProblem = "Payment is required for sites with more than "+PRO_FREE_ACCOUNTS+" accounts.";
+ response.redirect('/ep/payment-required');
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_utils.js b/etherpad/src/etherpad/pro/pro_utils.js
new file mode 100644
index 0000000..d3098d7
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_utils.js
@@ -0,0 +1,169 @@
+/**
+ * Copyright 2009 Google Inc.
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * 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("funhtml.*");
+import("stringutils.startsWith");
+
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_quotas");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+
+import("etherpad.control.pro.pro_main_control");
+
+jimport("java.lang.System.out.println");
+
+function _stripComet(x) {
+ if (x.indexOf('.comet.') > 0) {
+ x = x.split('.comet.')[1];
+ }
+ return x;
+}
+
+function getProRequestSubdomain() {
+ var d = _stripComet(request.domain);
+ return d.split('.')[0];
+}
+
+function getRequestSuperdomain() {
+ var parts = request.domain.split('.');
+ while (parts.length > 0) {
+ var domain = parts.join('.');
+ if (domainEnabled(domain)) {
+ return domain;
+ }
+ parts.shift();
+ }
+ return false;
+}
+
+function isProDomainRequest() {
+ if(!isProAccountEnabled())
+ return false;
+ // the result of this function never changes within the same request.
+ var c = appjet.requestCache;
+ if (c.isProDomainRequest === undefined) {
+ c.isProDomainRequest = _computeIsProDomainRequest();
+ }
+ return c.isProDomainRequest;
+}
+
+function _computeIsProDomainRequest() {
+ if (pne_utils.isPNE()) {
+ return true;
+ }
+
+ var domain = _stripComet(request.domain);
+
+ if (domainEnabled(domain)) {
+ return false;
+ }
+
+ var requestSuperdomain = getRequestSuperdomain();
+
+ if (domainEnabled(requestSuperdomain)) {
+ // now see if this subdomain is actually in our database.
+ if (domains.getRequestDomainRecord()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return false;
+}
+
+function preDispatchAccountCheck() {
+ // if account is not logged in, redirect to /ep/account/login
+ //
+ // if it's PNE and there is no admin account, allow them to create an admin
+ // account.
+
+ if (pro_main_control.isActivationAllowed()) {
+ return;
+ }
+
+ if (!pro_accounts.doesAdminExist()) {
+ if (request.path != '/ep/account/create-admin-account') {
+ // should only happen for eepnet installs
+ response.redirect('/ep/account/create-admin-account');
+ }
+ } else {
+ pro_accounts.requireAccount();
+ }
+
+ pro_quotas.perRequestBillingCheck();
+}
+
+function renderFramedMessage(m) {
+ renderFramedHtml(
+ DIV(
+ {style: "font-size: 2em; padding: 2em; margin: 4em; border: 1px solid #ccc; background: #e6e6e6;"},
+ m));
+}
+
+function getFullProDomain() {
+ // TODO: have a special config param for this? --etherpad.canonicalDomain
+ return request.domain;
+}
+
+// domain, including port if necessary
+function getFullProHost() {
+ var h = getFullProDomain();
+ var parts = request.host.split(':');
+ if (parts.length > 1) {
+ h += (':' + parts[1]);
+ }
+ return h;
+}
+
+function getFullSuperdomainHost() {
+ if (isProDomainRequest()) {
+ var h = getRequestSuperdomain()
+ var parts = request.host.split(':');
+ if (parts.length > 1) {
+ h += (':' + parts[1]);
+ }
+ return h;
+ } else {
+ return request.host;
+ }
+}
+
+function getEmailFromAddr() {
+ var fromDomain = 'pad.spline.inf.fu-berlin.de';
+ if (pne_utils.isPNE()) {
+ fromDomain = getFullProDomain();
+ }
+ return ('"EtherPad" <noreply@'+fromDomain+'>');
+}
+
+function renderGlobalProNotice() {
+ if (request.cache.globalProNotice) {
+ return DIV({className: 'global-pro-notice'},
+ request.cache.globalProNotice);
+ } else {
+ return "";
+ }
+}
+
diff --git a/etherpad/src/etherpad/quotas.js b/etherpad/src/etherpad/quotas.js
new file mode 100644
index 0000000..7e939ec
--- /dev/null
+++ b/etherpad/src/etherpad/quotas.js
@@ -0,0 +1,50 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+import("etherpad.pne.pne_utils");
+
+// TODO: hook into PNE?
+
+function getMaxSimultaneousPadEditors(globalPadId) {
+ if (isProDomainRequest()) {
+ if (pne_utils.isPNE()) {
+ return licensing.getMaxUsersPerPad();
+ } else {
+ return 1e6;
+ }
+ } else {
+ // pad.spline.inf.fu-berlin.de public pads
+ if (globalPadId && stringutils.startsWith(globalPadId, "conf-")) {
+ return 64;
+ } else {
+ return 16;
+ }
+ }
+ return 1e6;
+}
+
+function getMaxSavedRevisionsPerPad() {
+ if (isProDomainRequest()) {
+ return 1e3;
+ } else {
+ // free public pad.spline.inf.fu-berlin.de
+ return 100;
+ }
+}
+
diff --git a/etherpad/src/etherpad/sessions.js b/etherpad/src/etherpad/sessions.js
new file mode 100644
index 0000000..f430ddd
--- /dev/null
+++ b/etherpad/src/etherpad/sessions.js
@@ -0,0 +1,203 @@
+/**
+ * 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.
+ */
+
+import("sessions");
+import("stringutils.randomHash");
+import("funhtml.*");
+
+import("etherpad.log");
+import("etherpad.globals.*");
+import("etherpad.pro.pro_utils");
+import("etherpad.utils.*");
+import("cache_utils.syncedWithCache");
+
+jimport("java.lang.System.out.println");
+
+var _TRACKING_COOKIE_NAME = "ET";
+var _SESSION_COOKIE_NAME = "ES";
+
+function _updateInitialReferrer(data) {
+
+ if (data.initialReferer) {
+ return;
+ }
+
+ var ref = request.headers["Referer"];
+
+ if (!ref) {
+ return;
+ }
+ if (ref.indexOf('http://'+request.host) == 0) {
+ return;
+ }
+ if (ref.indexOf('https://'+request.host) == 0) {
+ return;
+ }
+
+ data.initialReferer = ref;
+ log.custom("referers", {referer: ref});
+}
+
+function _getScopedDomain(subDomain) {
+ var d = request.domain;
+ if (d.indexOf(".") == -1) {
+ // special case for "localhost". For some reason, firefox does not like cookie domains
+ // to be ".localhost".
+ return undefined;
+ }
+ if (subDomain) {
+ d = subDomain + "." + d;
+ }
+ return "." + d;
+}
+//--------------------------------------------------------------------------------
+
+// pass in subDomain to get the session data for a particular subdomain --
+// intended for debugging.
+function getSession(subDomain) {
+ var sessionData = sessions.getSession({
+ cookieName: _SESSION_COOKIE_NAME,
+ domain: _getScopedDomain(subDomain)
+ });
+ _updateInitialReferrer(sessionData);
+ return sessionData;
+}
+
+function getSessionId() {
+ return sessions.getSessionId(_SESSION_COOKIE_NAME, false, _getScopedDomain());
+}
+
+function _getGlobalSessionId() {
+ return (request.isDefined && request.cookies[_SESSION_COOKIE_NAME]) || null;
+}
+
+function isAnEtherpadAdmin() {
+ var sessionId = _getGlobalSessionId();
+ if (! sessionId) {
+ return false;
+ }
+
+ return syncedWithCache("isAnEtherpadAdmin", function(c) {
+ return !! c[sessionId];
+ });
+}
+
+function setIsAnEtherpadAdmin(v) {
+ var sessionId = _getGlobalSessionId();
+ if (! sessionId) {
+ return;
+ }
+
+ syncedWithCache("isAnEtherpadAdmin", function(c) {
+ if (v) {
+ c[sessionId] = true;
+ }
+ else {
+ delete c[sessionId];
+ }
+ });
+}
+
+//--------------------------------------------------------------------------------
+
+function setTrackingCookie() {
+ if (request.cookies[_TRACKING_COOKIE_NAME]) {
+ return;
+ }
+
+ var trackingVal = randomHash(16);
+ var expires = new Date(32503708800000); // year 3000
+
+ response.setCookie({
+ name: _TRACKING_COOKIE_NAME,
+ value: trackingVal,
+ path: "/",
+ domain: _getScopedDomain(),
+ expires: expires
+ });
+}
+
+function getTrackingId() {
+ // returns '-' if no tracking ID (caller can assume)
+ return (request.cookies[_TRACKING_COOKIE_NAME] || response.getCookie(_TRACKING_COOKIE_NAME) || '-');
+}
+
+//--------------------------------------------------------------------------------
+
+function preRequestCookieCheck() {
+ if (isStaticRequest()) {
+ return;
+ }
+
+ // If this function completes without redirecting, then it means
+ // there is a valid session cookie and tracking cookie.
+
+ if (request.cookies[_SESSION_COOKIE_NAME] &&
+ request.cookies[_TRACKING_COOKIE_NAME]) {
+
+ if (request.params.cookieShouldBeSet) {
+ response.redirect(qpath({cookieShouldBeSet: null}));
+ }
+ return;
+ }
+
+ // Only superdomains can set cookies.
+ var isSuperdomain = domainEnabled(request.domain);
+
+ if (isSuperdomain) {
+ // superdomain without cookies
+
+ getSession();
+ setTrackingCookie();
+
+ // check if we need to redirect back to a subdomain.
+ if ((request.path == "/") &&
+ (request.params.setCookie) &&
+ (request.params.contUrl)) {
+
+ var contUrl = request.params.contUrl;
+ if (contUrl.indexOf("?") == -1) {
+ contUrl += "?";
+ }
+ contUrl += "&cookieShouldBeSet=1";
+ response.redirect(contUrl);
+ }
+ } else {
+ var parts = request.domain.split(".");
+ if (parts.length < 3) {
+ // invalid superdomain
+ response.write("invalid superdomain");
+ response.stop();
+ }
+ // subdomain without cookies
+ if (request.params.cookieShouldBeSet) {
+ log.warn("Cookie failure!");
+ renderFramedHtml(DIV({style: "border: 1px solid #ccc; padding: 1em; width: 600px; margin: 1em auto; font-size: 1.4em;"},
+ P("Please enable cookies in your browser in order to access this site."),
+ BR(),
+ P(A({href: "/"}, "Continue"))));
+ response.stop();
+ } else {
+ var contUrl = request.url;
+ var p = request.host.split(':')[1];
+ p = (p ? (":"+p) : "");
+ response.redirect(request.scheme+"://"+pro_utils.getRequestSuperdomain()+p+
+ "/?setCookie=1&contUrl="+encodeURIComponent(contUrl));
+ }
+ }
+}
+
+
diff --git a/etherpad/src/etherpad/statistics/exceptions.js b/etherpad/src/etherpad/statistics/exceptions.js
new file mode 100644
index 0000000..723085d
--- /dev/null
+++ b/etherpad/src/etherpad/statistics/exceptions.js
@@ -0,0 +1,231 @@
+/**
+ * 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.
+ */
+
+
+import("fastJSON");
+import("etherpad.log");
+import("cache_utils.syncedWithCache");
+import("funhtml.*");
+import("jsutils.{eachProperty,keys}");
+
+function _dayKey(date) {
+ return [date.getFullYear(), date.getMonth()+1, date.getDate()].join(',');
+}
+
+function _dateAddDays(date, numDays) {
+ return new Date((+date) + numDays*1000*60*60*24);
+}
+
+function _loadDay(date) {
+ var fileName = log.frontendLogFileName('exception', date);
+ if (! fileName) {
+ return [];
+ }
+ var reader = new java.io.BufferedReader(new java.io.FileReader(fileName));
+ var line = null;
+ var array = [];
+ while ((line = reader.readLine()) !== null) {
+ array.push(fastJSON.parse(line));
+ }
+ return array;
+}
+
+function _accessLatestLogs(func) {
+ syncedWithCache("etherpad.statistics.exceptions", function(exc) {
+ if (! exc.byDay) {
+ exc.byDay = {};
+ }
+ // always reload today from disk
+ var now = new Date();
+ var today = now;
+ var todayKey = _dayKey(today);
+ exc.byDay[todayKey] = _loadDay(today);
+ var activeKeys = {};
+ activeKeys[todayKey] = true;
+ // load any of 7 previous days that aren't loaded or
+ // were not loaded as a historical day
+ for(var i=1;i<=7;i++) {
+ var pastDay = _dateAddDays(today, -i);
+ var pastDayKey = _dayKey(pastDay);
+ activeKeys[pastDayKey] = true;
+ if ((! exc.byDay[pastDayKey]) || (! exc.byDay[pastDayKey].sealed)) {
+ exc.byDay[pastDayKey] = _loadDay(pastDay);
+ exc.byDay[pastDayKey].sealed = true; // in the past, won't change
+ }
+ }
+ // clear old days
+ for(var k in exc.byDay) {
+ if (! (k in activeKeys)) {
+ delete exc.byDay[k];
+ }
+ }
+
+ var logs = {
+ getDay: function(daysAgo) {
+ return exc.byDay[_dayKey(_dateAddDays(today, -daysAgo))];
+ },
+ eachLineInLastNDays: function(n, func) {
+ var oldest = _dateAddDays(now, -n);
+ var oldestNum = +oldest;
+ for(var i=n;i>=0;i--) {
+ var lines = logs.getDay(i);
+ lines.forEach(function(line) {
+ if (line.date > oldestNum) {
+ func(line);
+ }
+ });
+ }
+ }
+ };
+
+ func(logs);
+ });
+}
+
+function _exceptionHash(line) {
+ // skip the first line of jsTrace, take hashCode of rest
+ var trace = line.jsTrace;
+ var stack = trace.substring(trace.indexOf('\n') + 1);
+ return new java.lang.String(stack).hashCode();
+}
+
+// Used to take a series of strings and produce an array of
+// [common prefix, example middle, common suffix], or
+// [string] if the strings are the same. Takes oldInfo
+// and returns newInfo; each is either null or an array
+// of length 1 or 3.
+function _accumCommonPrefixSuffix(oldInfo, newString) {
+ function _commonPrefixLength(a, b) {
+ var x = 0;
+ while (x < a.length && x < b.length && a.charAt(x) == b.charAt(x)) {
+ x++;
+ }
+ return x;
+ }
+
+ function _commonSuffixLength(a, b) {
+ var x = 0;
+ while (x < a.length && x < b.length &&
+ a.charAt(a.length-1-x) == b.charAt(b.length-1-x)) {
+ x++;
+ }
+ return x;
+ }
+
+ if (! oldInfo) {
+ return [newString];
+ }
+ else if (oldInfo.length == 1) {
+ var oldString = oldInfo[0];
+ if (oldString == newString) {
+ return oldInfo;
+ }
+ var newInfo = [];
+ var a = _commonPrefixLength(oldString, newString);
+ newInfo[0] = newString.substring(0, a);
+ oldString = oldString.substring(a);
+ newString = newString.substring(a);
+ var b = _commonSuffixLength(oldString, newString);
+ newInfo[2] = newString.slice(-b);
+ oldString = oldString.slice(0, -b);
+ newString = newString.slice(0, -b);
+ newInfo[1] = newString;
+ return newInfo;
+ }
+ else {
+ // oldInfo.length == 3
+ var a = _commonPrefixLength(oldInfo[0], newString);
+ var b = _commonSuffixLength(oldInfo[2], newString);
+ return [newString.slice(0, a), newString.slice(a, -b),
+ newString.slice(-b)];
+ }
+}
+
+function render() {
+
+ _accessLatestLogs(function(logs) {
+ var weekCounts = {};
+ var totalWeekCount = 0;
+
+ // count exceptions of each kind in last week
+ logs.eachLineInLastNDays(7, function(line) {
+ var hash = _exceptionHash(line);
+ weekCounts[hash] = (weekCounts[hash] || 0) + 1;
+ totalWeekCount++;
+ });
+
+ var dayData = {};
+ var totalDayCount = 0;
+
+ // accumulate data about each exception in last 24 hours
+ logs.eachLineInLastNDays(1, function(line) {
+ var hash = _exceptionHash(line);
+ var oldData = dayData[hash];
+ var data = (oldData || {});
+ if (! oldData) {
+ data.hash = hash;
+ data.trace = line.jsTrace.substring(line.jsTrace.indexOf('\n')+1);
+ data.trackers = {};
+ }
+ var msg = line.jsTrace.substring(0, line.jsTrace.indexOf('\n'));
+ data.message = _accumCommonPrefixSuffix(data.message, msg);
+ data.count = (data.count || 0)+1;
+ data.trackers[line.tracker] = true;
+ totalDayCount++;
+ dayData[hash] = data;
+ });
+
+ // put day datas in an array and sort
+ var dayDatas = [];
+ eachProperty(dayData, function(k,v) {
+ dayDatas.push(v);
+ });
+ dayDatas.sort(function(a, b) {
+ return b.count - a.count;
+ });
+
+ // process
+ dayDatas.forEach(function(data) {
+ data.weekCount = (weekCounts[data.hash] || 0);
+ data.numTrackers = keys(data.trackers).length;
+ });
+
+ // gen HTML
+ function num(n) { return SPAN({className:'num'}, n); }
+
+ response.write(STYLE(html(".trace { height: 300px; overflow: auto; background: #eee; margin-left: 1em; font-family: monospace; border: 1px solid #833; padding: 4px; }\n"+
+ ".exc { margin: 1em 0; }\n"+
+ ".num { font-size: 150%; }")));
+
+ response.write(P("Total exceptions in past day: ", num(totalDayCount),
+ ", past week: ", totalWeekCount));
+
+ response.write(P(SMALL(EM("Data on this page is live."))));
+
+ response.write(H2("Exceptions grouped by stack trace:"));
+
+ dayDatas.forEach(function(data) {
+ response.write(DIV({className:'exc'},
+ 'Past day: ',num(data.count),', Past week: ',
+ data.weekCount,', Different tracker cookies today: ',
+ data.numTrackers,
+ '\n',data.message[0],
+ (data.message[1] && I(data.message[1])) || '',
+ (data.message[2] || ''),'\n',
+ DIV({className:'trace'}, data.trace)));
+ });
+ });
+}
diff --git a/etherpad/src/etherpad/statistics/statistics.js b/etherpad/src/etherpad/statistics/statistics.js
new file mode 100644
index 0000000..8174405
--- /dev/null
+++ b/etherpad/src/etherpad/statistics/statistics.js
@@ -0,0 +1,1248 @@
+/**
+ * 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.
+ */
+
+import("dateutils.noon");
+import("execution");
+import("exceptionutils");
+import("fastJSON");
+import("fileutils.fileLineIterator");
+import("jsutils.*");
+import("sqlbase.sqlobj");
+
+import("etherpad.log");
+
+jimport("net.appjet.oui.GenericLoggerUtils");
+jimport("net.appjet.oui.LoggableFromJson");
+jimport("net.appjet.oui.FilterWrangler");
+jimport("java.lang.System.out.println");
+jimport("net.appjet.common.util.ExpiringMapping");
+
+var millisInDay = 86400*1000;
+
+function _stats() {
+ if (! appjet.cache.statistics) {
+ appjet.cache.statistics = {};
+ }
+ return appjet.cache.statistics;
+}
+
+function onStartup() {
+ execution.initTaskThreadPool("statistics", 1);
+ _scheduleNextDailyUpdate();
+
+ onReset();
+}
+
+function _info(m) {
+ log.info({type: 'statistics', message: m});
+}
+
+function _warn(m) {
+ log.info({type: 'statistics', message: m});
+}
+
+function _statData() {
+ return _stats().stats;
+}
+
+function getAllStatNames() {
+ return keys(_statData());
+}
+
+function getStatData(statName) {
+ return _statData()[statName];
+}
+
+function _setStatData(statName, data) {
+ _statData()[statName] = data;
+}
+
+function liveSnapshot(stat) {
+ var statObject;
+ if (typeof(stat) == 'string') {
+ // "stat" is the stat name.
+ statObject = getStatData(stat);
+ } else if (typeof(stat) == 'object') {
+ statObject = stat;
+ } else {
+ return;
+ }
+ return _callFunction(statObject.snapshot_f,
+ statObject.name, statObject.options, statObject.data);
+}
+
+// ------------------------------------------------------------------
+// stats processing
+// ------------------------------------------------------------------
+
+// some useful constants
+var LIVE = 'live';
+var HIST = 'historical';
+var HITS = 'hits';
+var UNIQ = 'uniques';
+var VALS = 'values';
+var HGRM = 'histogram';
+
+// helpers
+
+function _date(d) {
+ return new Date(d);
+}
+
+function _saveStat(day, name, value) {
+ var timestamp = Math.floor(day.valueOf() / 1000);
+ _info({statistic: name,
+ timestamp: timestamp,
+ value: value});
+ try {
+ sqlobj.insert('statistics', {
+ name: name,
+ timestamp: timestamp,
+ value: fastJSON.stringify(value)
+ });
+ } catch (e) {
+ var msg;
+ try {
+ msg = e.getMessage();
+ } catch (e2) {
+ try {
+ msg = e.toSource();
+ } catch (e3) {
+ msg = "(none)";
+ }
+ }
+ _warn("failed to save stat "+name+": "+msg);
+ }
+}
+
+function _convertScalaTopValuesToJs(topValues) {
+ var totalValue = topValues._1();
+ var countsMap = topValues._2();
+ countsObj = {};
+ countsMap.foreach(scalaF1(function(pair) { countsObj[pair._1()] = pair._2(); }));
+ return {total: totalValue, counts: countsObj};
+}
+
+function _fakeMap() {
+ var map = {}
+ return {
+ get: function(k) { return map[k]; },
+ put: function(k, v) { map[k] = v; },
+ remove: function(k) { delete map[k]; }
+ }
+}
+
+function _withinSecondsOf(numSeconds, t1, t2) {
+ return (t1 > t2-numSeconds*1000) && (t1 < t2+numSeconds*1000);
+}
+
+function _callFunction(functionName, arg1, arg2, etc) {
+ var f = this[functionName];
+ var args = Array.prototype.slice.call(arguments, 1);
+ return f.apply(this, args);
+}
+
+// trackers and other init functions
+
+function _hitTracker(trackerType, timescaleType) {
+ var className;
+ switch (trackerType) {
+ case HITS: className = "BucketedLastHits"; break;
+ case UNIQ: className = "BucketedUniques"; break;
+ case VALS: className = "BucketedValueCounts"; break;
+ case HGRM: className = "BucketedLastHitsHistogram"; break;
+ }
+ var tracker;
+ switch (timescaleType) {
+ case LIVE:
+ tracker = new net.appjet.oui[className](24*60*60*1000);
+ break;
+ case HIST:
+ // timescale just needs to be longer than a day.
+ tracker = new net.appjet.oui[className](365*24*60*60*1000, true);
+ break;
+ }
+
+ var conversionData = {
+ total_f: "count",
+ history_f: "history",
+ latest_f: "latest",
+ };
+ switch (trackerType) {
+ case HITS: case UNIQ:
+ conversionData.conversionFunction =
+ function(x) { return x; } // no conversion necessary.
+ break;
+ case VALS:
+ conversionData.conversionFunction = _convertScalaTopValuesToJs
+ break;
+ case HGRM:
+ conversionData.conversionFunction =
+ function(hFunc) { return function(pct) { return hFunc.apply(pct); } }
+ break;
+ }
+
+
+ return {
+ tracker: tracker,
+ conversionData: conversionData,
+ hit: function(d, n1, n2) {
+ d = _date(d);
+ if (n2 === undefined) {
+ this.tracker.hit(d, n1);
+ } else {
+ this.tracker.hit(d, n1, n2);
+ }
+ },
+ get total() {
+ return this.conversionData.conversionFunction(this.tracker[this.conversionData.total_f]());
+ },
+ history: function(bucketsPerSample, numSamples) {
+ var scalaArray = this.tracker[this.conversionData.history_f](bucketsPerSample, numSamples);
+ var jsArray = [];
+ for (var i = 0; i < scalaArray.length(); ++i) {
+ jsArray.push(this.conversionData.conversionFunction(scalaArray.apply(i)));
+ }
+ return jsArray;
+ },
+ latest: function(bucketsPerSample) {
+ return this.conversionData.conversionFunction(this.tracker[this.conversionData.latest_f](bucketsPerSample));
+ }
+ }
+}
+
+function _initCount(statName, options, timescaleType) {
+ return _hitTracker(HITS, timescaleType);
+}
+function _initUniques(statName, options, timescaleType) {
+ return _hitTracker(UNIQ, timescaleType);
+}
+function _initTopValues(statName, options, timescaleType) {
+ return _hitTracker(VALS, timescaleType);
+}
+function _initHistogram(statName, options, timescaleType) {
+ return _hitTracker(HGRM, timescaleType);
+}
+
+function _initLatencies(statName, options, type) {
+ var hits = _initTopValues(statName, options, type);
+ var latencies = _initTopValues(statName, options, type);
+
+ return {
+ hit: function(d, value, latency) {
+ hits.hit(d, value);
+ latencies.hit(d, value, latency);
+ },
+ hits: hits,
+ latencies: latencies
+ }
+}
+
+function _initDisconnectTracker(statName, options, timescaleType) {
+ return {
+ map: (timescaleType == LIVE ? new ExpiringMapping(60*1000) : _fakeMap()),
+ counter: _initCount(statName, options, timescaleType),
+ uniques: _initUniques(statName, options, timescaleType),
+ isLive: timescaleType == LIVE
+ }
+}
+
+// update functions
+
+function _updateCount(statName, options, logName, data, logObject) {
+ // println("update count: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+ if (options.filter == null || options.filter(logObject)) {
+ data.hit(logObject.date, 1);
+ }
+}
+
+function _updateSum(statName, options, logName, data, logObject) {
+ // println("update sum: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+ if (options.filter == null || options.filter(logObject)) {
+ data.hit(logObject.date, Math.round(Number(logObject[options.fieldName])));
+ }
+}
+
+function _updateUniquenessCount(statName, options, logName, data, logObject) {
+ // println("update uniqueness: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+ if (options.filter == null || options.filter(logObject)) {
+ var value = logObject[options.fieldName];
+ if (value === undefined) { return; }
+ data.hit(logObject.date, value);
+ }
+}
+
+function _updateTopValues(statName, options, logName, data, logObject) {
+ // println("update topvalues: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+
+ if (options.filter == null || options.filter(logObject)) {
+ var value = logObject[options.fieldName];
+ if (value === undefined) { return; }
+ if (options.canonicalizer) {
+ value = options.canonicalizer(value);
+ }
+ data.hit(logObject.date, value);
+ }
+}
+
+function _updateLatencies(statName, options, logName, data, logObject) {
+ // println("update latencies: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+
+ if (options.filter == null || options.filter(logObject)) {
+ var value = logObject[options.fieldName];
+ var latency = logObject[options.latencyFieldName];
+ if (value === undefined) { return; }
+ data.hit(logObject.date, value, latency);
+ }
+}
+
+function _updateDisconnectTracker(statName, options, logName, data, logObject) {
+ if (logName == "frontend/padevents" && logObject.type != "userleave") {
+ // we only care about userleaves from the padevents log.
+ return;
+ }
+
+ var [evtPrefix, otherPrefix] =
+ (logName == "frontend/padevents" ? ["l-", "d-"] : ["d-", "l-"]);
+ var dateLong = logObject.date;
+ var userId = logObject.session;
+
+ var lastOtherEvent = data.map.get(otherPrefix+userId);
+ if (lastOtherEvent != null && _withinSecondsOf(60, dateLong, lastOtherEvent.date)) {
+ data.counter.hit(logObject.date, 1);
+ data.uniques.hit(logObject.date, userId);
+ data.map.remove(otherPrefix+userId);
+ if (data.isLive) {
+ log.custom("avoidable_disconnects",
+ {userId: userId,
+ errorMessage: lastOtherEvent.errorMessage || logObject.errorMessage});
+ }
+ } else {
+ data.map.put(evtPrefix+userId, {date: dateLong, message: logObject.errorMessage});
+ }
+}
+
+// snapshot functions
+
+function _lazySnapshot(snapshot) {
+ var total;
+ var history = {};
+ var latest = {};
+ return {
+ get total() {
+ if (total === undefined) {
+ total = snapshot.total;
+ }
+ return total;
+ },
+ history: function(bucketsPerSample, numSamples) {
+ if (history[""+bucketsPerSample+":"+numSamples] === undefined) {
+ history[""+bucketsPerSample+":"+numSamples] = snapshot.history(bucketsPerSample, numSamples);
+ }
+ return history[""+bucketsPerSample+":"+numSamples];
+ },
+ latest: function(bucketsPerSample) {
+ if (latest[""+bucketsPerSample] === undefined) {
+ latest[""+bucketsPerSample] = snapshot.latest(bucketsPerSample);
+ }
+ return latest[""+bucketsPerSample];
+ }
+ }
+}
+
+function _snapshotTotal(statName, options, data) {
+ return _lazySnapshot(data);
+}
+
+function _convertTopValue(topValue) {
+ var counts = topValue.counts;
+ var sortedValues = keys(counts).sort(function(x, y) {
+ return counts[y] - counts[x];
+ }).map(function(key) {
+ return { value: key, count: counts[key] };
+ });
+ return {count: topValue.total, topValues: sortedValues.slice(0, 50) };
+}
+
+function _snapshotTopValues(statName, options, data) {
+ var convertedData = {};
+
+ return _lazySnapshot({
+ get total() {
+ return _convertTopValue(data.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return data.history(bucketsPerSample, numSamples).map(_convertTopValue);
+ },
+ latest: function(bucketsPerSample) {
+ return _convertTopValue(data.latest(bucketsPerSample));
+ }
+ });
+}
+
+function _snapshotLatencies(statName, options, data) {
+ // convert the hits + total latencies into a topValues-style data object.
+ var hits = data.hits;
+ var totalLatencies = data.latencies;
+
+ function convertCountsObjects(latencyCounts, hitCounts) {
+ var mergedCounts = {}
+ keys(latencyCounts.counts).forEach(function(value) {
+ mergedCounts[value] =
+ Math.round(latencyCounts.counts[value] / (hitCounts.counts[value] || 1));
+ });
+ return {counts: mergedCounts, total: latencyCounts.total / (hitCounts.total || 1)};
+ }
+
+ // ...and then convert that object into a snapshot.
+ return _snapshotTopValues(statName, options, {
+ get total() {
+ return convertCountsObjects(totalLatencies.total, hits.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return mergeArrays(
+ convertCountsObjects,
+ totalLatencies.history(bucketsPerSample, numSamples),
+ hits.history(bucketsPerSample, numSamples));
+ },
+ latest: function(bucketsPerSample) {
+ return convertCountsObjects(totalLatencies.latest(bucketsPerSample), hits.latest(bucketsPerSample));
+ }
+ });
+}
+
+function _snapshotDisconnectTracker(statName, options, data) {
+ var topValues = {};
+ var counts = data.counter;
+ var uniques = data.uniques;
+ function topValue(counts, uniques) {
+ return {
+ count: counts,
+ topValues: [{value: "total_disconnects", count: counts},
+ {value: "disconnected_userids", count: uniques}]
+ }
+ }
+ return _lazySnapshot({
+ get total() {
+ return topValue(counts.total, uniques.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return mergeArrays(
+ topValue,
+ counts.history(bucketsPerSample, numSamples),
+ uniques.history(bucketsPerSample, numSamples));
+ },
+ latest: function(bucketsPerSample) {
+ return topValue(counts.latest(bucketsPerSample), uniques.latest(bucketsPerSample));
+ }
+ });
+}
+
+function _generateLogInterestMap(statNames) {
+ var interests = {};
+ statNames.forEach(function(statName) {
+ var logs = getStatData(statName).logNames;
+ logs.forEach(function(logName) {
+ if (! interests[logName]) {
+ interests[logName] = {};
+ }
+ interests[logName][statName] = true;
+ });
+ });
+ return interests;
+}
+
+
+// ------------------------------------------------------------------
+// stat generators
+// ------------------------------------------------------------------
+
+// statSpec has these properties
+// name
+// dataType - line, topvalues, histogram, etc.
+// logNames
+// init_f
+// update_f
+// snapshot_f
+// options - object containing any additional data, passed in to to the various functions.
+
+// init_f gets (statName, options, "live"|"historical")
+// update_f gets (statName, options, logName, data, logObject)
+// snapshot_f gets (statName, options, data)
+function addStat(statSpec) {
+ var statName = statSpec.name;
+ if (! getStatData(statName)) {
+ var initialData =
+ _callFunction(statSpec.init_f, statName, statSpec.options, LIVE);
+ _setStatData(statName, {
+ data: initialData,
+ });
+ }
+
+ var s = getStatData(statName);
+
+ s.options = statSpec.options;
+ s.name = statName;
+ s.logNames = statSpec.logNames;
+ s.dataType = statSpec.dataType;
+ s.historicalDays = ("historicalDays" in statSpec ? statSpec.historicalDays : 1);
+
+ s.init_f = statSpec.init_f;
+ s.update_f = statSpec.update_f;
+ s.snapshot_f = statSpec.snapshot_f;
+
+ function registerInterest(logName) {
+ if (! _stats().logNamesToInterestedStatNames[logName]) {
+ _stats().logNamesToInterestedStatNames[logName] = {};
+ }
+ _stats().logNamesToInterestedStatNames[logName][statName] = true;
+ }
+ statSpec.logNames.forEach(registerInterest);
+}
+
+function addSimpleCount(statName, historicalDays, logName, filter) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initCount",
+ update_f: "_updateCount",
+ snapshot_f: "_snapshotTotal",
+ options: { filter: filter },
+ historicalDays: historicalDays || 1
+ });
+}
+
+function addSimpleSum(statName, historicalDays, logName, filter, fieldName) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initCount",
+ update_f: "_updateSum",
+ snapshot_f: "_snapshotTotal",
+ options: { filter: filter, fieldName: fieldName },
+ historicalDays: historicalDays || 1
+ });
+}
+
+function addUniquenessCount(statName, historicalDays, logName, filter, fieldName) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initUniques",
+ update_f: "_updateUniquenessCount",
+ snapshot_f: "_snapshotTotal",
+ options: { filter: filter, fieldName: fieldName },
+ historicalDays: historicalDays || 1
+ })
+}
+
+function addTopValuesStat(statName, historicalDays, logName, filter, fieldName, canonicalizer) {
+ addStat({
+ name: statName,
+ dataType: "topValues",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initTopValues",
+ update_f: "_updateTopValues",
+ snapshot_f: "_snapshotTopValues",
+ options: { filter: filter, fieldName: fieldName, canonicalizer: canonicalizer },
+ historicalDays: historicalDays || 1
+ });
+}
+
+function addLatenciesStat(statName, historicalDays, logName, filter, fieldName, latencyFieldName) {
+ addStat({
+ name: statName,
+ dataType: "topValues",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initLatencies",
+ update_f: "_updateLatencies",
+ snapshot_f: "_snapshotLatencies",
+ options: { filter: filter, fieldName: fieldName, latencyFieldName: latencyFieldName },
+ historicalDays: historicalDays || 1
+ });
+}
+
+
+// RETURNING USERS
+
+function _initReturningUsers(statName, options, timescaleType) {
+ return { cache: {}, uniques: _initUniques(statName, options, timescaleType) };
+}
+
+function _returningUsersUserId(logObject) {
+ if (logObject.type == "userjoin") {
+ return logObject.userId;
+ }
+}
+
+function _returningUsersUserCreationDate(userId) {
+ var record = sqlobj.selectSingle('pad_cookie_userids', {id: userId});
+ if (record) {
+ return record.createdDate.getTime();
+ }
+}
+
+function _returningUsersAccountId(logObject) {
+ return logObject.proAccountId;
+}
+
+function _returningUsersAccountCreationDate(accountId) {
+ var record = sqlobj.selectSingle('pro_accounts', {id: accountId});
+ if (record) {
+ return record.createdDate.getTime();
+ }
+}
+
+
+function _updateReturningUsers(statName, options, logName, data, logObject) {
+ var userId = (options.useProAccountId ? _returningUsersAccountId(logObject) : _returningUsersUserId(logObject));
+ if (! userId) { return; }
+ var date = logObject.date;
+ if (! data.cache[""+userId]) {
+ var creationTime = (options.useProAccountId ? _returningUsersAccountCreationDate(userId) : _returningUsersUserCreationDate(userId));
+ if (! creationTime) { return; } // hm. weird case.
+ data.cache[""+userId] = creationTime;
+ }
+ if (data.cache[""+userId] < date - options.registeredNDaysAgo*24*60*60*1000) {
+ data.uniques.hit(logObject.date, ""+userId);
+ }
+}
+function _snapshotReturningUsers(statName, options, data) {
+ return _lazySnapshot(data.uniques);
+}
+
+function addReturningUserStat(statName, pastNDays, registeredNDaysAgo) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: ["frontend/padevents"],
+ init_f: "_initReturningUsers",
+ update_f: "_updateReturningUsers",
+ snapshot_f: "_snapshotReturningUsers",
+ options: { registeredNDaysAgo: registeredNDaysAgo },
+ historicalDays: pastNDays
+ });
+}
+
+function addReturningProAccountStat(statName, pastNDays, registeredNDaysAgo) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: ["frontend/request"],
+ init_f: "_initReturningUsers",
+ update_f: "_updateReturningUsers",
+ snapshot_f: "_snapshotReturningUsers",
+ options: { registeredNDaysAgo: registeredNDaysAgo, useProAccountId: true },
+ historicalDays: pastNDays
+ });
+}
+
+
+function addDisconnectStat() {
+ addStat({
+ name: "streaming_disconnects",
+ dataType: "topValues",
+ logNames: ["frontend/padevents", "frontend/reconnect", "frontend/disconnected_autopost"],
+ init_f: "_initDisconnectTracker",
+ update_f: "_updateDisconnectTracker",
+ snapshot_f: "_snapshotDisconnectTracker",
+ options: {}
+ });
+}
+
+// PAD STARTUP LATENCY
+function _initPadStartupLatency(statName, options, timescaleType) {
+ return {
+ recentGets: (timescaleType == LIVE ? new ExpiringMapping(60*1000) : _fakeMap()),
+ latencies: _initHistogram(statName, options, timescaleType),
+ }
+}
+
+function _updatePadStartupLatency(statName, options, logName, data, logObject) {
+ var session = logObject.session;
+ if (logName == "frontend/request") {
+ if (! ('padId' in logObject)) { return; }
+ var padId = logObject.padId;
+ if (! data.recentGets.get(session)) {
+ data.recentGets.put(session, {});
+ }
+ data.recentGets.get(session)[padId] = logObject.date;
+ }
+ if (logName == "frontend/padevents") {
+ if (logObject.type != 'userjoin') { return; }
+ if (! data.recentGets.get(session)) { return; }
+ var padId = logObject.padId;
+ var getTime = data.recentGets.get(session)[padId];
+ if (! getTime) { return; }
+ delete data.recentGets.get(session)[padId];
+ var latency = logObject.date - getTime;
+ if (latency < 60*1000) {
+ // latencies longer than 60 seconds don't represent data we care about for this stat.
+ data.latencies.hit(logObject.date, latency);
+ }
+ }
+}
+
+function _snapshotPadStartupLatency(statName, options, data) {
+ var latencies = data.latencies;
+ function convertHistogram(histogram_f) {
+ var deciles = {};
+ [0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100].forEach(function(pct) {
+ deciles[""+pct] = histogram_f(pct);
+ });
+ return deciles;
+ }
+ return _lazySnapshot({
+ latencies: latencies,
+ get total() {
+ return convertHistogram(this.latencies.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return this.latencies.history(bucketsPerSample, numSamples).map(convertHistogram);
+ },
+ latest: function(bucketsPerSample) {
+ return convertHistogram(this.latencies.latest(bucketsPerSample));
+ }
+ });
+}
+
+function addPadStartupLatencyStat() {
+ addStat({
+ name: "pad_startup_times",
+ dataType: "histogram",
+ logNames: ["frontend/padevents", "frontend/request"],
+ init_f: "_initPadStartupLatency",
+ update_f: "_updatePadStartupLatency",
+ snapshot_f: "_snapshotPadStartupLatency",
+ options: {}
+ });
+}
+
+
+function _initSampleTracker(statName, options, timescaleType) {
+ return {
+ samples: Array(1440), // 1 hour at 1 sample/minute
+ nextSample: 0,
+ numSamples: 0
+ }
+}
+
+function _updateSampleTracker(statName, options, logName, data, logObject) {
+ if (options.filter && ! options.filter(logObject)) {
+ return;
+ }
+ if (options.fieldName && ! (options.fieldName in logObject)) {
+ return;
+ }
+ data.samples[data.nextSample] = (options.fieldName ? logObject[fieldName] : logObject);
+ data.nextSample++;
+ data.nextSample %= data.samples.length;
+ data.numSamples = Math.min(data.samples.length, data.numSamples+1);
+}
+
+function _snapshotSampleTracker(statName, options, data) {
+ function indexTransform(i) {
+ return (data.nextSample-data.numSamples+i + data.samples.length) % data.samples.length;
+ }
+ var merge_f = options.mergeFunction || function(a, b) { return a+b; }
+ var process_f = options.processFunction || function(a) { return a; }
+ function mergeValues(values) {
+ if (values.length <= 1) { return values[0]; }
+ var t = values[0];
+ for (var i = 1; i < values.length; ++i) {
+ t = merge_f(values[i], t);
+ }
+ return t;
+ }
+ return _lazySnapshot({
+ get total() {
+ var t = [];
+ for (var i = 0; i < data.numSamples; ++i) {
+ t.push(data.samples[indexTransform(i)]);
+ }
+ return process_f(mergeValues(t), t.length);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ var allSamples = [];
+ for (var i = data.numSamples-1; i >= Math.max(0, data.numSamples - bucketsPerSample*numSamples); --i) {
+ allSamples.push(data.samples[indexTransform(i)]);
+ }
+ var out = [];
+ for (var i = 0; i < numSamples && i*bucketsPerSample < allSamples.length; ++i) {
+ var subArray = [];
+ for (var j = 0; j < bucketsPerSample && i*bucketsPerSample+j < allSamples.length; ++j) {
+ subArray.push(allSamples[i*bucketsPerSample+j]);
+ }
+ out.push(process_f(mergeValues(subArray), subArray.length));
+ }
+ return out.reverse();
+ },
+ latest: function(bucketsPerSample) {
+ var t = [];
+ for (var i = data.numSamples-1; i >= Math.max(0, data.numSamples-bucketsPerSample); --i) {
+ t.push(data.samples[indexTransform(i)]);
+ }
+ return process_f(mergeValues(t), t.length);
+ }
+ });
+}
+
+function addSampleTracker(statName, logName, filter, fieldName, mergeFunction, processFunction) {
+ addStat({
+ name: statName,
+ dataType: "histogram",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initSampleTracker",
+ update_f: "_updateSampleTracker",
+ snapshot_f: "_snapshotSampleTracker",
+ options: { filter: filter, fieldName: fieldName,
+ mergeFunction: mergeFunction, processFunction: processFunction }
+ });
+}
+
+function addCometLatencySampleTracker(statName) {
+ addSampleTracker(statName, "backend/server-events", typeMatcher("streaming-message-latencies"), null,
+ function(a, b) {
+ var ret = {};
+ ["count", "p50", "p90", "p95", "p99", "max"].forEach(function(key) {
+ ret[key] = (Number(a[key]) || 0) + (Number(b[key]) || 0);
+ });
+ return ret;
+ },
+ function(v, count) {
+ if (count == 0) {
+ return {
+ "50": 0, "90": 0, "95": 0, "99": 0, "100": 0
+ }
+ }
+ var ret = {count: v.count};
+ ["p50", "p90", "p95", "p99", "max"].forEach(function(key) {
+ ret[key] = (Number(v[key]) || 0)/(Number(count) || 1);
+ });
+ return {"50": Math.round(ret.p50/1000),
+ "90": Math.round(ret.p90/1000),
+ "95": Math.round(ret.p95/1000),
+ "99": Math.round(ret.p99/1000),
+ "100": Math.round(ret.max/1000)};
+ });
+}
+
+function addConnectionTypeSampleTracker(statName) {
+ var caredAboutFields = ["streaming", "longpolling", "shortpolling", "(unconnected)"];
+
+ addSampleTracker(statName, "backend/server-events", typeMatcher("streaming-connection-count"), null,
+ function(a, b) {
+ var ret = {};
+ caredAboutFields.forEach(function(k) {
+ ret[k] = (Number(a[k]) || 0) + (Number(b[k]) || 0);
+ });
+ return ret;
+ },
+ function(v, count) {
+ if (count == 0) {
+ return _convertTopValue({total: 0, counts: {}});
+ }
+ var values = {};
+ var total = 0;
+ caredAboutFields.forEach(function(k) {
+ values[k] = Math.round((Number(v[k]) || 0)/count);
+ total += values[k];
+ });
+ values["Total"] = total;
+ return _convertTopValue({
+ total: Math.round(total),
+ counts: values
+ });
+ });
+}
+
+// helpers for filter functions
+
+function expectedHostnames() {
+ var hostPart = appjet.config.listenHost || "localhost";
+ if (appjet.config.listenSecureHost != hostPart) {
+ hostPart = "("+hostPart+"|"+(appjet.config.listenSecureHost || "localhost")+")";
+ }
+ var ports = [];
+ if (appjet.config.listenPort != 80) {
+ ports.push(""+appjet.config.listenPort);
+ }
+ if (appjet.config.listenSecurePort != 443) {
+ ports.push(""+appjet.config.listenSecurePort);
+ }
+ var portPart = (ports.length > 0 ? ":("+ports.join("|")+")" : "");
+ return hostPart + portPart;
+}
+
+function fieldMatcher(fieldName, fieldValue) {
+ if (fieldValue instanceof RegExp) {
+ return function(logObject) {
+ return fieldValue.test(logObject[fieldName]);
+ }
+ } else {
+ return function(logObject) {
+ return logObject[fieldName] == fieldValue;
+ }
+ }
+}
+
+function typeMatcher(type) {
+ return fieldMatcher("type", type);
+}
+
+function invertMatcher(f) {
+ return function(logObject) {
+ return ! f(logObject);
+ }
+}
+
+function setupStatsCollector() {
+ var c;
+
+ function unwatchLog(logName) {
+ GenericLoggerUtils.clearWrangler(logName.split('/')[1], c.wranglers[logName]);
+ }
+ function watchLog(logName) {
+ c.wranglers[logName] = new Packages.net.appjet.oui.LogWrangler({
+ tell: function(lpb) {
+ c.queue.add({logName: logName, json: lpb.json()});
+ }
+ });
+ c.wranglers[logName].watch(logName.split('/')[1]);
+ }
+
+ c = _stats().liveCollector;
+ if (c) {
+ c.watchedLogs.forEach(unwatchLog);
+ delete c.wrangler;
+ } else {
+ c = _stats().liveCollector = {};
+ }
+ c.watchedLogs = keys(_stats().logNamesToInterestedStatNames);
+ c.queue = new java.util.concurrent.ConcurrentLinkedQueue();
+ c.wranglers = {};
+ c.watchedLogs.forEach(watchLog);
+
+ if (! c.updateTask || c.updateTask.isDone()) {
+ c.updateTask = execution.scheduleTask('statistics', "statisticsLiveUpdate", 2000, []);
+ }
+}
+
+serverhandlers.tasks.statisticsLiveUpdate = function() {
+ var c = _stats().liveCollector;
+ try {
+ while (true) {
+ var obj = c.queue.poll();
+ if (obj != null) {
+ var statNames =
+ keys(_stats().logNamesToInterestedStatNames[obj.logName]);
+ var logObject = fastJSON.parse(obj.json);
+ statNames.forEach(function(statName) {
+ var statObject = getStatData(statName);
+ _callFunction(statObject.update_f,
+ statName, statObject.options, obj.logName, statObject.data, logObject);
+ });
+ } else {
+ break;
+ }
+ }
+ } catch (e) {
+ println("EXCEPTION IN LIVE UPDATE: "+e+" / "+e.fileName+":"+e.lineNumber)
+ println(exceptionutils.getStackTracePlain(new net.appjet.bodylock.JSRuntimeException(String(e), e.javaException || e.rhinoException)));
+ } finally {
+ c.updateTask = execution.scheduleTask('statistics', "statisticsLiveUpdate", 2000, []);
+ }
+}
+
+function onReset() {
+ // this gets refilled every reset.
+ _stats().logNamesToInterestedStatNames = {};
+
+ // we'll want to keep around the live data, though, so this is conditionally set.
+ if (! _stats().stats) {
+ _stats().stats = {};
+ }
+
+ addSimpleCount("site_pageviews", 1, "frontend/request", null);
+ addUniquenessCount("site_unique_ips", 1, "frontend/request", null, "clientAddr");
+
+ addUniquenessCount("active_user_ids", 1, "frontend/padevents", typeMatcher("userjoin"), "userId");
+ addUniquenessCount("active_user_ids_7days", 7, "frontend/padevents", typeMatcher("userjoin"), "userId");
+ addUniquenessCount("active_user_ids_30days", 30, "frontend/padevents", typeMatcher("userjoin"), "userId");
+
+ addUniquenessCount("active_pro_accounts", 1, "frontend/request", invertMatcher(fieldMatcher("proAccountId", undefined)),
+ "proAccountId");
+ addUniquenessCount("active_pro_accounts_7days", 7, "frontend/request", invertMatcher(fieldMatcher("proAccountId", undefined)),
+ "proAccountId");
+ addUniquenessCount("active_pro_accounts_30days", 30, "frontend/request", invertMatcher(fieldMatcher("proAccountId", undefined)),
+ "proAccountId");
+
+
+ addUniquenessCount("active_pads", 1, "frontend/padevents", typeMatcher("userjoin"), "padId");
+ addSimpleCount("new_pads", 1, "frontend/padevents", typeMatcher("newpad"));
+
+ addSimpleCount("chat_messages", 1, "frontend/chat", null);
+ addUniquenessCount("active_chatters", 1, "frontend/chat", null, "userId");
+
+ addSimpleCount("exceptions", 1, "frontend/exception", null);
+
+ addSimpleCount("eepnet_trial_downloads", 1, "frontend/eepnet_download_info", null);
+
+ addSimpleSum("revenue", 1, "frontend/billing", typeMatcher("purchase-complete"), "dollars")
+
+ var hostRegExp = new RegExp("^https?:\\/\\/([-a-zA-Z0-9]+.)?"+expectedHostnames()+"\\/");
+ addTopValuesStat("top_referers", 1, "frontend/request",
+ invertMatcher(fieldMatcher(
+ "referer", hostRegExp)),
+ "referer");
+
+ addTopValuesStat("paths_404", 1, "frontend/request", fieldMatcher("statusCode", 404), "path");
+ addTopValuesStat("paths_500", 1, "frontend/request", fieldMatcher("statusCode", 500), "path");
+ addTopValuesStat("paths_exception", 1, "frontend/exception", null, "path");
+
+ addTopValuesStat("top_exceptions", 1, ["frontend/exception", "backend/exceptions"],
+ invertMatcher(fieldMatcher("trace", undefined)),
+ "trace", function(trace) {
+ var jstrace = trace.split("\n").filter(function(line) {
+ return /^\tat JS\$.*?\.js:\d+\)$/.test(line);
+ });
+ if (jstrace.length > 3) {
+ return "JS Exception:\n"+jstrace.slice(0, 10).join("\n").replace(/\t[^\(]*/g, "");
+ }
+ return trace.split("\n").slice(1, 10).join("\n").replace(/\t/g, "");
+ });
+
+ addReturningUserStat("users_1day_returning_7days", 1, 7);
+ addReturningUserStat("users_7day_returning_7days", 7, 7);
+ addReturningUserStat("users_30day_returning_7days", 30, 7);
+
+ addReturningUserStat("users_1day_returning_30days", 1, 30);
+ addReturningUserStat("users_7day_returning_30days", 7, 30);
+ addReturningUserStat("users_30day_returning_30days", 30, 30);
+
+ addReturningProAccountStat("pro_accounts_1day_returning_7days", 1, 7);
+ addReturningProAccountStat("pro_accounts_7day_returning_7days", 7, 7);
+ addReturningProAccountStat("pro_accounts_30day_returning_7days", 30, 7);
+
+ addReturningProAccountStat("pro_accounts_1day_returning_30days", 1, 30);
+ addReturningProAccountStat("pro_accounts_7day_returning_30days", 7, 30);
+ addReturningProAccountStat("pro_accounts_30day_returning_30days", 30, 30);
+
+
+ addDisconnectStat();
+ addTopValuesStat("disconnect_causes", 1, "frontend/avoidable_disconnects", null, "errorMessage");
+
+ var staticFileRegExp = /^\/static\/|^\/favicon.ico/;
+ addLatenciesStat("execution_latencies", 1, "backend/latency",
+ invertMatcher(fieldMatcher('path', staticFileRegExp)),
+ "path", "time");
+ addLatenciesStat("static_file_latencies", 1, "backend/latency",
+ fieldMatcher('path', staticFileRegExp),
+ "path", "time");
+
+ addUniquenessCount("disconnects_with_clientside_errors", 1,
+ ["frontend/reconnect", "frontend/disconnected_autopost"],
+ fieldMatcher("hasClientErrors", true), "uniqueId");
+
+ addTopValuesStat("imports_exports_counts", 1, "frontend/import-export",
+ typeMatcher("request"), "direction");
+
+ addPadStartupLatencyStat();
+
+ addCometLatencySampleTracker("streaming_latencies");
+ addConnectionTypeSampleTracker("streaming_connections");
+ // TODO: add more stats here.
+
+ setupStatsCollector();
+}
+
+//----------------------------------------------------------------
+// Log processing
+//----------------------------------------------------------------
+
+function _whichStats(statNames) {
+ var whichStats = _statData();
+ var logNamesToInterestedStatNames = _stats().logNamesToInterestedStatNames;
+
+ if (statNames) {
+ whichStats = {};
+ statNames.forEach(function(statName) { whichStats[statName] = getStatData(statName) });
+ logNamesToInterestedStatNames = _generateLogInterestMap(statNames);
+ }
+
+ return [whichStats, logNamesToInterestedStatNames];
+}
+
+function _initStatDataMap(statNames) {
+ var [whichStats, logNamesToInterestedStatNames] = _whichStats(statNames);
+
+ var statDataMap = {};
+
+ function initStat(statName, statObject) {
+ statDataMap[statName] =
+ _callFunction(statObject.init_f, statName, statObject.options, HIST);
+ }
+ eachProperty(whichStats, initStat);
+
+ return statDataMap;
+}
+
+function _saveStats(day, statDataMap, statNames) {
+ var [whichStats, logNamesToInterestedStatNames] = _whichStats(statNames);
+
+ function saveStat(statName, statObject) {
+ var value = _callFunction(statObject.snapshot_f,
+ statName, statObject.options, statDataMap[statName]).total;
+ if (typeof(value) != 'object') {
+ value = {value: value};
+ }
+ _saveStat(day, statName, value);
+ }
+ eachProperty(whichStats, saveStat);
+}
+
+function _processSingleDayLogs(day, logNamesToInterestedStatNames, statDataMap) {
+ var iterators = {};
+ keys(logNamesToInterestedStatNames).forEach(function(logName) {
+ var [prefix, logId] = logName.split("/");
+ var fileName = log.logFileName(prefix, logId, day);
+ if (! fileName) {
+ _info("No such file: "+logName+" on day "+day);
+ return;
+ }
+ iterators[logName] = fileLineIterator(fileName);
+ });
+
+ var numIterators = keys(iterators).length;
+ if (numIterators == 0) {
+ _info("No logs to process on day "+day);
+ return;
+ }
+ var sortedLogObjects = new java.util.PriorityQueue(numIterators,
+ new java.util.Comparator({
+ compare: function(o1, o2) { return o1.logObject.date - o2.logObject.date }
+ }));
+
+ function lineToLogObject(logName, json) {
+ return {logName: logName, logObject: fastJSON.parse(json)};
+ }
+
+ // begin by filling the queue with one object from each log.
+ eachProperty(iterators, function(logName, iterator) {
+ if (iterator.hasNext) {
+ sortedLogObjects.add(lineToLogObject(logName, iterator.next));
+ }
+ });
+
+ // update with all log objects, in date order (enforced by priority queue).
+ while (! sortedLogObjects.isEmpty()) {
+ var nextObject = sortedLogObjects.poll();
+ var logName = nextObject.logName;
+
+ keys(logNamesToInterestedStatNames[logName]).forEach(function(statName) {
+ var statObject = getStatData(statName);
+ _callFunction(statObject.update_f,
+ statName, statObject.options, logName, statDataMap[statName], nextObject.logObject);
+ });
+
+ // get next entry from this log, if there is one.
+ if (iterators[logName].hasNext) {
+ sortedLogObjects.add(lineToLogObject(logName, iterators[logName].next));
+ }
+ }
+}
+
+function processStatsForDay(day, statNames, statDataMap) {
+ var [whichStats, logNamesToInterestedStatNames] = _whichStats(statNames);
+
+ // process the logs, notifying the right statistics updaters.
+ _processSingleDayLogs(day, logNamesToInterestedStatNames, statDataMap);
+}
+
+//----------------------------------------------------------------
+// Daily update
+//----------------------------------------------------------------
+serverhandlers.tasks.statisticsDailyUpdate = function() {
+// do nothing for now.
+
+// dailyUpdate();
+};
+
+function _scheduleNextDailyUpdate() {
+ // Run at 1:11am every day
+ var now = +(new Date);
+ var tomorrow = new Date(now + 1000*60*60*24);
+ tomorrow.setHours(1);
+ tomorrow.setMinutes(11);
+ tomorrow.setMilliseconds(111);
+ log.info("Scheduling next daily statistics update for: "+tomorrow.toString());
+ var delay = +tomorrow - (+(new Date));
+ execution.scheduleTask("statistics", "statisticsDailyUpdate", delay, []);
+}
+
+function processStatsAsOfDay(date, statNames) {
+ var latestDay = noon(new Date(date - 1000*60*60*24));
+
+ _processLogsForNeededDays(latestDay, statNames);
+}
+
+function _processLogsForNeededDays(latestDay, statNames) {
+ if (! statNames) {
+ statNames = getAllStatNames();
+ }
+ var statDataMap = _initStatDataMap(statNames);
+
+ var agesToStats = [];
+ var atLeastOneStat = true;
+ for (var i = 0; atLeastOneStat; ++i) {
+ atLeastOneStat = false;
+ agesToStats[i] = [];
+ statNames.forEach(function(statName) {
+ var statData = getStatData(statName);
+ if (statData.historicalDays > i) {
+ atLeastOneStat = true;
+ agesToStats[i].push(statName);
+ }
+ });
+ }
+ agesToStats.pop();
+
+ for (var i = agesToStats.length-1; i >= 0; --i) {
+ var day = new Date(+latestDay - i*24*60*60*1000);
+ processStatsForDay(day, agesToStats[i], statDataMap);
+ }
+ _saveStats(latestDay, statDataMap, statNames);
+}
+
+function doDailyUpdate(date) {
+ var now = (date === undefined ? new Date() : date);
+ var yesterdayNoon = noon(new Date(+now - 1000*60*60*24));
+
+ _processLogsForNeededDays(yesterdayNoon);
+}
+
+function dailyUpdate() {
+ try {
+ doDailyUpdate();
+ } catch (ex) {
+ log.warn("statistics.dailyUpdate() failed: "+ex.toString());
+ } finally {
+ _scheduleNextDailyUpdate();
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/store/checkout.js b/etherpad/src/etherpad/store/checkout.js
new file mode 100644
index 0000000..2a4d7e7
--- /dev/null
+++ b/etherpad/src/etherpad/store/checkout.js
@@ -0,0 +1,300 @@
+/**
+ * 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.
+ */
+
+import("dateutils");
+import("email.sendEmail");
+import("jsutils.*");
+import("sqlbase.sqlobj");
+import("stringutils");
+import("sync");
+
+import("etherpad.globals");
+import("etherpad.globals.*");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+
+import("static.js.billing_shared.{billing=>billingJS}");
+
+function dollars(x, nocommas) {
+ if (! x) { return "0.00"; }
+ var s = String(x);
+ var dollars = s.split('.')[0];
+ var pennies = s.split('.')[1];
+
+ if (!dollars) {
+ dollars = "0";
+ }
+
+ if (!nocommas && dollars.length > 3) {
+ var newDollars = [];
+ newDollars.push(dollars[dollars.length-1]);
+
+ for (var i = 1; i < dollars.length; ++i) {
+ if (i % 3 == 0) {
+ newDollars.push(",");
+ }
+ newDollars.push(dollars[dollars.length-1-i]);
+ }
+ dollars = newDollars.reverse().join('');
+ }
+
+ if (!pennies) {
+ pennies = "00";
+ }
+
+ if (pennies.length == 1) {
+ pennies = pennies + "0";
+ }
+
+ if (pennies.length > 2) {
+ pennies = pennies.substr(0,2);
+ }
+
+ return [dollars,pennies].join('.');
+}
+
+function obfuscateCC(x) {
+ if (x.length == 16 || x.length == 15) {
+ return stringutils.repeat("X", x.length-4) + x.substr(-4);
+ } else {
+ return x;
+ }
+}
+
+
+// validation functions
+
+function isOnlyDigits(s) {
+ return /^[0-9]+$/.test(s);
+}
+
+function isOnlyLettersAndSpaces(s) {
+ return /^[a-zA-Z ]+$/.test(s);
+}
+
+function isLength(s, minLen, maxLen) {
+ if (maxLen === undefined) {
+ return (typeof(s) == 'string' && s.length == minLen);
+ } else {
+ return (typeof(s) == 'string' && s.length >= minLen && s.length <= maxLen);
+ }
+}
+
+function errorMissing(validationError, name, description) {
+ validationError(name, "Please enter a "+description+".");
+}
+
+function errorTooSomething(validationError, name, description, max, tooWhat, betterAdjective) {
+ validationError(name, "Your "+description+" is too " + tooWhat + "; please provide a "+description+
+ " that is "+max+" characters or "+betterAdjective);
+}
+
+function validateString(validationError, s, name, description, mustExist, maxLength, minLength) {
+ if (mustExist && ! s) {
+ errorMissing(validationError, name, description);
+ }
+ if (s && s.length > maxLength) {
+ errorTooSomething(validationError, name, description, maxLength, "long", "shorter");
+ }
+ if (minLength > 0 && s.length < minLength) {
+ errorTooSomething(validationError, name, description, minLength, "short", "longer");
+ }
+}
+
+function validateZip(validationError, s) {
+ if (! s) {
+ errorMissing(validationError, 'billingZipCode', "ZIP code");
+ }
+ if (! (/^\d{5}(-\d{4})?$/.test(s))) {
+ validationError('billingZipCode', "Please enter a valid ZIP code");
+ }
+}
+
+function validateBillingCart(validationError, cart) {
+ var p = cart;
+
+ if (! isOnlyLettersAndSpaces(p.billingFirstName)) {
+ validationError("billingFirstName", "Name fields may only contain alphanumeric characters.");
+ }
+
+ if (! isOnlyLettersAndSpaces(p.billingLastName)) {
+ validationError("billingLastName", "Name fields may only contain alphanumeric characters.");
+ }
+
+ var validPurchaseTypes = arrayToSet(['creditcard', 'invoice', 'paypal']);
+ if (! p.billingPurchaseType in validPurchaseTypes) {
+ validationError("billingPurchaseType", "Please select a valid purchase type.")
+ }
+
+ switch (p.billingPurchaseType) {
+ case 'creditcard':
+ if (! billingJS.validateCcNumber(p.billingCCNumber)) {
+ validationError("billingCCNumber", "Your card number doesn't appear to be valid.");
+ }
+ if (! isOnlyDigits(p.billingExpirationMonth) ||
+ ! isLength(p.billingExpirationMonth, 1, 2)) {
+ validationError("billingMeta", "Invalid expiration month.");
+ }
+ if (! isOnlyDigits(p.billingExpirationYear) ||
+ ! isLength(p.billingExpirationYear, 1, 2)) {
+ validationError("billingMeta", "Invalid expiration year.");
+ }
+ if (Number("20"+p.billingExpirationYear) <= (new Date()).getFullYear() &&
+ Number(p.billingExpirationMonth) < (new Date()).getMonth()+1) {
+ validationError("billingMeta", "Invalid expiration date.");
+ }
+ var ccType = billingJS.getCcType(p.billingCCNumber);
+ if (! isOnlyDigits(p.billingCSC) ||
+ ! isLength(p.billingCSC, (ccType == 'amex' ? 4 : 3))) {
+ validationError("billingMeta", "Invalid CSC.");
+ }
+ // falling through here!
+ case 'invoice':
+ validateString(validationError, p.billingCountry, "billingCountry", "country name", true, 2);
+ validateString(validationError, p.billingAddressLine1, "billingAddressLine1", "billing address", true, 100);
+ validateString(validationError, p.billingAddressLine2, "billingAddressLine2", "billing address", false, 100);
+ validateString(validationError, p.billingCity, "billingCity", "city name", true, 40);
+ if (p.billingCountry == "US") {
+ validateString(validationError, p.billingState, "billingState", "state name", true, 2);
+ validateZip(validationError, p.billingZipCode);
+ } else {
+ validateString(validationError, p.billingProvince, "billingProvince", "province name", true, 40, 1);
+ validateString(validationError, p.billingPostalCode, "billingPostalCode", "postal code", true, 20, 5);
+ }
+ }
+}
+
+function _cardType(number) {
+ var cardType = billingJS.getCcType(number);
+ switch (cardType) {
+ case 'visa':
+ return "Visa";
+ case 'amex':
+ return "Amex";
+ case 'disc':
+ return "Discover";
+ case 'mc':
+ return "MasterCard";
+ }
+}
+
+function generatePayInfo(cart) {
+ var isUs = cart.billingCountry == "US";
+
+ var payInfo = {
+ cardType: _cardType(cart.billingCCNumber),
+ cardNumber: cart.billingCCNumber,
+ cardExpiration: ""+cart.billingExpirationMonth+"20"+cart.billingExpirationYear,
+ cardCvv: cart.billingCSC,
+
+ nameSalutation: "",
+ nameFirst: cart.billingFirstName,
+ nameMiddle: "",
+ nameLast: cart.billingLastName,
+ nameSuffix: "",
+
+ addressStreet: cart.billingAddressLine1,
+ addressStreet2: cart.billingAddressLine2,
+ addressCity: cart.billingCity,
+ addressState: (isUs ? cart.billingState : cart.billingProvince),
+ addressZip: (isUs ? cart.billingZipCode : cart.billingPostalCode),
+ addressCountry: cart.billingCountry
+ }
+
+ return payInfo;
+}
+
+var billingCartFieldMap = {
+ cardType: {f: ["billingCCNumber"], d: "credit card number"},
+ cardNumber: { f: ["billingCCNumber"], d: "credit card number"},
+ cardExpiration: { f: ["billingMeta", "billingMeta"], d: "expiration date" },
+ cardCvv: { f: ["billingMeta"], d: "card security code" },
+ card: { f: ["billingCCNumber", "billingMeta"], d: "credit card"},
+ nameFirst: { f: ["billingFirstName"], d: "first name" },
+ nameLast: {f: ["billingLastName"], d: "last name" },
+ addressStreet: { f: ["billingAddressLine1"], d: "billing address" },
+ addressStreet2: { f: ["billingAddressLine2"], d: "billing address" },
+ addressCity: { f: ["billingCity"], d: "city" },
+ addressState: { f: ["billingState", "billingProvince"], d: "state or province" },
+ addressCountry: { f: ["billingCountry"], d: "country" },
+ addressZip: { f: ["billingZipCode", "billingPostalCode"], d: "ZIP or postal code" },
+ address: { f: ["billingAddressLine1", "billingAddressLine2", "billingCity", "billingState", "billingCountry", "billingZipCode"], d: "address" }
+}
+
+function validateErrorFields(validationError, errorPrefix, fieldList) {
+ if (fieldList.length > 0) {
+ var errorMsg;
+ var errorFields;
+ errorMsg = errorPrefix +
+ fieldList.map(function(field) { return billingCartFieldMap[field].d }).join(", ") +
+ ".";
+ errorFields = [];
+ fieldList.forEach(function(field) {
+ errorFields = errorFields.concat(billingCartFieldMap[field].f);
+ });
+ validationError(errorFields, errorMsg);
+ }
+}
+
+function guessBillingNames(cart, name) {
+ if (! cart.billingFirstName && ! cart.billingLastName) {
+ var nameParts = name.split(/\s+/);
+ if (nameParts.length == 1) {
+ cart.billingFirstName = nameParts[0];
+ } else {
+ cart.billingLastName = nameParts[nameParts.length-1];
+ cart.billingFirstName = nameParts.slice(0, nameParts.length-1).join(' ');
+ }
+ }
+}
+
+function writeToEncryptedLog(s) {
+ if (! appjet.config["etherpad.billingEncryptedLog"]) {
+ // no need to log, this probably isn't the live server.
+ return;
+ }
+ var e = net.appjet.oui.Encryptomatic;
+ sync.callsyncIfTrue(appjet.cache,
+ function() { return ! appjet.cache.billingEncryptedLog },
+ function() {
+ appjet.cache.billingEncryptedLog = {
+ writer: new java.io.FileWriter(appjet.config["etherpad.billingEncryptedLog"], true),
+ key: e.readPublicKey("RSA", new java.io.FileInputStream(appjet.config["etherpad.billingPublicKey"]))
+ }
+ });
+ var l = appjet.cache.billingEncryptedLog;
+ sync.callsync(l, function() {
+ l.writer.write(e.bytesToAscii(e.encrypt(
+ new java.io.ByteArrayInputStream((new java.lang.String(s)).getBytes("UTF-8")),
+ l.key))+"\n");
+ l.writer.flush();
+ })
+}
+
+function formatExpiration(expiration) {
+ return dateutils.shortMonths[Number(expiration.substr(0, 2))-1]+" "+expiration.substr(2);
+}
+
+function formatDate(date) {
+ return dateutils.months[date.getMonth()]+" "+date.getDate()+", "+date.getFullYear();
+}
+
+function salesEmail(to, from, subject, headers, body) {
+ sendEmail(to, from, subject, headers, body);
+ if (globals.isProduction()) {
+ sendEmail("sales@pad.spline.inf.fu-berlin.de", from, subject, headers, body);
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/store/eepnet_checkout.js b/etherpad/src/etherpad/store/eepnet_checkout.js
new file mode 100644
index 0000000..62137d3
--- /dev/null
+++ b/etherpad/src/etherpad/store/eepnet_checkout.js
@@ -0,0 +1,101 @@
+/**
+ * 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.
+ */
+
+import("email.sendEmail");
+import("sqlbase.sqlobj");
+import("stringutils");
+
+import("etherpad.globals");
+import("etherpad.globals.*");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+import("etherpad.store.checkout.*");
+
+var COST_PER_USER = 99;
+var SUPPORT_COST_PCT = 20;
+var SUPPORT_MIN_COST = 50;
+
+function getPurchaseByEmail(email) {
+ return sqlobj.selectSingle('checkout_purchase', {email: email});
+}
+
+function hasEmailAlreadyPurchased(email) {
+ var purchase = getPurchaseByEmail(email);
+ return purchase && purchase.licenseKey ? true : false;
+}
+
+function mailLostLicense(email) {
+ var purchase = getPurchaseByEmail(email);
+ if (purchase && purchase.licenseKey) {
+ sendLicenseEmail({
+ email: email,
+ ownerName: purchase.owner,
+ orgName: purchase.organization,
+ licenseKey: purchase.licenseKey
+ });
+ }
+}
+
+function _updatePurchaseWithKey(id, key) {
+ sqlobj.updateSingle('checkout_purchase', {id: id}, {licenseKey: key});
+}
+
+function updatePurchaseWithReceipt(id, text) {
+ sqlobj.updateSingle('checkout_purchase', {id: id}, {receiptEmail: text});
+}
+
+function getPurchaseByInvoiceId(id) {
+ sqlobj.selectSingle('checkout_purchase', {invoiceId: id});
+}
+
+function generateLicenseKey(cart) {
+ var licenseKey = licensing.generateNewKey(cart.ownerName, cart.orgName, null, 2, cart.userCount);
+ cart.licenseKey = licenseKey;
+ _updatePurchaseWithKey(cart.customerId, cart.licenseKey);
+ return licenseKey;
+}
+
+function receiptEmailText(cart) {
+ return renderTemplateAsString('email/eepnet_purchase_receipt.ejs', {
+ cart: cart,
+ dollars: dollars,
+ obfuscateCC: obfuscateCC
+ });
+}
+
+function licenseEmailText(userName, licenseKey) {
+ return renderTemplateAsString('email/eepnet_license_info.ejs', {
+ userName: userName,
+ licenseKey: licenseKey,
+ isEvaluation: false
+ });
+}
+
+function sendReceiptEmail(cart) {
+ var receipt = cart.receiptEmail || receiptEmailText(cart);
+
+ salesEmail(cart.email, "sales@pad.spline.inf.fu-berlin.de",
+ "EtherPad: Receipt for "+cart.ownerName+" ("+cart.orgName+")",
+ {}, receipt);
+}
+
+function sendLicenseEmail(cart) {
+ var licenseEmail = licenseEmailText(cart.ownerName, cart.licenseKey);
+
+ salesEmail(cart.email, "sales@pad.spline.inf.fu-berlin.de",
+ "EtherPad: License Key for "+cart.ownerName+" ("+cart.orgName+")",
+ {}, licenseEmail);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/store/eepnet_trial.js b/etherpad/src/etherpad/store/eepnet_trial.js
new file mode 100644
index 0000000..570d351
--- /dev/null
+++ b/etherpad/src/etherpad/store/eepnet_trial.js
@@ -0,0 +1,241 @@
+/**
+ * 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.
+ */
+
+import("email.sendEmail");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("execution");
+
+import("etherpad.sessions.getSession");
+import("etherpad.log");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+
+//----------------------------------------------------------------
+
+function getTrialDays() {
+ return 30;
+}
+
+function getTrialUserQuota() {
+ return 100;
+}
+
+function mailLicense(data, licenseKey, expiresDate) {
+ var toAddr = data.email;
+ if (isTestEmail(toAddr)) {
+ toAddr = "blackhole@appjet.com";
+ }
+ var subject = ('EtherPad: Trial License Information for '+
+ data.firstName+' '+data.lastName+' ('+data.orgName+')');
+
+ var emailBody = renderTemplateAsString("email/eepnet_license_info.ejs", {
+ userName: data.firstName+" "+data.lastName,
+ licenseKey: licenseKey,
+ expiresDate: expiresDate,
+ isEvaluation: true
+ });
+
+ sendEmail(
+ toAddr,
+ 'sales@pad.spline.inf.fu-berlin.de',
+ subject,
+ {},
+ emailBody
+ );
+}
+
+function mailLostLicense(email) {
+ var data = sqlobj.selectSingle('eepnet_signups', {email: email});
+ var keyInfo = licensing.decodeLicenseInfoFromKey(data.licenseKey);
+ var expiresDate = keyInfo.expiresDate;
+
+ mailLicense(data, data.licenseKey, expiresDate);
+}
+
+function hasEmailAlreadyDownloaded(email) {
+ var existingRecord = sqlobj.selectSingle('eepnet_signups', {email: email});
+ if (existingRecord) {
+ return true;
+ } else {
+ return false
+ }
+}
+
+function createAndMailNewLicense(data) {
+ sqlcommon.inTransaction(function() {
+ var expiresDate = new Date(+(new Date)+(1000*60*60*24*getTrialDays()));
+ var licenseKey = licensing.generateNewKey(
+ data.firstName + ' ' + data.lastName,
+ data.orgName,
+ +expiresDate,
+ licensing.getEditionId('PRIVATE_NETWORK_EVALUATION'),
+ getTrialUserQuota()
+ );
+
+ // confirm key
+ if (!licensing.isValidKey(licenseKey)) {
+ throw Error("License key I just created is not valid: "+l);
+ }
+
+ // Log all this precious info
+ _logDownloadData(data, licenseKey);
+
+ // Store in database
+ sqlobj.insert("eepnet_signups", {
+ firstName: data.firstName,
+ lastName: data.lastName,
+ email: data.email,
+ orgName: data.orgName,
+ jobTitle: data.jobTitle,
+ date: new Date(),
+ signupIp: String(request.clientAddr).substr(0,16),
+ estUsers: data.estUsers,
+ licenseKey: licenseKey,
+ phone: data.phone,
+ industry: data.industry
+ });
+
+ mailLicense(data, licenseKey, expiresDate);
+
+ // Send sales notification
+ var clientAddr = request.clientAddr;
+ var initialReferer = getSession().initialReferer;
+ execution.async(function() {
+ _sendSalesNotification(data, clientAddr, initialReferer);
+ });
+
+ }); // end transaction
+}
+
+function _logDownloadData(data, licenseKey) {
+ log.custom("eepnet_download_info", {
+ email: data.email,
+ firstName: data.firstName,
+ lastName: data.lastName,
+ org: data.orgName,
+ jobTitle: data.jobTitle,
+ phone: data.phone,
+ estUsers: data.estUsers,
+ licenseKey: licenseKey,
+ ip: request.clientAddr,
+ industry: data.industry,
+ referer: getSession().initialReferer
+ });
+}
+
+function getWeb2LeadData(data, ip, ref) {
+ var googleQuery = extractGoogleQuery(ref);
+ var w2ldata = {
+ oid: "00D80000000b7ey",
+ first_name: data.firstName,
+ last_name: data.lastName,
+ email: data.email,
+ company: data.orgName,
+ title: data.jobTitle,
+ phone: data.phone,
+ '00N80000003FYtG': data.estUsers,
+ '00N80000003FYto': ref,
+ '00N80000003FYuI': googleQuery,
+ lead_source: 'EEPNET Download',
+ industry: data.industry
+ };
+
+ if (!isProduction()) {
+// w2ldata.debug = "1";
+// w2ldata.debugEmail = "aaron@appjet.com";
+ }
+
+ return w2ldata;
+}
+
+function _sendSalesNotification(data, ip, ref) {
+ var hostname = ipToHostname(ip) || "unknown";
+
+ var subject = "EEPNET Trial Download: "+[data.orgName, data.firstName + ' ' + data.lastName, data.email].join(" / ");
+
+ var body = [
+ "",
+ "This is an automated message.",
+ "",
+ "Somebody downloaded a "+getTrialDays()+"-day trial of EEPNET.",
+ "",
+ "This lead should be automatically added to the AppJet salesforce account.",
+ "",
+ "Organization: "+data.orgName,
+ "Industry: "+data.industry,
+ "Full Name: "+data.firstName + ' ' + data.lastName,
+ "Job Title: "+data.jobTitle,
+ "Email: "+data.email,
+ 'Phone: '+data.phone,
+ "Est. Users: "+data.estUsers,
+ "IP Address: "+ip+" ("+hostname+")",
+ "Session Referer: "+ref,
+ ""
+ ].join("\n");
+
+ var toAddr = 'sales@pad.spline.inf.fu-berlin.de';
+ if (isTestEmail(data.email)) {
+ toAddr = 'blackhole@appjet.com';
+ }
+ sendEmail(
+ toAddr,
+ 'sales@pad.spline.inf.fu-berlin.de',
+ subject,
+ {'Reply-To': data.email},
+ body
+ );
+}
+
+function getSalesforceIndustryList() {
+ return [
+ '--None--',
+ 'Agriculture',
+ 'Apparel',
+ 'Banking',
+ 'Biotechnology',
+ 'Chemicals',
+ 'Communications',
+ 'Construction',
+ 'Consulting',
+ 'Education',
+ 'Electronics',
+ 'Energy',
+ 'Engineering',
+ 'Entertainment',
+ 'Environmental',
+ 'Finance',
+ 'Food & Beverage',
+ 'Government',
+ 'Healthcare',
+ 'Hospitality',
+ 'Insurance',
+ 'Machinery',
+ 'Manufacturing',
+ 'Media',
+ 'Not For Profit',
+ 'Other',
+ 'Recreation',
+ 'Retail',
+ 'Shipping',
+ 'Technology',
+ 'Telecommunications',
+ 'Transportation',
+ 'Utilities'
+ ];
+}
+
diff --git a/etherpad/src/etherpad/testing/testutils.js b/etherpad/src/etherpad/testing/testutils.js
new file mode 100644
index 0000000..eac7840
--- /dev/null
+++ b/etherpad/src/etherpad/testing/testutils.js
@@ -0,0 +1,23 @@
+/**
+ * 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.
+ */
+
+function assertTruthy(x) {
+ if (!x) {
+ throw new Error("assertTruthy failure: "+x);
+ }
+}
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0000_test.js b/etherpad/src/etherpad/testing/unit_tests/t0000_test.js
new file mode 100644
index 0000000..9e0e78b
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0000_test.js
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+
+
+function run() {
+ return "This is a test test.";
+}
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js b/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js
new file mode 100644
index 0000000..96a74e4
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js
@@ -0,0 +1,48 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.sqlcommon.{withConnection,inTransaction,closing}");
+import("sqlbase.sqlobj");
+
+import("etherpad.testing.testutils.*");
+
+function run() {
+
+ withConnection(function(conn) {
+ var s = conn.createStatement();
+ closing(s, function() {
+ s.execute("delete from just_a_test");
+ });
+ });
+
+ sqlobj.insert("just_a_test", {id: 1, x: "a"});
+
+ try { // this should fail
+ inTransaction(function(conn) {
+ sqlobj.updateSingle("just_a_test", {id: 1}, {id: 1, x: "b"});
+ // note: this will be pritned to the console, but that's OK
+ throw Error();
+ });
+ } catch (e) {}
+
+ var testRecord = sqlobj.selectSingle("just_a_test", {id: 1});
+
+ assertTruthy(testRecord.x == "a");
+}
+
+
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js b/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js
new file mode 100644
index 0000000..67c79d8
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js
@@ -0,0 +1,89 @@
+/**
+ * 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.
+ */
+
+import("stringutils");
+import("sqlbase.sqlobj");
+
+import("etherpad.licensing");
+
+jimport("java.util.Random");
+
+function run() {
+ var r = new Random(0);
+
+ function testLicense(name, org, expires, editionId, userQuota) {
+ function keydataString() {
+ return "{name: "+name+", org: "+org+", expires: "+expires+", editionId: "+editionId+", userQuota: "+userQuota+"}";
+ }
+ var key = licensing.generateNewKey(name, org, expires, editionId, userQuota);
+ var info = licensing.decodeLicenseInfoFromKey(key);
+ if (!info) {
+ println("Generated key does not decode at all: "+keydataString());
+ println(" generated key: "+key);
+ throw new Error("Generated key does not decode at all. See stdout.");
+ }
+ function testMatch(name, x, y) {
+ if (x != y) {
+ println("key match error ("+name+"): ["+x+"] != ["+y+"]");
+ println(" key data: "+keydataString());
+ println(" generated key: "+key);
+ println(" decoded key: "+info.toSource());
+ throw new Error(name+" mismatch. see stdout.");
+ }
+ }
+ testMatch("personName", info.personName, name);
+ testMatch("orgName", info.organizationName, org);
+ testMatch("expires", +info.expiresDate, +expires);
+ testMatch("editionName", info.editionName, licensing.getEditionName(editionId));
+ testMatch("userQuota", +info.userQuota, +userQuota);
+ }
+
+ testLicense("aaron", "test", +(new Date)+1000*60*60*24*30, licensing.getEditionId('PRIVATE_NETWORK_EVALUATION'), 1001);
+
+ for (var editionId = 0; editionId < 3; editionId++) {
+ for (var unlimitedUsers = 0; unlimitedUsers <= 1; unlimitedUsers++) {
+ for (var noExpiry = 0; noExpiry <= 1; noExpiry++) {
+ for (var j = 0; j < 100; j++) {
+ var name = stringutils.randomString(1+r.nextInt(39));
+ var org = stringutils.randomString(1+r.nextInt(39));
+ var expires = null;
+ if (noExpiry == 0) {
+ expires = +(new Date)+(1000*60*60*24*r.nextInt(100));
+ }
+ var userQuota = -1;
+ if (unlimitedUsers == 1) {
+ userQuota = r.nextInt(1e6);
+ }
+
+ testLicense(name, org, expires, editionId, userQuota);
+ }
+ }
+ }
+ }
+
+ // test that all previously generated keys continue to decode.
+ var historicalKeys = sqlobj.selectMulti('eepnet_signups', {}, {});
+ historicalKeys.forEach(function(d) {
+ var key = d.licenseKey;
+ if (key && !licensing.isValidKey(key)) {
+ throw new Error("Historical license key no longer validates: "+key);
+ }
+ });
+
+}
+
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js b/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js
new file mode 100644
index 0000000..0898fbe
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js
@@ -0,0 +1,42 @@
+/**
+ * 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.
+ */
+
+import("sqlbase.persistent_vars");
+
+import("stringutils");
+
+import("etherpad.testing.testutils.*");
+
+function run() {
+ var varname = stringutils.randomString(50);
+ var varval = stringutils.randomString(50);
+
+ var x = persistent_vars.get(varname);
+ assertTruthy(!x);
+
+ persistent_vars.put(varname, varval);
+
+ for (var i = 0; i < 3; i++) {
+ x = persistent_vars.get(varname);
+ assertTruthy(x == varval);
+ }
+
+ persistent_vars.remove(varname);
+
+ var x = persistent_vars.get(varname);
+ assertTruthy(!x);
+}
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js b/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js
new file mode 100644
index 0000000..7f8c996
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js
@@ -0,0 +1,214 @@
+/**
+ * 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.
+ */
+
+import("jsutils.*");
+import("stringutils");
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+import("etherpad.globals.*");
+import("etherpad.testing.testutils.*");
+
+function run() {
+ cleanUpTables();
+ testGeneral();
+ testAlterColumn();
+ cleanUpTables();
+}
+
+function _getTestTableName() {
+ return 'sqlobj_unit_test_'+stringutils.randomString(10);
+}
+
+function testGeneral() {
+
+ if (isProduction()) {
+ return; // we dont run this in productin!
+ }
+
+ // create a test table
+ var tableName = _getTestTableName();
+
+ sqlobj.createTable(tableName, {
+ id: sqlobj.getIdColspec(),
+ varChar: 'VARCHAR(128)',
+ dateTime: sqlobj.getDateColspec("NOT NULL"),
+ int11: 'INT',
+ tinyInt: sqlobj.getBoolColspec("DEFAULT 0")
+ });
+
+ // add some columns
+ sqlobj.addColumns(tableName, {
+ a: 'VARCHAR(256)',
+ b: 'VARCHAR(256)',
+ c: 'VARCHAR(256)',
+ d: 'VARCHAR(256)'
+ });
+
+ // drop columns
+ sqlobj.dropColumn(tableName, 'c');
+ sqlobj.dropColumn(tableName, 'd');
+
+ // list tables and make sure it contains tableName
+ var l = sqlobj.listTables();
+ var found = false;
+ l.forEach(function(x) {
+ if (x == tableName) { found = true; }
+ });
+ assertTruthy(found);
+
+ if (sqlcommon.isMysql()) {
+ for (var i = 0; i < 3; i++) {
+ ['MyISAM', 'InnoDB'].forEach(function(e) {
+ sqlobj.setTableEngine(tableName, e);
+ assertTruthy(e == sqlobj.getTableEngine(tableName));
+ });
+ }
+ }
+
+ sqlobj.createIndex(tableName, ['a', 'b']);
+ sqlobj.createIndex(tableName, ['int11', 'a', 'b']);
+
+ // test null columns
+ for (var i = 0; i < 10; i++) {
+ var id = sqlobj.insert(tableName, {dateTime: new Date(), a: null, b: null});
+ sqlobj.deleteRows(tableName, {id: id});
+ }
+
+ //----------------------------------------------------------------
+ // data management
+ //----------------------------------------------------------------
+
+ // insert + selectSingle
+ function _randomDate() {
+ // millisecond accuracy is lost in DB.
+ var d = +(new Date);
+ d = Math.round(d / 1000) * 1000;
+ return new Date(d);
+ }
+ var obj_data_list = [];
+ for (var i = 0; i < 40; i++) {
+ var obj_data = {
+ varChar: stringutils.randomString(20),
+ dateTime: _randomDate(),
+ int11: +(new Date) % 10000,
+ tinyInt: !!(+(new Date) % 2),
+ a: "foo",
+ b: "bar"
+ };
+ obj_data_list.push(obj_data);
+
+ var obj_id = sqlobj.insert(tableName, obj_data);
+ var obj_result = sqlobj.selectSingle(tableName, {id: obj_id});
+
+ assertTruthy(obj_result.id == obj_id);
+ keys(obj_data).forEach(function(k) {
+ var d1 = obj_data[k];
+ var d2 = obj_result[k];
+ if (k == "dateTime") {
+ d1 = +d1;
+ d2 = +d2;
+ }
+ if (d1 != d2) {
+ throw Error("result mismatch ["+k+"]: "+d1+" != "+d2);
+ }
+ });
+ }
+
+ // selectMulti: no constraints, no options
+ var obj_result_list = sqlobj.selectMulti(tableName, {}, {});
+ assertTruthy(obj_result_list.length == obj_data_list.length);
+ // orderBy
+ ['int11', 'a', 'b'].forEach(function(colName) {
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {orderBy: colName});
+ assertTruthy(obj_result_list.length == obj_data_list.length);
+ for (var i = 1; i < obj_result_list.length; i++) {
+ assertTruthy(obj_result_list[i-1][colName] <= obj_result_list[i][colName]);
+ }
+
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {orderBy: "-"+colName});
+ assertTruthy(obj_result_list.length == obj_data_list.length);
+ for (var i = 1; i < obj_result_list.length; i++) {
+ assertTruthy(obj_result_list[i-1][colName] >= obj_result_list[i][colName]);
+ }
+ });
+
+ // selectMulti: with constraints
+ var obj_result_list1 = sqlobj.selectMulti(tableName, {tinyInt: true}, {});
+ var obj_result_list2 = sqlobj.selectMulti(tableName, {tinyInt: false}, {});
+ assertTruthy((obj_result_list1.length + obj_result_list2.length) == obj_data_list.length);
+ obj_result_list1.forEach(function(o) {
+ assertTruthy(o.tinyInt == true);
+ });
+ obj_result_list2.forEach(function(o) {
+ assertTruthy(o.tinyInt == false);
+ });
+
+ // updateSingle
+ obj_result_list1.forEach(function(o) {
+ o.a = "ttt";
+ sqlobj.updateSingle(tableName, {id: o.id}, o);
+ });
+ // update
+ sqlobj.update(tableName, {tinyInt: false}, {a: "fff"});
+ // verify
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {});
+ obj_result_list.forEach(function(o) {
+ if (o.tinyInt) {
+ assertTruthy(o.a == "ttt");
+ } else {
+ assertTruthy(o.a == "fff");
+ }
+ });
+
+ // deleteRows
+ sqlobj.deleteRows(tableName, {a: "ttt"});
+ sqlobj.deleteRows(tableName, {a: "fff"});
+ // verify
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {});
+ assertTruthy(obj_result_list.length == 0);
+}
+
+function cleanUpTables() {
+ // delete testing table (and any other old testing tables)
+ sqlobj.listTables().forEach(function(t) {
+ if (t.indexOf("sqlobj_unit_test") == 0) {
+ sqlobj.dropTable(t);
+ }
+ });
+}
+
+function testAlterColumn() {
+ var tableName = _getTestTableName();
+
+ sqlobj.createTable(tableName, {
+ x: 'INT',
+ a: 'INT NOT NULL',
+ b: 'INT NOT NULL'
+ });
+
+ if (sqlcommon.isMysql()) {
+ sqlobj.modifyColumn(tableName, 'a', 'INT');
+ sqlobj.modifyColumn(tableName, 'b', 'INT');
+ } else {
+ sqlobj.alterColumn(tableName, 'a', 'NULL');
+ sqlobj.alterColumn(tableName, 'b', 'NULL');
+ }
+
+ sqlobj.insert(tableName, {a: 5});
+}
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js b/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js
new file mode 100644
index 0000000..9cd3f21
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+
+
+import("etherpad.collab.ace.easysync2_tests");
+
+function run() {
+ easysync2_tests.runTests();
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/usage_stats/usage_stats.js b/etherpad/src/etherpad/usage_stats/usage_stats.js
new file mode 100644
index 0000000..59074ed
--- /dev/null
+++ b/etherpad/src/etherpad/usage_stats/usage_stats.js
@@ -0,0 +1,162 @@
+/**
+ * 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.
+ */
+
+import("execution");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("jsutils.*");
+import("fastJSON");
+
+import("etherpad.log");
+import("etherpad.log.frontendLogFileName");
+import("etherpad.statistics.statistics");
+import("fileutils.eachFileLine");
+
+jimport("java.lang.System.out.println");
+jimport("java.io.BufferedReader");
+jimport("java.io.FileReader");
+jimport("java.io.File");
+jimport("java.awt.Color");
+
+jimport("org.jfree.chart.ChartFactory");
+jimport("org.jfree.chart.ChartUtilities");
+jimport("org.jfree.chart.JFreeChart");
+jimport("org.jfree.chart.axis.DateAxis");
+jimport("org.jfree.chart.axis.NumberAxis");
+jimport("org.jfree.chart.plot.XYPlot");
+jimport("org.jfree.chart.renderer.xy.XYLineAndShapeRenderer");
+jimport("org.jfree.data.time.Day");
+jimport("org.jfree.data.time.TimeSeries");
+jimport("org.jfree.data.time.TimeSeriesCollection");
+
+//----------------------------------------------------------------
+// Database reading/writing
+//----------------------------------------------------------------
+
+
+function _listStats(statName) {
+ return sqlobj.selectMulti('statistics', {name: statName}, {orderBy: '-timestamp'});
+}
+
+// public accessor
+function getStatData(statName) {
+ return _listStats(statName);
+}
+
+//----------------------------------------------------------------
+// HTML & Graph generating
+//----------------------------------------------------------------
+
+function respondWithGraph(statName) {
+ var width = 500;
+ var height = 300;
+ if (request.params.size) {
+ var parts = request.params.size.split('x');
+ width = +parts[0];
+ height = +parts[1];
+ }
+
+ var dataset = new TimeSeriesCollection();
+ var hideLegend = true;
+
+ switch (statistics.getStatData(statName).plotType) {
+ case 'line':
+ var ts = new TimeSeries(statName);
+
+ _listStats(statName).forEach(function(stat) {
+ var day = new Day(new java.util.Date(stat.timestamp * 1000));
+ ts.addOrUpdate(day, fastJSON.parse(stat.value).value);
+ });
+ dataset.addSeries(ts);
+ break;
+ case 'topValues':
+ hideLegend = false;
+ var stats = _listStats(statName);
+ if (stats.length == 0) break;
+ var latestStat = fastJSON.parse(stats[0].value);
+ var valuesToWatch = [];
+ var series = {};
+ var nLines = 5;
+ function forEachFirstN(n, stat, f) {
+ for (var i = 0; i < Math.min(n, stat.topValues.length); i++) {
+ f(stat.topValues[i].value, stat.topValues[i].count);
+ }
+ }
+ forEachFirstN(nLines, latestStat, function(value, count) {
+ valuesToWatch.push(value);
+ series[value] = new TimeSeries(value);
+ });
+ stats.forEach(function(stat) {
+ var day = new Day(new java.util.Date(stat.timestamp*1000));
+ var statData = fastJSON.parse(stat.value);
+ valuesToWatch.forEach(function(value) { series[value].addOrUpdate(day, 0); })
+ forEachFirstN(nLines, statData, function(value, count) {
+ if (series[value]) {
+ series[value].addOrUpdate(day, count);
+ }
+ });
+ });
+ valuesToWatch.forEach(function(value) {
+ dataset.addSeries(series[value]);
+ });
+ break;
+ case 'histogram':
+ hideLegend = false;
+ var stats = _listStats(statName);
+ percentagesToGraph = ["50", "90", "100"];
+ series = {};
+ percentagesToGraph.forEach(function(pct) {
+ series[pct] = new TimeSeries(pct+"%");
+ dataset.addSeries(series[pct]);
+ });
+ if (stats.length == 0) break;
+ stats.forEach(function(stat) {
+ var day = new Day(new java.util.Date(stat.timestamp*1000));
+ var statData = fastJSON.parse(stat.value);
+ eachProperty(series, function(pct, timeseries) {
+ timeseries.addOrUpdate(day, statData[pct] || 0);
+ });
+ });
+ break;
+ }
+
+ var domainAxis = new DateAxis("");
+ var rangeAxis = new NumberAxis();
+ var renderer = new XYLineAndShapeRenderer();
+
+ var numSeries = dataset.getSeriesCount();
+ var colors = [Color.blue, Color.red, Color.green, Color.orange, Color.pink, Color.magenta];
+ for (var i = 0; i < numSeries; ++i) {
+ renderer.setSeriesPaint(i, colors[i]);
+ renderer.setSeriesShapesVisible(i, false);
+ }
+
+ var plot = new XYPlot(dataset, domainAxis, rangeAxis, renderer);
+
+ var chart = new JFreeChart(plot);
+ chart.setTitle(statName);
+ if (hideLegend) {
+ chart.removeLegend();
+ }
+
+ var jos = new java.io.ByteArrayOutputStream();
+ ChartUtilities.writeChartAsJPEG(
+ jos, 1.0, chart, width, height);
+
+ response.setContentType('image/jpeg');
+ response.writeBytes(jos.toByteArray());
+}
+
diff --git a/etherpad/src/etherpad/utils.js b/etherpad/src/etherpad/utils.js
new file mode 100644
index 0000000..65ebe1f
--- /dev/null
+++ b/etherpad/src/etherpad/utils.js
@@ -0,0 +1,464 @@
+/**
+ * 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.
+ */
+
+import("exceptionutils");
+import("fileutils.{readFile,fileLastModified}");
+import("ejs.EJS");
+import("funhtml.*");
+import("stringutils");
+import("stringutils.startsWith");
+import("jsutils.*");
+
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.globals.*");
+import("etherpad.helpers");
+import("etherpad.collab.collab_server");
+import("etherpad.pad.model");
+import("etherpad.pro.domains");
+import("etherpad.pne.pne_utils");
+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");
+
+jimport("java.io.File");
+
+//----------------------------------------------------------------
+// utilities
+//----------------------------------------------------------------
+
+// returns globally-unique padId
+function randomUniquePadId() {
+ var id = stringutils.randomString(10);
+ while (model.accessPadGlobal(id, function(p) { return p.exists(); }, "r")) {
+ id = stringutils.randomString(10);
+ }
+ return id;
+}
+
+//----------------------------------------------------------------
+// template rendering
+//----------------------------------------------------------------
+
+function findExistsingFile(files) {
+ for (var i = 0; i < files.length; i++) {
+ var f = new File('./src' + files[i]);
+ if (f.exists())
+ return files[i];
+ }
+}
+
+function findTemplate(filename, plugin) {
+ var files = [];
+
+ var pluginList = [plugin];
+ try {
+ if (plugin.forEach !== undefined)
+ pluginList = plugin;
+ else
+ pluginList = [plugin];
+ } catch (e) {}
+
+ pluginList.forEach(function (plugin) {
+ if (plugin != undefined) {
+ files.push('/plugins/' + plugin + '/templates/' + filename);
+ files.push('/themes/' + appjet.config.theme + '/plugins/' + plugin + '/templates/' + filename);
+ files.push('/themes/default/plugins/' + plugin + '/templates/' + filename);
+ }
+ });
+ files.push('/themes/' + appjet.config.theme + '/templates/' + filename);
+ files.push('/themes/default/templates/' + filename);
+
+ return findExistsingFile(files);
+}
+
+function Template(params, plugin) {
+ this._defines = {}
+ this._params = params;
+ this._params.template = this;
+ this._plugin = plugin;
+}
+
+Template.prototype.define = function(name, fn) {
+ this._defines[name] = fn;
+ return '';
+}
+
+Template.prototype.use = function (name, fn, arg) {
+ if (this._defines[name] != undefined)
+ return this._defines[name](arg);
+ else if (fn != undefined)
+ return fn(arg);
+ else
+ return '';
+}
+
+Template.prototype.inherit = function (template) {
+ return renderTemplateAsString(template, this._params, this._plugin);
+}
+
+function renderTemplateAsString(filename, data, plugin) {
+ data = data || {};
+ data.helpers = helpers; // global helpers
+ data.plugins = plugins; // Access callHook and the like...
+ if (data.template == undefined)
+ new Template(data, plugin);
+
+ var f = findTemplate(filename, plugin); //"/templates/"+filename;
+ if (! appjet.scopeCache.ejs) {
+ appjet.scopeCache.ejs = {};
+ }
+ var cacheObj = appjet.scopeCache.ejs[filename];
+ if (cacheObj === undefined || fileLastModified(f) > cacheObj.mtime) {
+ var templateText = readFile(f);
+ templateText += "<%: template.use('body', function () { return ''; }); %> ";
+ cacheObj = {};
+ cacheObj.tmpl = new EJS({text: templateText, name: filename});
+ cacheObj.mtime = fileLastModified(f);
+ appjet.scopeCache.ejs[filename] = cacheObj;
+ }
+ var html = cacheObj.tmpl.render(data);
+ return html;
+}
+
+function renderTemplate(filename, data, plugin) {
+ response.write(renderTemplateAsString(filename, data, plugin));
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+}
+
+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, plugin) {
+ var getContentHtml;
+ if (typeof(contentHtml) == 'function') {
+ getContentHtml = contentHtml;
+ } else {
+ getContentHtml = function() { return contentHtml; }
+ }
+
+ var template = "framed/framedpage.ejs";
+ if (isProDomainRequest()) {
+ template = "framed/framedpage-pro.ejs";
+ }
+
+ renderHtml(template, {
+ renderHeader: renderMainHeader,
+ renderFooter: renderMainFooter,
+ getContentHtml: getContentHtml,
+ isProDomainRequest: isProDomainRequest(),
+ renderGlobalProNotice: pro_utils.renderGlobalProNotice
+ }, plugin);
+}
+
+function renderFramed(bodyFileName, data, plugin) {
+ function _getContentHtml() {
+ return renderTemplateAsString(bodyFileName, data, plugin);
+ }
+ renderFramedHtml(_getContentHtml);
+}
+
+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, plugin);
+}
+
+function renderNotice(bodyFileName, data, plugin) {
+ renderNoticeString(renderTemplateAsString(bodyFileName, data, plugin), plugin);
+}
+
+function renderNoticeString(contentHtml, plugin) {
+ renderFramed("notice.ejs", {content: contentHtml}, plugin);
+}
+
+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))), plugin);
+ if (! noStop) {
+ response.stop();
+ }
+}
+
+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}, plugin);
+}
+
+function _renderEtherpadDotComHeader(data) {
+ if (!data) {
+ data = {selected: ''};
+ }
+ data.html = stringutils.html;
+ data.UL = UL;
+ data.LI = LI;
+ data.A = A;
+ data.isPNE = isPrivateNetworkEdition();
+ return renderTemplateAsString("framed/framedheader.ejs", data);
+}
+
+function _renderProHeader(data) {
+ if (!pro_accounts.isAccountSignedIn()) {
+ return '<div style="height: 140px;">&nbsp;</div>';
+ }
+
+ var r = domains.getRequestDomainRecord();
+ if (!data) { data = {}; }
+ data.navSelection = (data.navSelection || appjet.requestCache.proTopNavSelection || '');
+ data.proDomainOrgName = pro_config.getConfig().siteName;
+ data.isPNE = isPrivateNetworkEdition();
+ data.account = getSessionProAccount();
+ data.validLicense = pne_utils.isServerLicensed();
+ data.pneTrackerHtml = pne_utils.pneTrackerHtml();
+ data.isAnEtherpadAdmin = sessions.isAnEtherpadAdmin();
+ data.fullSuperdomain = pro_utils.getFullSuperdomainHost();
+ return renderTemplateAsString("framed/framedheader-pro.ejs", data);
+}
+
+function renderMainHeader(data) {
+ if (isProDomainRequest()) {
+ return _renderProHeader(data);
+ } else {
+ return _renderEtherpadDotComHeader(data);
+ }
+}
+
+function renderMainFooter() {
+ return renderTemplateAsString("framed/framedfooter.ejs", {
+ isProDomainRequest: isProDomainRequest()
+ });
+}
+
+//----------------------------------------------------------------
+// isValidEmail
+//----------------------------------------------------------------
+
+// TODO: make better and use the better version on the client in
+// various places as well (pad.js and etherpad.js)
+function isValidEmail(x) {
+ return (x &&
+ ((x.length > 0) &&
+ (x.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/))));
+}
+
+//----------------------------------------------------------------
+
+function timeAgo(d, now) {
+ if (!now) { now = new Date(); }
+
+ function format(n, word) {
+ n = Math.round(n);
+ return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago');
+ }
+
+ d = (+now - (+d)) / 1000;
+ if (d < 60) { return format(d, 'second'); }
+ d /= 60;
+ if (d < 60) { return format(d, 'minute'); }
+ d /= 60;
+ if (d < 24) { return format(d, 'hour'); }
+ d /= 24;
+ return format(d, 'day');
+};
+
+
+//----------------------------------------------------------------
+// linking to a set of new CGI parameters
+//----------------------------------------------------------------
+function qpath(m) {
+ var q = {};
+ if (request.query) {
+ request.query.split('&').forEach(function(kv) {
+ if (kv) {
+ var parts = kv.split('=');
+ q[parts[0]] = parts[1];
+ }
+ });
+ }
+ eachProperty(m, function(k,v) {
+ q[k] = v;
+ });
+ var r = request.path + '?';
+ eachProperty(q, function(k,v) {
+ if (v !== undefined && v !== null) {
+ r += ('&' + k + '=' + v);
+ }
+ });
+ return r;
+}
+
+//----------------------------------------------------------------
+
+function ipToHostname(ip) {
+ var DNS = Packages.org.xbill.DNS;
+
+ if (!DNS.Address.isDottedQuad(ip)) {
+ return null
+ }
+
+ try {
+ var addr = DNS.Address.getByAddress(ip);
+ return DNS.Address.getHostName(addr);
+ } catch (ex) {
+ return null;
+ }
+}
+
+function extractGoogleQuery(ref) {
+ ref = String(ref);
+ ref = ref.toLowerCase();
+ if (!(ref.indexOf("google") >= 0)) {
+ return "";
+ }
+
+ ref = ref.split('?')[1];
+
+ var q = "";
+ ref.split("&").forEach(function(x) {
+ var parts = x.split("=");
+ if (parts[0] == "q") {
+ q = parts[1];
+ }
+ });
+
+ q = decodeURIComponent(q);
+ q = q.replace(/\+/g, " ");
+
+ return q;
+}
+
+function isTestEmail(x) {
+ return (x.indexOf("+appjetseleniumtest+") >= 0);
+}
+
+function isPrivateNetworkEdition() {
+ return pne_utils.isPNE();
+}
+
+function isProDomainRequest() {
+ return pro_utils.isProDomainRequest();
+}
+
+function hasOffice() {
+ return appjet.config["etherpad.soffice"] || appjet.config["etherpad.sofficeConversionServer"];
+}
+
+////////// console progress bar
+
+function startConsoleProgressBar(barWidth, updateIntervalSeconds) {
+ barWidth = barWidth || 40;
+ updateIntervalSeconds = ((typeof updateIntervalSeconds) == "number" ? updateIntervalSeconds : 1.0);
+
+ var unseenStatus = null;
+ var lastPrintTime = 0;
+ var column = 0;
+
+ function replaceLineWith(str) {
+ //print((new Array(column+1)).join('\b')+str);
+ print('\r'+str);
+ column = str.length;
+ }
+
+ var bar = {
+ update: function(frac, msg, force) {
+ var t = +new Date();
+ if ((!force) && ((t - lastPrintTime)/1000 < updateIntervalSeconds)) {
+ unseenStatus = {frac:frac, msg:msg};
+ }
+ else {
+ var pieces = [];
+ pieces.push(' ', (' '+Math.round(frac*100)).slice(-3), '%', ' [');
+ var barEndLoc = Math.max(0, Math.min(barWidth-1, Math.floor(frac*barWidth)));
+ for(var i=0;i<barWidth;i++) {
+ if (i < barEndLoc) pieces.push('=');
+ else if (i == barEndLoc) pieces.push('>');
+ else pieces.push(' ');
+ }
+ pieces.push('] ', msg || '');
+ replaceLineWith(pieces.join(''));
+
+ unseenStatus = null;
+ lastPrintTime = t;
+ }
+ },
+ finish: function() {
+ if (unseenStatus) {
+ bar.update(unseenStatus.frac, unseenStatus.msg, true);
+ }
+ println();
+ }
+ };
+
+ println();
+ bar.update(0, null, true);
+
+ return bar;
+}
+
+function isStaticRequest() {
+ return (startsWith(request.path, '/static/') ||
+ startsWith(request.path, '/favicon.ico') ||
+ startsWith(request.path, '/robots.txt'));
+}
+
+function httpsHost(h) {
+ h = h.split(":")[0]; // strip any existing port
+ if (appjet.config.listenSecurePort != "443" && !appjet.config.hidePorts) {
+ h = (h + ":" + appjet.config.listenSecurePort);
+ }
+ return h;
+}
+
+function httpHost(h) {
+ h = h.split(":")[0]; // strip any existing port
+ if (appjet.config.listenPort != "80" && !appjet.config.hidePorts) {
+ h = (h + ":" + appjet.config.listenPort);
+ }
+ return h;
+}
+
+function toJavaException(e) {
+ var exc = ((e instanceof java.lang.Throwable) && e) || e.rhinoException || e.javaException ||
+ new java.lang.Throwable(e.message+"/"+e.fileName+"/"+e.lineNumber);
+ return exc;
+}