diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/control/admincontrol.js')
-rw-r--r-- | trunk/etherpad/src/etherpad/control/admincontrol.js | 1471 |
1 files changed, 0 insertions, 1471 deletions
diff --git a/trunk/etherpad/src/etherpad/control/admincontrol.js b/trunk/etherpad/src/etherpad/control/admincontrol.js deleted file mode 100644 index 02f6428..0000000 --- a/trunk/etherpad/src/etherpad/control/admincontrol.js +++ /dev/null @@ -1,1471 +0,0 @@ -/** - * 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)); -}
\ No newline at end of file |