diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/control')
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("µ"), "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 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("«"), " back to admin")); +// body.push(_renderTopnav(cat)); +// +// body.push(form); +// +// if (request.params.stat) { +// body.push(A({className: "viewall", +// href: qpath({stat: null})}, html("«"), " 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+" ("+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"); + } +} + |