diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/pro')
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/domains.js | 141 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js | 101 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_accounts.js | 496 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_config.js | 92 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_pad_db.js | 232 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_pad_editors.js | 104 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_padlist.js | 289 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_padmeta.js | 111 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_quotas.js | 141 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_utils.js | 165 |
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("↓"); + } + if (sortBy == ("-"+m.id)) { + arrow = html("↑"); + } + 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 ""; + } +} + |