summaryrefslogtreecommitdiffstats
path: root/trunk/etherpad/src/etherpad/control
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/etherpad/src/etherpad/control')
-rw-r--r--trunk/etherpad/src/etherpad/control/aboutcontrol.js263
-rw-r--r--trunk/etherpad/src/etherpad/control/admincontrol.js1471
-rw-r--r--trunk/etherpad/src/etherpad/control/blogcontrol.js199
-rw-r--r--trunk/etherpad/src/etherpad/control/connection_diagnostics_control.js87
-rw-r--r--trunk/etherpad/src/etherpad/control/global_pro_account_control.js143
-rw-r--r--trunk/etherpad/src/etherpad/control/historycontrol.js226
-rw-r--r--trunk/etherpad/src/etherpad/control/loadtestcontrol.js93
-rw-r--r--trunk/etherpad/src/etherpad/control/maincontrol.js54
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js280
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_control.js780
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js319
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_view_control.js287
-rw-r--r--trunk/etherpad/src/etherpad/control/pne_manual_control.js75
-rw-r--r--trunk/etherpad/src/etherpad/control/pne_tracker_control.js48
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/account_control.js369
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/admin/account_manager_control.js260
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/admin/license_manager_control.js128
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js283
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/admin/pro_config_control.js54
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/admin/team_billing_control.js447
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/pro_main_control.js150
-rw-r--r--trunk/etherpad/src/etherpad/control/pro/pro_padlist_control.js200
-rw-r--r--trunk/etherpad/src/etherpad/control/pro_beta_control.js136
-rw-r--r--trunk/etherpad/src/etherpad/control/pro_signup_control.js173
-rw-r--r--trunk/etherpad/src/etherpad/control/scriptcontrol.js75
-rw-r--r--trunk/etherpad/src/etherpad/control/static_control.js65
-rw-r--r--trunk/etherpad/src/etherpad/control/statscontrol.js1214
-rw-r--r--trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js757
-rw-r--r--trunk/etherpad/src/etherpad/control/store/storecontrol.js201
-rw-r--r--trunk/etherpad/src/etherpad/control/testcontrol.js74
30 files changed, 8911 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/control/aboutcontrol.js b/trunk/etherpad/src/etherpad/control/aboutcontrol.js
new file mode 100644
index 0000000..9d77142
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/admincontrol.js b/trunk/etherpad/src/etherpad/control/admincontrol.js
new file mode 100644
index 0000000..02f6428
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/control/admincontrol.js
@@ -0,0 +1,1471 @@
+/**
+ * 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");
+
+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"]
+];
+
+function onRequest(name) {
+ if (name == "auth") {
+ return;
+ }
+ if (!_isAuthorizedAdmin()) {
+ getSession().cont = request.path;
+ response.redirect('/ep/admin/auth');
+ }
+
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [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("«"), " home"));
+ div.push(H1("Admin"));
+
+ _mainLinks.forEach(function(l) {
+ div.push(DIV(A({href: l[0]}, l[1])));
+ });
+ 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("« 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/trunk/etherpad/src/etherpad/control/blogcontrol.js b/trunk/etherpad/src/etherpad/control/blogcontrol.js
new file mode 100644
index 0000000..9ec485d
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/connection_diagnostics_control.js b/trunk/etherpad/src/etherpad/control/connection_diagnostics_control.js
new file mode 100644
index 0000000..aaa1bb3
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/global_pro_account_control.js b/trunk/etherpad/src/etherpad/control/global_pro_account_control.js
new file mode 100644
index 0000000..65d2124
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/historycontrol.js b/trunk/etherpad/src/etherpad/control/historycontrol.js
new file mode 100644
index 0000000..a78cfad
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/loadtestcontrol.js b/trunk/etherpad/src/etherpad/control/loadtestcontrol.js
new file mode 100644
index 0000000..2a4e3f7
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/maincontrol.js b/trunk/etherpad/src/etherpad/control/maincontrol.js
new file mode 100644
index 0000000..261ddaf
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js
new file mode 100644
index 0000000..5af7ed0
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pad/pad_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_control.js
new file mode 100644
index 0000000..3c32202
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/control/pad/pad_control.js
@@ -0,0 +1,780 @@
+/**
+ * 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);
+ 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({
+ 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),
+ opts: opts
+ });
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ var isFullWidth = false;
+ var hideSidebar = false;
+ var cookiePrefs = padutils.getPrefsCookieData();
+ if (cookiePrefs) {
+ isFullWidth = !! cookiePrefs.fullWidth;
+ hideSidebar = !! cookiePrefs.hideSidebar;
+ }
+ if (opts.fullScreen) {
+ isFullWidth = true;
+ if (opts.tokbox) {
+ hideSidebar = true;
+ }
+ }
+ if ('sidebar' in opts) {
+ hideSidebar = ! opts.sidebar;
+ }
+ var bodyClass = (isFullWidth ? "fullwidth" : "limwidth")+
+ " "+(isPro ? "propad" : "nonpropad")+" "+
+ (isProUser ? "prouser" : "nonprouser");
+
+ var cookiePrefsToSet = {fullWidth:isFullWidth, hideSidebar:hideSidebar};
+ helpers.addClientVars({cookiePrefsToSet: cookiePrefsToSet});
+
+ 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: {isFullWidth:isFullWidth, hideSidebar:hideSidebar},
+ 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/trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js
new file mode 100644
index 0000000..b7e5f4d
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pad/pad_view_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_view_control.js
new file mode 100644
index 0000000..0606d2c
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pne_manual_control.js b/trunk/etherpad/src/etherpad/control/pne_manual_control.js
new file mode 100644
index 0000000..0dd65f8
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pne_tracker_control.js b/trunk/etherpad/src/etherpad/control/pne_tracker_control.js
new file mode 100644
index 0000000..ee36645
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/account_control.js b/trunk/etherpad/src/etherpad/control/pro/account_control.js
new file mode 100644
index 0000000..031dbe6
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/account_manager_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/account_manager_control.js
new file mode 100644
index 0000000..8f93b2e
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/license_manager_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/license_manager_control.js
new file mode 100644
index 0000000..ca6d6a6
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js
new file mode 100644
index 0000000..f9ce179
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js
@@ -0,0 +1,283 @@
+/**
+ * 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"],
+ [_PNE_ONLY, "pne-license-manager/", "Manage License"],
+ [_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"]]],
+ [_PNE_ONLY, null, "Documentation"],
+ [_PNE_ONLY, "/ep/pne-manual/", "Administrator's Manual"],
+ ]
+ ];
+ 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/trunk/etherpad/src/etherpad/control/pro/admin/pro_config_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/pro_config_control.js
new file mode 100644
index 0000000..b03da45
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/team_billing_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/team_billing_control.js
new file mode 100644
index 0000000..5be6a0e
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/pro_main_control.js b/trunk/etherpad/src/etherpad/control/pro/pro_main_control.js
new file mode 100644
index 0000000..b4e3bc4
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/pro_padlist_control.js b/trunk/etherpad/src/etherpad/control/pro/pro_padlist_control.js
new file mode 100644
index 0000000..9a90c67
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro_beta_control.js b/trunk/etherpad/src/etherpad/control/pro_beta_control.js
new file mode 100644
index 0000000..ec99b43
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro_signup_control.js b/trunk/etherpad/src/etherpad/control/pro_signup_control.js
new file mode 100644
index 0000000..6bf7cc3
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/scriptcontrol.js b/trunk/etherpad/src/etherpad/control/scriptcontrol.js
new file mode 100644
index 0000000..16efc60
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/static_control.js b/trunk/etherpad/src/etherpad/control/static_control.js
new file mode 100644
index 0000000..5c087b6
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/control/static_control.js
@@ -0,0 +1,65 @@
+/**
+ * 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.*");
+
+function onRequest() {
+ var staticBase = '/static';
+
+ var opts = {cache: isProduction()};
+
+ 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);
+
+ var disp = new Dispatcher();
+
+ 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/trunk/etherpad/src/etherpad/control/statscontrol.js b/trunk/etherpad/src/etherpad/control/statscontrol.js
new file mode 100644
index 0000000..3659107
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js b/trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js
new file mode 100644
index 0000000..ddd4973
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/store/storecontrol.js b/trunk/etherpad/src/etherpad/control/store/storecontrol.js
new file mode 100644
index 0000000..43569e4
--- /dev/null
+++ b/trunk/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/trunk/etherpad/src/etherpad/control/testcontrol.js b/trunk/etherpad/src/etherpad/control/testcontrol.js
new file mode 100644
index 0000000..ed13006
--- /dev/null
+++ b/trunk/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");
+ }
+}
+