summaryrefslogtreecommitdiffstats
path: root/trunk/etherpad/src/etherpad/pro
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/etherpad/src/etherpad/pro')
-rw-r--r--trunk/etherpad/src/etherpad/pro/domains.js141
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js101
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_accounts.js496
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_config.js92
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_pad_db.js232
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_pad_editors.js104
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_padlist.js289
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_padmeta.js111
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_quotas.js141
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_utils.js165
10 files changed, 1872 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/pro/domains.js b/trunk/etherpad/src/etherpad/pro/domains.js
new file mode 100644
index 0000000..e56a408
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/domains.js
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Library for managing subDomains
+
+import("jsutils.*");
+import("sqlbase.sqlobj");
+
+import("etherpad.pro.pro_utils");
+import("etherpad.pne.pne_utils");
+import("etherpad.licensing");
+
+jimport("java.lang.System.out.println");
+
+// reserved domains
+var reservedSubdomains = {
+ 'alpha': 1,
+ 'beta': 1,
+ 'blog': 1,
+ 'comet': 1,
+ 'diagnostic': 1,
+ 'forums': 1,
+ 'forumsdev': 1,
+ 'staging': 1,
+ 'web': 1,
+ 'www': 1
+};
+
+function _getCache() {
+ if (!appjet.cache.pro_domains) {
+ appjet.cache.pro_domains = {
+ records: {id: {}, subDomain: {}}
+ };
+ }
+ return appjet.cache.pro_domains;
+}
+
+function doesSubdomainExist(subDomain) {
+ if (reservedSubdomains[subDomain]) {
+ return true;
+ }
+ if (getDomainRecordFromSubdomain(subDomain) != null) {
+ return true;
+ }
+ return false;
+}
+
+function _updateCache(locator) {
+ var record = sqlobj.selectSingle('pro_domains', locator);
+ var recordCache = _getCache().records;
+
+ if (record) {
+ // update both maps: recordCache.id, recordCache.subDomain
+ keys(recordCache).forEach(function(key) {
+ recordCache[key][record[key]] = record;
+ });
+ } else {
+ // write false for whatever hit with this locator
+ keys(locator).forEach(function(key) {
+ recordCache[key][locator[key]] = false;
+ });
+ }
+}
+
+function getDomainRecord(domainId) {
+ if (!(domainId in _getCache().records.id)) {
+ _updateCache({id: domainId});
+ }
+ var record = _getCache().records.id[domainId];
+ return (record ? record : null);
+}
+
+function getDomainRecordFromSubdomain(subDomain) {
+ subDomain = subDomain.toLowerCase();
+ if (!(subDomain in _getCache().records.subDomain)) {
+ _updateCache({subDomain: subDomain});
+ }
+ var record = _getCache().records.subDomain[subDomain];
+ return (record ? record : null);
+}
+
+/** returns id of newly created subDomain */
+function createNewSubdomain(subDomain, orgName) {
+ var id = sqlobj.insert('pro_domains', {subDomain: subDomain, orgName: orgName});
+ _updateCache({id: id});
+ return id;
+}
+
+function getPrivateNetworkDomainId() {
+ var r = getDomainRecordFromSubdomain('<<private-network>>');
+ if (!r) {
+ throw Error("<<private-network>> does not exist in the domains table!");
+ }
+ return r.id;
+}
+
+/** returns null if not found. */
+function getRequestDomainRecord() {
+ if (pne_utils.isPNE()) {
+ var r = getDomainRecord(getPrivateNetworkDomainId());
+ if (appjet.cache.fakePNE) {
+ r.orgName = "fake";
+ } else {
+ var licenseInfo = licensing.getLicense();
+ if (licenseInfo) {
+ r.orgName = licenseInfo.organizationName;
+ } else {
+ r.orgName = "Private Network Edition TRIAL";
+ }
+ }
+ return r;
+ } else {
+ var subDomain = pro_utils.getProRequestSubdomain();
+ var r = getDomainRecordFromSubdomain(subDomain);
+ return r;
+ }
+}
+
+/* throws exception if not pro domain request. */
+function getRequestDomainId() {
+ var r = getRequestDomainRecord();
+ if (!r) {
+ throw Error("Error getting request domain id.");
+ }
+ return r.id;
+}
+
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js b/trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js
new file mode 100644
index 0000000..ebcd227
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("stringutils");
+
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.lang.System.out.println");
+
+var _COOKIE_NAME = "PUAS";
+
+function dmesg(m) {
+ if (false) {
+ println("[pro-account-auto-sign-in]: "+m);
+ }
+}
+
+function checkAutoSignin() {
+ dmesg("checking auto sign-in...");
+ if (pro_accounts.isAccountSignedIn()) {
+ dmesg("account already signed in...");
+ // don't mess with already signed-in account
+ return;
+ }
+ var cookie = request.cookies[_COOKIE_NAME];
+ if (!cookie) {
+ dmesg("no auto-sign-in cookie found...");
+ return;
+ }
+ var record = sqlobj.selectSingle('pro_accounts_auto_signin', {cookie: cookie}, {});
+ if (!record) {
+ return;
+ }
+
+ var now = +(new Date);
+ if (+record.expires < now) {
+ sqlobj.deleteRows('pro_accounts_auto_signin', {id: record.id});
+ response.deleteCookie(_COOKIE_NAME);
+ dmesg("deleted expired record...");
+ return;
+ }
+ // do auto-signin (bypasses normal security)
+ dmesg("Doing auto sign in...");
+ var account = pro_accounts.getAccountById(record.accountId);
+ pro_accounts.signInSession(account);
+ response.redirect('/ep/account/sign-in?cont='+encodeURIComponent(request.url));
+}
+
+function setAutoSigninCookie(rememberMe) {
+ if (!pro_accounts.isAccountSignedIn()) {
+ return; // only call this function after account is already signed in.
+ }
+
+ var accountId = getSessionProAccount().id;
+ // delete any existing auto-signins for this account.
+ sqlobj.deleteRows('pro_accounts_auto_signin', {accountId: accountId});
+
+ // set this insecure cookie just to indicate that account is auto-sign-in-able
+ response.setCookie({
+ name: "ASIE",
+ value: (rememberMe ? "T" : "F"),
+ path: "/",
+ domain: request.domain,
+ expires: new Date(32503708800000), // year 3000
+ });
+
+ if (!rememberMe) {
+ return;
+ }
+
+ var cookie = stringutils.randomHash(16);
+ var now = +(new Date);
+ var expires = new Date(now + 1000*60*60*24*30); // 30 days
+ //var expires = new Date(now + 1000 * 60 * 5); // 2 minutes
+
+ sqlobj.insert('pro_accounts_auto_signin', {cookie: cookie, accountId: accountId, expires: expires});
+ response.setCookie({
+ name: _COOKIE_NAME,
+ value: cookie,
+ path: "/ep/account/",
+ domain: request.domain,
+ expires: new Date(32503708800000), // year 3000
+ secure: true
+ });
+}
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_accounts.js b/trunk/etherpad/src/etherpad/pro/pro_accounts.js
new file mode 100644
index 0000000..2024970
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_accounts.js
@@ -0,0 +1,496 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// library for pro accounts
+
+import("funhtml.*");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+import("email.sendEmail");
+import("cache_utils.syncedWithCache");
+import("stringutils.*");
+
+import("etherpad.globals.*");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.pro.domains");
+import("etherpad.control.pro.account_control");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_quotas");
+import("etherpad.pad.padusers");
+import("etherpad.log");
+import("etherpad.billing.team_billing");
+
+jimport("org.mindrot.BCrypt");
+jimport("java.lang.System.out.println");
+
+function _dmesg(m) {
+ if (!isProduction()) {
+ println(m);
+ }
+}
+
+function _computePasswordHash(p) {
+ var pwh;
+ pwh = BCrypt.hashpw(p, BCrypt.gensalt(10));
+ return pwh;
+}
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro_accounts.'+name, fn);
+}
+
+//----------------------------------------------------------------
+// validation
+//----------------------------------------------------------------
+
+function validateEmail(email) {
+ if (!email) { return "Email is required."; }
+ if (!isValidEmail(email)) { return "\""+email+"\" does not look like a valid email address."; }
+ return null;
+}
+
+function validateFullName(name) {
+ if (!name) { return "Full name is required."; }
+ if (name.length < 2) { return "Full name must be at least 2 characters."; }
+ return null;
+}
+
+function validatePassword(p) {
+ if (!p) { return "Password is required."; }
+ if (p.length < 6) { return "Passwords must be at least 6 characters."; }
+ return null;
+}
+
+function validateEmailDomainPair(email, domainId) {
+ // TODO: make sure the same email address cannot exist more than once within
+ // the same domainid.
+}
+
+/* if domainId is null, then use domainId of current request. */
+function createNewAccount(domainId, fullName, email, password, isAdmin) {
+ if (!domainId) {
+ domainId = domains.getRequestDomainId();
+ }
+ email = trim(email);
+ isAdmin = !!isAdmin; // convert to bool
+
+ // validation
+ var e;
+ e = validateEmail(email); if (e) { throw Error(e); }
+ e = validateFullName(fullName); if (e) { throw Error(e); }
+ e = validatePassword(password); if (e) { throw Error(e); }
+
+ // xss normalization
+ fullName = toHTML(fullName);
+
+ // make sure account does not already exist on this domain.
+ var ret = inTransaction(function() {
+ var existingAccount = getAccountByEmail(email, domainId);
+ if (existingAccount) {
+ throw Error("There is already an account with that email address.");
+ }
+ // No existing account. Proceed.
+ var now = new Date();
+ var account = {
+ domainId: domainId,
+ fullName: fullName,
+ email: email,
+ passwordHash: _computePasswordHash(password),
+ createdDate: now,
+ isAdmin: isAdmin
+ };
+ return sqlobj.insert('pro_accounts', account);
+ });
+
+ _withCache('does-domain-admin-exist', function(cache) {
+ delete cache[domainId];
+ });
+
+ pro_quotas.updateAccountUsageCount(domainId);
+ updateCachedActiveCount(domainId);
+
+ if (ret) {
+ log.custom('pro-accounts',
+ {type: "account-created",
+ accountId: ret,
+ domainId: domainId,
+ name: fullName,
+ email: email,
+ admin: isAdmin});
+ }
+
+ return ret;
+}
+
+function _checkAccess(account) {
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+ if (account.domainId != domains.getRequestDomainId()) {
+ throw Error("access denied");
+ }
+}
+
+function setPassword(account, newPass) {
+ _checkAccess(account);
+ var passHash = _computePasswordHash(newPass);
+ sqlobj.update('pro_accounts', {id: account.id}, {passwordHash: passHash});
+ markDirtySessionAccount(account.id);
+}
+
+function setTempPassword(account, tempPass) {
+ _checkAccess(account);
+ var tempPassHash = _computePasswordHash(tempPass);
+ sqlobj.update('pro_accounts', {id: account.id}, {tempPassHash: tempPassHash});
+ markDirtySessionAccount(account.id);
+}
+
+function setEmail(account, newEmail) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {email: newEmail});
+ markDirtySessionAccount(account.id);
+}
+
+function setFullName(account, newName) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {fullName: newName});
+ markDirtySessionAccount(account.id);
+}
+
+function setIsAdmin(account, newVal) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {isAdmin: newVal});
+ markDirtySessionAccount(account.id);
+}
+
+function setDeleted(account) {
+ _checkAccess(account);
+ if (!isNumeric(account.id)) {
+ throw new Error("Invalid account id: "+account.id);
+ }
+ sqlobj.update('pro_accounts', {id: account.id}, {isDeleted: true});
+ markDirtySessionAccount(account.id);
+ pro_quotas.updateAccountUsageCount(account.domainId);
+ updateCachedActiveCount(account.domainId);
+
+ log.custom('pro-accounts',
+ {type: "account-deleted",
+ accountId: account.id,
+ domainId: account.domainId,
+ name: account.fullName,
+ email: account.email,
+ admin: account.isAdmin,
+ createdDate: account.createdDate.getTime()});
+}
+
+//----------------------------------------------------------------
+
+function doesAdminExist() {
+ var domainId = domains.getRequestDomainId();
+ return _withCache('does-domain-admin-exist', function(cache) {
+ if (cache[domainId] === undefined) {
+ _dmesg("cache miss for doesAdminExist (domainId="+domainId+")");
+ var admins = sqlobj.selectMulti('pro_accounts', {domainId: domainId, isAdmin: true}, {});
+ cache[domainId] = (admins.length > 0);
+ }
+ return cache[domainId]
+ });
+}
+
+function getSessionProAccount() {
+ if (sessions.isAnEtherpadAdmin()) {
+ return getEtherpadAdminAccount();
+ }
+ var account = getSession().proAccount;
+ if (!account) {
+ return null;
+ }
+ if (account.isDeleted) {
+ delete getSession().proAccount;
+ return null;
+ }
+ return account;
+}
+
+function isAccountSignedIn() {
+ if (getSessionProAccount()) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function isAdminSignedIn() {
+ return isAccountSignedIn() && getSessionProAccount().isAdmin;
+}
+
+function requireAccount(message) {
+ if ((request.path == "/ep/account/sign-in") ||
+ (request.path == "/ep/account/sign-out") ||
+ (request.path == "/ep/account/guest-sign-in") ||
+ (request.path == "/ep/account/guest-knock") ||
+ (request.path == "/ep/account/forgot-password")) {
+ return;
+ }
+
+ function checkSessionAccount() {
+ if (!getSessionProAccount()) {
+ if (message) {
+ account_control.setSigninNotice(message);
+ }
+ response.redirect('/ep/account/sign-in?cont='+encodeURIComponent(request.url));
+ }
+ }
+
+ checkSessionAccount();
+
+ if (getSessionProAccount().domainId != domains.getRequestDomainId()) {
+ // This should theoretically never happen unless the account is spoofing cookies / trying to
+ // hack the site.
+ pro_utils.renderFramedMessage("Permission denied.");
+ response.stop();
+ }
+ // update dirty session account if necessary
+ _withCache('dirty-session-accounts', function(cache) {
+ var uid = getSessionProAccount().id;
+ if (cache[uid]) {
+ reloadSessionAccountData(uid);
+ cache[uid] = false;
+ }
+ });
+
+ // need to check again in case dirty update caused account to be marked
+ // deleted.
+ checkSessionAccount();
+}
+
+function requireAdminAccount() {
+ requireAccount();
+ if (!getSessionProAccount().isAdmin) {
+ pro_utils.renderFramedMessage("Permission denied.");
+ response.stop();
+ }
+}
+
+/* returns undefined on success, error string otherise. */
+function authenticateSignIn(email, password) {
+ var accountRecord = getAccountByEmail(email, null);
+ if (!accountRecord) {
+ return "Account not found: "+email;
+ }
+
+ if (BCrypt.checkpw(password, accountRecord.passwordHash) != true) {
+ return "Incorrect password. Please try again.";
+ }
+
+ signInSession(accountRecord);
+
+ return undefined; // success
+}
+
+function signOut() {
+ delete getSession().proAccount;
+}
+
+function authenticateTempSignIn(uid, tempPass) {
+ var emsg = "That password reset link that is no longer valid.";
+
+ var account = getAccountById(uid);
+ if (!account) {
+ return emsg+" (Account not found.)";
+ }
+ if (account.domainId != domains.getRequestDomainId()) {
+ return emsg+" (Wrong domain.)";
+ }
+ if (!account.tempPassHash) {
+ return emsg+" (Expired.)";
+ }
+ if (BCrypt.checkpw(tempPass, account.tempPassHash) != true) {
+ return emsg+" (Bad temp pass.)";
+ }
+
+ signInSession(account);
+
+ getSession().accountMessage = "Please choose a new password";
+ getSession().changePass = true;
+
+ response.redirect("/ep/account/");
+}
+
+function signInSession(account) {
+ account.lastLoginDate = new Date();
+ account.tempPassHash = null;
+ sqlobj.updateSingle('pro_accounts', {id: account.id}, account);
+ reloadSessionAccountData(account.id);
+ padusers.notifySignIn();
+}
+
+function listAllDomainAccounts(domainId) {
+ if (domainId === undefined) {
+ domainId = domains.getRequestDomainId();
+ }
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false}, {});
+ return records;
+}
+
+function listAllDomainAdmins(domainId) {
+ if (domainId === undefined) {
+ domainId = domains.getRequestDomainId();
+ }
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false, isAdmin: true},
+ {});
+ return records;
+}
+
+function getActiveCount(domainId) {
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false}, {});
+ return records.length;
+}
+
+/* getAccountById works for deleted and non-deleted accounts.
+ * The assumption is that cases whewre you look up an account by ID, you
+ * want the account info even if the account has been deleted. For
+ * example, when asking who created a pad.
+ */
+function getAccountById(accountId) {
+ var r = sqlobj.selectSingle('pro_accounts', {id: accountId});
+ if (r) {
+ return r;
+ } else {
+ return undefined;
+ }
+}
+
+/* getting an account by email only returns the account if it is
+ * not deleted. The assumption is that when you look up an account by
+ * email address, you only want active accounts. Furthermore, some
+ * deleted accounts may match a given email, but only one non-deleted
+ * account should ever match a single (email,domainId) pair.
+ */
+function getAccountByEmail(email, domainId) {
+ if (!domainId) {
+ domainId = domains.getRequestDomainId();
+ }
+ var r = sqlobj.selectSingle('pro_accounts', {domainId: domainId, email: email, isDeleted: false});
+ if (r) {
+ return r;
+ } else {
+ return undefined;
+ }
+}
+
+function getFullNameById(id) {
+ if (!id) {
+ return null;
+ }
+
+ return _withCache('names-by-id', function(cache) {
+ if (cache[id] === undefined) {
+ _dmesg("cache miss for getFullNameById (accountId="+id+")");
+ var r = getAccountById(id);
+ if (r) {
+ cache[id] = r.fullName;
+ } else {
+ cache[id] = false;
+ }
+ }
+ if (cache[id]) {
+ return cache[id];
+ } else {
+ return null;
+ }
+ });
+}
+
+function getTempSigninUrl(account, tempPass) {
+ return [
+ 'https://', httpsHost(pro_utils.getFullProHost()), '/ep/account/sign-in?',
+ 'uid=', account.id, '&tp=', tempPass
+ ].join('');
+}
+
+
+// TODO: this session account object storage / dirty cache is a
+// ridiculous hack. What we should really do is have a caching/access
+// layer for accounts similar to accessPad() and accessProPadMeta(), and
+// have that abstraction take care of caching and marking accounts as
+// dirty. This can be incorporated into getSessionProAccount(), and we
+// should actually refactor that into accessSessionProAccount().
+
+/* will force session data for this account to be updated next time that
+ * account requests a page. */
+function markDirtySessionAccount(uid) {
+ var domainId = domains.getRequestDomainId();
+
+ _withCache('dirty-session-accounts', function(cache) {
+ cache[uid] = true;
+ });
+ _withCache('names-by-id', function(cache) {
+ delete cache[uid];
+ });
+ _withCache('does-domain-admin-exist', function(cache) {
+ delete cache[domainId];
+ });
+}
+
+function reloadSessionAccountData(uid) {
+ if (!uid) {
+ uid = getSessionProAccount().id;
+ }
+ getSession().proAccount = getAccountById(uid);
+}
+
+function getAllAccountsWithEmail(email) {
+ var accountRecords = sqlobj.selectMulti('pro_accounts', {email: email, isDeleted: false}, {});
+ return accountRecords;
+}
+
+function getEtherpadAdminAccount() {
+ return {
+ id: 0,
+ isAdmin: true,
+ fullName: "ETHERPAD ADMIN",
+ email: "support@pad.spline.inf.fu-berlin.de",
+ domainId: domains.getRequestDomainId(),
+ isDeleted: false
+ };
+}
+
+function getCachedActiveCount(domainId) {
+ return _withCache('user-counts.'+domainId, function(c) {
+ if (!c.count) {
+ c.count = getActiveCount(domainId);
+ }
+ return c.count;
+ });
+}
+
+function updateCachedActiveCount(domainId) {
+ _withCache('user-counts.'+domainId, function(c) {
+ c.count = getActiveCount(domainId);
+ });
+}
+
+
+
+
+
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_config.js b/trunk/etherpad/src/etherpad/pro/pro_config.js
new file mode 100644
index 0000000..d2d119f
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_config.js
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_utils");
+
+function _guessSiteName() {
+ var x = request.host.split('.')[0];
+ x = (x.charAt(0).toUpperCase() + x.slice(1));
+ return x;
+}
+
+function _getDefaultConfig() {
+ return {
+ siteName: _guessSiteName(),
+ alwaysHttps: false,
+ defaultPadText: renderTemplateAsString("misc/pad_default.ejs")
+ };
+}
+
+// must be fast! gets called per request, on every request.
+function getConfig() {
+ if (!pro_utils.isProDomainRequest()) {
+ return null;
+ }
+
+ if (!appjet.cache.pro_config) {
+ appjet.cache.pro_config = {};
+ }
+
+ var domainId = domains.getRequestDomainId();
+ if (!appjet.cache.pro_config[domainId]) {
+ reloadConfig();
+ }
+
+ return appjet.cache.pro_config[domainId];
+}
+
+function reloadConfig() {
+ var domainId = domains.getRequestDomainId();
+ var config = _getDefaultConfig();
+ var records = sqlobj.selectMulti('pro_config', {domainId: domainId}, {});
+
+ records.forEach(function(r) {
+ var name = r.name;
+ var val = fastJSON.parse(r.jsonVal).x;
+ config[name] = val;
+ });
+
+ if (!appjet.cache.pro_config) {
+ appjet.cache.pro_config = {};
+ }
+
+ appjet.cache.pro_config[domainId] = config;
+}
+
+function setConfigVal(name, val) {
+ var domainId = domains.getRequestDomainId();
+ var jsonVal = fastJSON.stringify({x: val});
+
+ var r = sqlobj.selectSingle('pro_config', {domainId: domainId, name: name});
+ if (!r) {
+ sqlobj.insert('pro_config',
+ {domainId: domainId, name: name, jsonVal: jsonVal});
+ } else {
+ sqlobj.update('pro_config',
+ {name: name, domainId: domainId},
+ {jsonVal: jsonVal});
+ }
+
+ reloadConfig();
+}
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_pad_db.js b/trunk/etherpad/src/etherpad/pro/pro_pad_db.js
new file mode 100644
index 0000000..dbb412c
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_pad_db.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("cache_utils.syncedWithCache");
+import("stringutils");
+
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_pad_editors");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.lang.System.out.println");
+
+
+// TODO: actually implement the cache part
+
+// NOTE: must return a deep-CLONE of the actual record, because caller
+// may proceed to mutate the returned record.
+
+function _makeRecord(r) {
+ if (!r) {
+ return null;
+ }
+ r.proAttrs = {};
+ if (r.proAttrsJson) {
+ r.proAttrs = fastJSON.parse(r.proAttrsJson);
+ }
+ if (!r.proAttrs.editors) {
+ r.proAttrs.editors = [];
+ }
+ r.proAttrs.editors.sort();
+ return r;
+}
+
+function getSingleRecord(domainId, localPadId) {
+ // TODO: make clone
+ // TODO: use cache
+ var record = sqlobj.selectSingle('pro_padmeta', {domainId: domainId, localPadId: localPadId});
+ return _makeRecord(record);
+}
+
+function update(padRecord) {
+ // TODO: use cache
+
+ padRecord.proAttrsJson = fastJSON.stringify(padRecord.proAttrs);
+ delete padRecord.proAttrs;
+
+ sqlobj.update('pro_padmeta', {id: padRecord.id}, padRecord);
+}
+
+
+//--------------------------------------------------------------------------------
+// create/edit/destory events
+//--------------------------------------------------------------------------------
+
+function onCreatePad(pad) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ var data = {
+ domainId: padutils.getDomainId(pad.getId()),
+ localPadId: padutils.getLocalPadId(pad),
+ createdDate: new Date(),
+ };
+
+ if (getSessionProAccount()) {
+ data.creatorId = getSessionProAccount().id;
+ }
+
+ sqlobj.insert('pro_padmeta', data);
+}
+
+// Not a normal part of the UI. This is only called from admin interface,
+// and thus should actually destroy all record of the pad.
+function onDestroyPad(pad) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ sqlobj.deleteRows('pro_padmeta', {
+ domainId: padutils.getDomainId(pad.getId()),
+ localPadId: padutils.getLocalPadId(pad)
+ });
+}
+
+// Called within the context of a comet post.
+function onEditPad(pad, padAuthorId) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ var editorId = undefined;
+ if (getSessionProAccount()) {
+ editorId = getSessionProAccount().id;
+ }
+
+ if (!(editorId && (editorId > 0))) {
+ return; // etherpad admins
+ }
+
+ pro_pad_editors.notifyEdit(
+ padutils.getDomainId(pad.getId()),
+ padutils.getLocalPadId(pad),
+ editorId,
+ new Date()
+ );
+}
+
+//--------------------------------------------------------------------------------
+// accessing the pad list.
+//--------------------------------------------------------------------------------
+
+function _makeRecordList(lis) {
+ lis.forEach(function(r) {
+ r = _makeRecord(r);
+ });
+ return lis;
+}
+
+function listMyPads() {
+ var domainId = domains.getRequestDomainId();
+ var accountId = getSessionProAccount().id;
+
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, creatorId: accountId, isDeleted: false, isArchived: false});
+ return _makeRecordList(padlist);
+}
+
+function listAllDomainPads() {
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: false});
+ return _makeRecordList(padlist);
+}
+
+function listArchivedPads() {
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: true});
+ return _makeRecordList(padlist);
+}
+
+function listPadsByEditor(editorId) {
+ editorId = Number(editorId);
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: false});
+ padlist = _makeRecordList(padlist);
+ padlist = padlist.filter(function(p) {
+ // NOTE: could replace with binary search to speed things up,
+ // since we know that editors array is sorted.
+ return (p.proAttrs.editors.indexOf(editorId) >= 0);
+ });
+ return padlist;
+}
+
+function listLiveDomainPads() {
+ var thisDomainId = domains.getRequestDomainId();
+ var allLivePadIds = collab_server.getAllPadsWithConnections();
+ var livePadMap = {};
+
+ allLivePadIds.forEach(function(globalId) {
+ if (padutils.isProPadId(globalId)) {
+ var domainId = padutils.getDomainId(globalId);
+ var localId = padutils.globalToLocalId(globalId);
+ if (domainId == thisDomainId) {
+ livePadMap[localId] = true;
+ }
+ }
+ });
+
+ var padList = listAllDomainPads();
+ padList = padList.filter(function(p) {
+ return (!!livePadMap[p.localPadId]);
+ });
+
+ return padList;
+}
+
+//--------------------------------------------------------------------------------
+// misc utils
+//--------------------------------------------------------------------------------
+
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro-padmeta.'+name, fn);
+}
+
+function _withDomainCache(domainId, name, fn) {
+ return _withCache(name+"."+domainId, fn);
+}
+
+
+
+// returns the next pad ID to use for a newly-created pad on this domain.
+function getNextPadId() {
+ var domainId = domains.getRequestDomainId();
+ return _withDomainCache(domainId, 'padcounters', function(c) {
+ var ret;
+ if (c.x === undefined) {
+ c.x = _getLargestNumericPadId(domainId) + 1;
+ }
+ while (sqlobj.selectSingle('pro_padmeta', {domainId: domainId, localPadId: String(c.x)})) {
+ c.x++;
+ }
+ ret = c.x;
+ c.x++;
+ return ret;
+ });
+}
+
+function _getLargestNumericPadId(domainId) {
+ var max = 0;
+ var allPads = listAllDomainPads();
+ allPads.forEach(function(p) {
+ if (stringutils.isNumeric(p.localPadId)) {
+ max = Math.max(max, Number(p.localPadId));
+ }
+ });
+ return max;
+}
+
+
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_pad_editors.js b/trunk/etherpad/src/etherpad/pro/pro_pad_editors.js
new file mode 100644
index 0000000..a90f05b
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_pad_editors.js
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("execution");
+import("jsutils.*");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.log");
+
+var _DOMAIN_EDIT_WRITE_INTERVAL = 2000; // 2 seconds
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro-padmeta.'+name, fn);
+}
+
+function _withDomainCache(domainId, name, fn) {
+ return _withCache(name+"."+domainId, fn);
+}
+
+
+function onStartup() {
+ execution.initTaskThreadPool("pro-padmeta-edits", 1);
+}
+
+function onShutdown() {
+ var success = execution.shutdownAndWaitOnTaskThreadPool("pro-padmeta-edits", 4000);
+ if (!success) {
+ log.warn("Warning: pro.padmeta failed to flush pad edits on shutdown.");
+ }
+}
+
+function notifyEdit(domainId, localPadId, editorId, editTime) {
+ if (!editorId) {
+ // guest editors
+ return;
+ }
+ _withDomainCache(domainId, "edits", function(c) {
+ if (!c[localPadId]) {
+ c[localPadId] = {
+ lastEditorId: editorId,
+ lastEditTime: editTime,
+ recentEditors: []
+ };
+ }
+ var info = c[localPadId];
+ if (info.recentEditors.indexOf(editorId) < 0) {
+ info.recentEditors.push(editorId);
+ }
+ });
+ _flushPadEditsEventually(domainId);
+}
+
+
+function _flushPadEditsEventually(domainId) {
+ // Make sure there is a recurring edit-writer for this domain
+ _withDomainCache(domainId, "recurring-edit-writers", function(c) {
+ if (!c[domainId]) {
+ flushEditsNow(domainId);
+ c[domainId] = true;
+ }
+ });
+}
+
+function flushEditsNow(domainId) {
+ if (!appjet.cache.shutdownHandlerIsRunning) {
+ execution.scheduleTask("pro-padmeta-edits", "proPadmetaFlushEdits",
+ _DOMAIN_EDIT_WRITE_INTERVAL, [domainId]);
+ }
+
+ _withDomainCache(domainId, "edits", function(edits) {
+ var padIdList = keys(edits);
+ padIdList.forEach(function(localPadId) {
+ _writePadEditsToDbNow(domainId, localPadId, edits[localPadId]);
+ delete edits[localPadId];
+ });
+ });
+}
+
+function _writePadEditsToDbNow(domainId, localPadId, editInfo) {
+ var globalPadId = padutils.makeGlobalId(domainId, localPadId);
+ pro_padmeta.accessProPad(globalPadId, function(propad) {
+ propad.setLastEditedDate(editInfo.lastEditTime);
+ propad.setLastEditor(editInfo.lastEditorId);
+ editInfo.recentEditors.forEach(function(eid) {
+ propad.addEditor(eid);
+ });
+ });
+}
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_padlist.js b/trunk/etherpad/src/etherpad/pro/pro_padlist.js
new file mode 100644
index 0000000..73b179c
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_padlist.js
@@ -0,0 +1,289 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("jsutils.*");
+import("stringutils");
+
+import("etherpad.utils.*");
+import("etherpad.helpers");
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_accounts");
+
+function _getColumnMeta() {
+ // returns map of {id --> {
+ // title,
+ // sortFn(a,b),
+ // render(p)
+ // }
+
+ function _dateNum(d) {
+ if (!d) {
+ return 0;
+ }
+ return -1 * (+d);
+ }
+
+
+ var cols = {};
+
+ function addAvailableColumn(id, cdata) {
+ if (!cdata.render) {
+ cdata.render = function(p) {
+ return p[id];
+ };
+ }
+ if (!cdata.cmpFn) {
+ cdata.cmpFn = function(a,b) {
+ return cmp(a[id], b[id]);
+ };
+ }
+ cdata.id = id;
+ cols[id] = cdata;
+ }
+
+ addAvailableColumn('public', {
+ title: "",
+ render: function(p) {
+ // TODO: implement an icon with hover text that says public vs.
+ // private
+ return "";
+ },
+ cmpFn: function(a,b) {
+ return 0; // not sort-able
+ }
+ });
+ addAvailableColumn('secure', {
+ title: "",
+ render: function(p) {
+ if (p.password) {
+ return IMG({src: '/static/img/may09/padlock.gif'});
+ } else {
+ return "";
+ }
+ },
+ cmpFn: function(a,b) {
+ return cmp(a.password, b.password);
+ }
+ });
+ addAvailableColumn('title', {
+ title: "Title",
+ render: function(p) {
+ var t = padutils.getProDisplayTitle(p.localPadId, p.title);
+ return A({href: "/"+p.localPadId}, t);
+ },
+ sortFn: function(a, b) {
+ return cmp(padutils.getProDisplayTitle(a.localPadId, a.title),
+ padutils.getProDisplayTitle(b.localPadId, b.title));
+ }
+ });
+ addAvailableColumn('creatorId', {
+ title: "Creator",
+ render: function(p) {
+ return pro_accounts.getFullNameById(p.creatorId);
+ },
+ sortFn: function(a, b) {
+ return cmp(pro_accounts.getFullNameById(a.creatorId),
+ pro_accounts.getFullNameById(b.creatorId));
+ }
+ });
+ addAvailableColumn('createdDate', {
+ title: "Created",
+ render: function(p) {
+ return timeAgo(p.createdDate);
+ },
+ sortFn: function(a, b) {
+ return cmp(_dateNum(a.createdDate), _dateNum(b.createdDate));
+ }
+ });
+ addAvailableColumn('lastEditorId', {
+ title: "Last Editor",
+ render: function(p) {
+ if (p.lastEditorId) {
+ return pro_accounts.getFullNameById(p.lastEditorId);
+ } else {
+ return "";
+ }
+ },
+ sortFn: function(a, b) {
+ var a_ = a.lastEditorId ? pro_accounts.getFullNameById(a.lastEditorId) : "ZZZZZZZZZZ";
+ var b_ = b.lastEditorId ? pro_accounts.getFullNameById(b.lastEditorId) : "ZZZZZZZZZZ";
+ return cmp(a_, b_);
+ }
+ });
+
+ addAvailableColumn('editors', {
+ title: "Editors",
+ render: function(p) {
+ var editors = [];
+ p.proAttrs.editors.forEach(function(editorId) {
+ editors.push([editorId, pro_accounts.getFullNameById(editorId)]);
+ });
+ editors.sort(function(a,b) { return cmp(a[1], b[1]); });
+ var sp = SPAN();
+ for (var i = 0; i < editors.length; i++) {
+ if (i > 0) {
+ sp.push(", ");
+ }
+ sp.push(A({href: "/ep/padlist/edited-by?editorId="+editors[i][0]}, editors[i][1]));
+ }
+ return sp;
+ }
+ });
+
+ addAvailableColumn('lastEditedDate', {
+ title: "Last Edited",
+ render: function(p) {
+ if (p.lastEditedDate) {
+ return timeAgo(p.lastEditedDate);
+ } else {
+ return "never";
+ }
+ },
+ sortFn: function(a,b) {
+ return cmp(_dateNum(a.lastEditedDate), _dateNum(b.lastEditedDate));
+ }
+ });
+ addAvailableColumn('localPadId', {
+ title: "Path",
+ });
+ addAvailableColumn('actions', {
+ title: "",
+ render: function(p) {
+ return DIV({className: "gear-drop", id: "pad-gear-"+p.id}, " ");
+ }
+ });
+
+ addAvailableColumn('connectedUsers', {
+ title: "Connected Users",
+ render: function(p) {
+ var names = [];
+ padutils.accessPadLocal(p.localPadId, function(pad) {
+ var userList = collab_server.getConnectedUsers(pad);
+ userList.forEach(function(u) {
+ if (collab_server.translateSpecialKey(u.specialKey) != 'invisible') {
+ // excludes etherpad admin user
+ names.push(u.name);
+ }
+ });
+ });
+ return names.join(", ");
+ }
+ });
+
+ return cols;
+}
+
+function _sortPads(padList) {
+ var meta = _getColumnMeta();
+ var sortId = _getCurrentSortId();
+ var reverse = false;
+ if (sortId.charAt(0) == '-') {
+ reverse = true;
+ sortId = sortId.slice(1);
+ }
+ padList.sort(function(a,b) { return cmp(a.localPadId, b.localPadId); });
+ padList.sort(function(a,b) { return meta[sortId].sortFn(a, b); });
+ if (reverse) { padList.reverse(); }
+}
+
+function _addClientVars(padList) {
+ var padTitles = {}; // maps localPadId -> title
+ var localPadIds = {}; // maps padmetaId -> localPadId
+ padList.forEach(function(p) {
+ padTitles[p.localPadId] = stringutils.toHTML(padutils.getProDisplayTitle(p.localPadId, p.title));
+ localPadIds[p.id] = p.localPadId;
+ });
+ helpers.addClientVars({
+ padTitles: padTitles,
+ localPadIds: localPadIds
+ });
+}
+
+function _getCurrentSortId() {
+ return request.params.sortBy || "lastEditedDate";
+}
+
+function _renderColumnHeader(m) {
+ var sp = SPAN();
+ var sortBy = _getCurrentSortId();
+ if (m.sortFn) {
+ var d = {sortBy: m.id};
+ var arrow = "";
+ if (sortBy == m.id) {
+ d.sortBy = ("-"+m.id);
+ arrow = html("&#8595;");
+ }
+ if (sortBy == ("-"+m.id)) {
+ arrow = html("&#8593;");
+ }
+ sp.push(arrow, " ", A({href: qpath(d)}, m.title));
+ } else {
+ sp.push(m.title);
+ }
+ return sp;
+}
+
+function renderPadList(padList, columnIds, limit) {
+ _sortPads(padList);
+ _addClientVars(padList);
+
+ if (limit && (limit < padList.length)) {
+ padList = padList.slice(0,limit);
+ }
+
+ var showSecurityInfo = false;
+ padList.forEach(function(p) {
+ if (p.password && p.password.length > 0) { showSecurityInfo = true; }
+ });
+ if (!showSecurityInfo && (columnIds[0] == 'secure')) {
+ columnIds.shift();
+ }
+
+ var columnMeta = _getColumnMeta();
+
+ var t = TABLE({id: "padtable", cellspacing:"0", cellpadding:"0"});
+ var toprow = TR({className: "toprow"});
+ columnIds.forEach(function(cid) {
+ toprow.push(TH(_renderColumnHeader(columnMeta[cid])));
+ });
+ t.push(toprow);
+
+ padList.forEach(function(p) {
+ // Note that this id is always numeric, and is the actual
+ // canonical padmeta id.
+ var row = TR({id: 'padmeta-'+p.id});
+ var first = true;
+ for (var i = 0; i < columnIds.length; i++) {
+ var cid = columnIds[i];
+ var m = columnMeta[cid];
+ var classes = cid;
+ if (i == 0) {
+ classes += (" first");
+ }
+ if (i == (columnIds.length - 1)) {
+ classes += (" last");
+ }
+ row.push(TD({className: classes}, m.render(p)));
+ }
+ t.push(row);
+ });
+
+ return t;
+}
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_padmeta.js b/trunk/etherpad/src/etherpad/pro/pro_padmeta.js
new file mode 100644
index 0000000..6f911b2
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_padmeta.js
@@ -0,0 +1,111 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("cache_utils.syncedWithCache");
+import("sync");
+
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_pad_db");
+
+function _doWithProPadLock(domainId, localPadId, func) {
+ var lockName = ["pro-pad", domainId, localPadId].join("/");
+ return sync.doWithStringLock(lockName, func);
+}
+
+function accessProPad(globalPadId, fn) {
+ // retrieve pad from cache
+ var domainId = padutils.getDomainId(globalPadId);
+ if (!domainId) {
+ throw Error("not a pro pad: "+globalPadId);
+ }
+ var localPadId = padutils.globalToLocalId(globalPadId);
+ var padRecord = pro_pad_db.getSingleRecord(domainId, localPadId);
+
+ return _doWithProPadLock(domainId, localPadId, function() {
+ var isDirty = false;
+
+ var proPad = {
+ exists: function() { return !!padRecord; },
+ getDomainId: function() { return domainId; },
+ getLocalPadId: function() { return localPadId; },
+ getGlobalId: function() { return globalPadId; },
+ getDisplayTitle: function() { return padutils.getProDisplayTitle(localPadId, padRecord.title); },
+ setTitle: function(newTitle) {
+ padRecord.title = newTitle;
+ isDirty = true;
+ },
+ isDeleted: function() { return padRecord.isDeleted; },
+ markDeleted: function() {
+ padRecord.isDeleted = true;
+ isDirty = true;
+ },
+ getPassword: function() { return padRecord.password; },
+ setPassword: function(newPass) {
+ if (newPass == "") {
+ newPass = null;
+ }
+ padRecord.password = newPass;
+ isDirty = true;
+ },
+ isArchived: function() { return padRecord.isArchived; },
+ markArchived: function() {
+ padRecord.isArchived = true;
+ isDirty = true;
+ },
+ unmarkArchived: function() {
+ padRecord.isArchived = false;
+ isDirty = true;
+ },
+ setLastEditedDate: function(d) {
+ padRecord.lastEditedDate = d;
+ isDirty = true;
+ },
+ addEditor: function(editorId) {
+ var es = String(editorId);
+ if (es && es.length > 0 && stringutils.isNumeric(editorId)) {
+ if (padRecord.proAttrs.editors.indexOf(editorId) < 0) {
+ padRecord.proAttrs.editors.push(editorId);
+ padRecord.proAttrs.editors.sort();
+ }
+ isDirty = true;
+ }
+ },
+ setLastEditor: function(editorId) {
+ var es = String(editorId);
+ if (es && es.length > 0 && stringutils.isNumeric(editorId)) {
+ padRecord.lastEditorId = editorId;
+ this.addEditor(editorId);
+ isDirty = true;
+ }
+ }
+ };
+
+ var ret = fn(proPad);
+
+ if (isDirty) {
+ pro_pad_db.update(padRecord);
+ }
+
+ return ret;
+ });
+}
+
+function accessProPadLocal(localPadId, fn) {
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ return accessProPad(globalPadId, fn);
+}
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_quotas.js b/trunk/etherpad/src/etherpad/pro/pro_quotas.js
new file mode 100644
index 0000000..ed69e1c
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_quotas.js
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("stringutils.startsWith");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+
+import("etherpad.billing.team_billing");
+import("etherpad.globals.*");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.domains");
+import("etherpad.sessions.getSession");
+import("etherpad.store.checkout");
+
+function _createRecordIfNecessary(domainId) {
+ inTransaction(function() {
+ var r = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ if (!r) {
+ var count = pro_accounts.getActiveCount(domainId);
+ sqlobj.insert('pro_account_usage', {
+ domainId: domainId,
+ count: count,
+ lastReset: (new Date),
+ lastUpdated: (new Date)
+ });
+ }
+ });
+}
+
+/**
+ * Called after a successful payment has been made.
+ * Effect: counts the current number of domain accounts and stores that
+ * as the current account usage count.
+ */
+function resetAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var newCount = pro_accounts.getActiveCount(domainId);
+ sqlobj.update(
+ 'pro_account_usage',
+ {domainId: domainId},
+ {count: newCount, lastUpdated: (new Date), lastReset: (new Date)}
+ );
+}
+
+/**
+ * Returns the max number of accounts that have existed simultaneously
+ * since the last reset.
+ */
+function getAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var record = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ return record.count;
+}
+
+
+/**
+ * Updates the current account usage count by computing:
+ * usage_count = max(current_accounts, usage_count)
+ */
+function updateAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var record = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ var currentCount = pro_accounts.getActiveCount(domainId);
+ var newCount = Math.max(record.count, currentCount);
+ sqlobj.update(
+ 'pro_account_usage',
+ {domainId: domainId},
+ {count: newCount, lastUpdated: (new Date)}
+ );
+}
+
+// called per request
+
+function _generateGlobalBillingNotice(status) {
+ if (status == team_billing.CURRENT) {
+ return;
+ }
+ var notice = SPAN();
+ if (status == team_billing.PAST_DUE) {
+ var suspensionDate = checkout.formatDate(team_billing.getDomainSuspensionDate(domains.getRequestDomainId()));
+ notice.push(
+ "Warning: your account is past due and will be suspended on ",
+ suspensionDate, ".");
+ }
+ if (status == team_billing.SUSPENDED) {
+ notice.push(
+ "Warning: your account is suspended because it is more than ",
+ team_billing.GRACE_PERIOD_DAYS, " days past due.");
+ }
+
+ if (pro_accounts.isAdminSignedIn()) {
+ notice.push(" ", A({href: "/ep/admin/billing/"}, "Manage billing"), ".");
+ } else {
+ getSession().billingProblem = "Payment is required for sites with more than "+PRO_FREE_ACCOUNTS+" accounts.";
+ notice.push(" ", "Please ",
+ A({href: "/ep/payment-required"}, "contact a site administrator"), ".");
+ }
+ request.cache.globalProNotice = notice;
+}
+
+function perRequestBillingCheck() {
+ // Do nothing if under the free account limit.
+ var activeAccounts = pro_accounts.getCachedActiveCount(domains.getRequestDomainId());
+ if (activeAccounts <= PRO_FREE_ACCOUNTS) {
+ return;
+ }
+
+ var status = team_billing.getDomainStatus(domains.getRequestDomainId());
+ _generateGlobalBillingNotice(status);
+
+ // now see if we need to block the request because of account
+ // suspension
+ if (status != team_billing.SUSPENDED) {
+ return;
+ }
+ // These path sare still OK if a suspension is on.
+ if ((startsWith(request.path, "/ep/account/") ||
+ startsWith(request.path, "/ep/admin/") ||
+ startsWith(request.path, "/ep/pro-help/") ||
+ startsWith(request.path, "/ep/payment-required"))) {
+ return;
+ }
+
+ getSession().billingProblem = "Payment is required for sites with more than "+PRO_FREE_ACCOUNTS+" accounts.";
+ response.redirect('/ep/payment-required');
+}
+
diff --git a/trunk/etherpad/src/etherpad/pro/pro_utils.js b/trunk/etherpad/src/etherpad/pro/pro_utils.js
new file mode 100644
index 0000000..1dc2468
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pro/pro_utils.js
@@ -0,0 +1,165 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("stringutils.startsWith");
+
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_quotas");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+
+import("etherpad.control.pro.pro_main_control");
+
+jimport("java.lang.System.out.println");
+
+function _stripComet(x) {
+ if (x.indexOf('.comet.') > 0) {
+ x = x.split('.comet.')[1];
+ }
+ return x;
+}
+
+function getProRequestSubdomain() {
+ var d = _stripComet(request.domain);
+ return d.split('.')[0];
+}
+
+function getRequestSuperdomain() {
+ var parts = request.domain.split('.');
+ while (parts.length > 0) {
+ var domain = parts.join('.');
+ if (SUPERDOMAINS[domain]) {
+ return domain;
+ }
+ parts.shift(); // Remove next level
+ }
+}
+
+function isProDomainRequest() {
+ // the result of this function never changes within the same request.
+ var c = appjet.requestCache;
+ if (c.isProDomainRequest === undefined) {
+ c.isProDomainRequest = _computeIsProDomainRequest();
+ }
+ return c.isProDomainRequest;
+}
+
+function _computeIsProDomainRequest() {
+ if (pne_utils.isPNE()) {
+ return true;
+ }
+
+ var domain = _stripComet(request.domain);
+
+ if (SUPERDOMAINS[domain]) {
+ return false;
+ }
+
+ var requestSuperdomain = getRequestSuperdomain();
+
+ if (SUPERDOMAINS[requestSuperdomain]) {
+ // now see if this subdomain is actually in our database.
+ if (domains.getRequestDomainRecord()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return false;
+}
+
+function preDispatchAccountCheck() {
+ // if account is not logged in, redirect to /ep/account/login
+ //
+ // if it's PNE and there is no admin account, allow them to create an admin
+ // account.
+
+ if (pro_main_control.isActivationAllowed()) {
+ return;
+ }
+
+ if (!pro_accounts.doesAdminExist()) {
+ if (request.path != '/ep/account/create-admin-account') {
+ // should only happen for eepnet installs
+ response.redirect('/ep/account/create-admin-account');
+ }
+ } else {
+ pro_accounts.requireAccount();
+ }
+
+ pro_quotas.perRequestBillingCheck();
+}
+
+function renderFramedMessage(m) {
+ renderFramedHtml(
+ DIV(
+ {style: "font-size: 2em; padding: 2em; margin: 4em; border: 1px solid #ccc; background: #e6e6e6;"},
+ m));
+}
+
+function getFullProDomain() {
+ // TODO: have a special config param for this? --etherpad.canonicalDomain
+ return request.domain;
+}
+
+// domain, including port if necessary
+function getFullProHost() {
+ var h = getFullProDomain();
+ var parts = request.host.split(':');
+ if (parts.length > 1) {
+ h += (':' + parts[1]);
+ }
+ return h;
+}
+
+function getFullSuperdomainHost() {
+ if (isProDomainRequest()) {
+ var h = getRequestSuperdomain()
+ var parts = request.host.split(':');
+ if (parts.length > 1) {
+ h += (':' + parts[1]);
+ }
+ return h;
+ } else {
+ return request.host;
+ }
+}
+
+function getEmailFromAddr() {
+ var fromDomain = 'pad.spline.inf.fu-berlin.de';
+ if (pne_utils.isPNE()) {
+ fromDomain = getFullProDomain();
+ }
+ return ('"EtherPad" <noreply@'+fromDomain+'>');
+}
+
+function renderGlobalProNotice() {
+ if (request.cache.globalProNotice) {
+ return DIV({className: 'global-pro-notice'},
+ request.cache.globalProNotice);
+ } else {
+ return "";
+ }
+}
+