/**
* 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("µ"), "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(" "), 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(" » ")));
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("« back")));
response.write(HTML(body));
}