summaryrefslogtreecommitdiffstats
path: root/trunk/etherpad/src/etherpad/pad
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/etherpad/src/etherpad/pad')
-rw-r--r--trunk/etherpad/src/etherpad/pad/activepads.js52
-rw-r--r--trunk/etherpad/src/etherpad/pad/chatarchive.js67
-rw-r--r--trunk/etherpad/src/etherpad/pad/dbwriter.js338
-rw-r--r--trunk/etherpad/src/etherpad/pad/easysync2migration.js675
-rw-r--r--trunk/etherpad/src/etherpad/pad/exporthtml.js383
-rw-r--r--trunk/etherpad/src/etherpad/pad/importhtml.js230
-rw-r--r--trunk/etherpad/src/etherpad/pad/model.js651
-rw-r--r--trunk/etherpad/src/etherpad/pad/noprowatcher.js110
-rw-r--r--trunk/etherpad/src/etherpad/pad/pad_migrations.js206
-rw-r--r--trunk/etherpad/src/etherpad/pad/pad_security.js237
-rw-r--r--trunk/etherpad/src/etherpad/pad/padevents.js170
-rw-r--r--trunk/etherpad/src/etherpad/pad/padusers.js397
-rw-r--r--trunk/etherpad/src/etherpad/pad/padutils.js154
-rw-r--r--trunk/etherpad/src/etherpad/pad/revisions.js103
14 files changed, 3773 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/pad/activepads.js b/trunk/etherpad/src/etherpad/pad/activepads.js
new file mode 100644
index 0000000..07f5e2e
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/activepads.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("jsutils.cmp");
+
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+var HISTORY_SIZE = 100;
+
+function _getMap() {
+ if (!appjet.cache['activepads']) {
+ appjet.cache['activepads'] = {
+ map: new LimitedSizeMapping(HISTORY_SIZE)
+ };
+ }
+ return appjet.cache['activepads'].map;
+}
+
+function touch(padId) {
+ _getMap().put(padId, +(new Date));
+}
+
+function getActivePads() {
+ var m = _getMap();
+ var a = m.listAllKeys().toArray();
+ var activePads = [];
+ for (var i = 0; i < a.length; i++) {
+ activePads.push({
+ padId: a[i],
+ timestamp: m.get(a[i])
+ });
+ }
+
+ activePads.sort(function(a,b) { return cmp(b.timestamp,a.timestamp); });
+ return activePads;
+}
+
+
+
diff --git a/trunk/etherpad/src/etherpad/pad/chatarchive.js b/trunk/etherpad/src/etherpad/pad/chatarchive.js
new file mode 100644
index 0000000..2f8e33a
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/chatarchive.js
@@ -0,0 +1,67 @@
+/**
+ * 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("etherpad.log");
+
+jimport("java.lang.System.out.println");
+
+function onChatMessage(pad, senderUserInfo, msg) {
+ pad.appendChatMessage({
+ name: senderUserInfo.name,
+ userId: senderUserInfo.userId,
+ time: +(new Date),
+ lineText: msg.lineText
+ });
+}
+
+function getRecentChatBlock(pad, howMany) {
+ var numMessages = pad.getNumChatMessages();
+ var firstToGet = Math.max(0, numMessages - howMany);
+
+ return getChatBlock(pad, firstToGet, numMessages);
+}
+
+function getChatBlock(pad, start, end) {
+ if (start < 0) {
+ start = 0;
+ }
+ if (end > pad.getNumChatMessages()) {
+ end = pad.getNumChatMessages();
+ }
+
+ var historicalAuthorData = {};
+ var lines = [];
+ var block = {start: start, end: end,
+ historicalAuthorData: historicalAuthorData,
+ lines: lines};
+
+ for(var i=start; i<end; i++) {
+ var x = pad.getChatMessage(i);
+ var userId = x.userId;
+ if (! historicalAuthorData[userId]) {
+ historicalAuthorData[userId] = (pad.getAuthorData(userId) || {});
+ }
+ lines.push({
+ name: x.name,
+ time: x.time,
+ userId: x.userId,
+ lineText: x.lineText
+ });
+ }
+
+ return block;
+} \ No newline at end of file
diff --git a/trunk/etherpad/src/etherpad/pad/dbwriter.js b/trunk/etherpad/src/etherpad/pad/dbwriter.js
new file mode 100644
index 0000000..233622b
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/dbwriter.js
@@ -0,0 +1,338 @@
+/**
+ * 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("profiler");
+
+import("etherpad.pad.model");
+import("etherpad.pad.model.accessPadGlobal");
+import("etherpad.log");
+import("etherpad.utils");
+
+jimport("net.appjet.oui.exceptionlog");
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("java.lang.System.out.println");
+
+var MIN_WRITE_INTERVAL_MS = 2000; // 2 seconds
+var MIN_WRITE_DELAY_NOTIFY_MS = 2000; // 2 seconds
+var AGE_FOR_PAD_FLUSH_MS = 5*60*1000; // 5 minutes
+var DBUNWRITABLE_WRITE_DELAY_MS = 30*1000; // 30 seconds
+
+// state is { constant: true }, { constant: false }, { trueAfter: timeInMs }
+function setWritableState(state) {
+ _dbwriter().dbWritable = state;
+}
+
+function getWritableState() {
+ return _dbwriter().dbWritable;
+}
+
+function isDBWritable() {
+ return _isDBWritable();
+}
+
+function _isDBWritable() {
+ var state = _dbwriter().dbWritable;
+ if (typeof state != "object") {
+ return true;
+ }
+ else if (state.constant !== undefined) {
+ return !! state.constant;
+ }
+ else if (state.trueAfter !== undefined) {
+ return (+new Date()) > state.trueAfter;
+ }
+ else return true;
+}
+
+function getWritableStateDescription(state) {
+ var v = _isDBWritable();
+ var restOfMessage = "";
+ if (state.trueAfter !== undefined) {
+ var now = +new Date();
+ var then = state.trueAfter;
+ var diffSeconds = java.lang.String.format("%.1f", Math.abs(now - then)/1000);
+ if (now < then) {
+ restOfMessage = " until "+diffSeconds+" seconds from now";
+ }
+ else {
+ restOfMessage = " since "+diffSeconds+" seconds ago";
+ }
+ }
+ return v+restOfMessage;
+}
+
+function _dbwriter() {
+ return appjet.cache.dbwriter;
+}
+
+function onStartup() {
+ appjet.cache.dbwriter = {};
+ var dbwriter = _dbwriter();
+ dbwriter.pendingWrites = new ConcurrentHashMap();
+ dbwriter.scheduledFor = new ConcurrentHashMap(); // padId --> long
+ dbwriter.dbWritable = { constant: true };
+
+ execution.initTaskThreadPool("dbwriter", 4);
+ // we don't wait for scheduled tasks in the infreq pool to run and complete
+ execution.initTaskThreadPool("dbwriter_infreq", 1);
+
+ _scheduleCheckForStalePads();
+}
+
+function _scheduleCheckForStalePads() {
+ execution.scheduleTask("dbwriter_infreq", "checkForStalePads", AGE_FOR_PAD_FLUSH_MS, []);
+}
+
+function onShutdown() {
+ log.info("Doing final DB writes before shutdown...");
+ var success = execution.shutdownAndWaitOnTaskThreadPool("dbwriter", 10000);
+ if (! success) {
+ log.warn("ERROR! DB WRITER COULD NOT SHUTDOWN THREAD POOL!");
+ }
+}
+
+function _logException(e) {
+ var exc = utils.toJavaException(e);
+ log.warn("writeAllToDB: Error writing to SQL! Written to exceptions.log: "+exc);
+ log.logException(exc);
+ exceptionlog.apply(exc);
+}
+
+function taskFlushPad(padId, reason) {
+ var dbwriter = _dbwriter();
+ if (! _isDBWritable()) {
+ // DB is unwritable, delay
+ execution.scheduleTask("dbwriter_infreq", "flushPad", DBUNWRITABLE_WRITE_DELAY_MS, [padId, reason]);
+ return;
+ }
+
+ model.accessPadGlobal(padId, function(pad) {
+ writePadNow(pad, true);
+ }, "r");
+
+ log.info("taskFlushPad: flushed "+padId+(reason?(" (reason: "+reason+")"):''));
+}
+
+function taskWritePad(padId) {
+ var dbwriter = _dbwriter();
+ if (! _isDBWritable()) {
+ // DB is unwritable, delay
+ dbwriter.scheduledFor.put(padId, (+(new Date)+DBUNWRITABLE_WRITE_DELAY_MS));
+ execution.scheduleTask("dbwriter", "writePad", DBUNWRITABLE_WRITE_DELAY_MS, [padId]);
+ return;
+ }
+
+ profiler.reset();
+ var t1 = profiler.rcb("lock wait");
+ model.accessPadGlobal(padId, function(pad) {
+ t1();
+ _dbwriter().pendingWrites.remove(padId); // do this first
+
+ var success = false;
+ try {
+ var t2 = profiler.rcb("write");
+ writePadNow(pad);
+ t2();
+
+ success = true;
+ }
+ finally {
+ if (! success) {
+ log.warn("DB WRITER FAILED TO WRITE PAD: "+padId);
+ }
+ profiler.print();
+ }
+ }, "r");
+}
+
+function taskCheckForStalePads() {
+ // do this first
+ _scheduleCheckForStalePads();
+
+ if (! _isDBWritable()) return;
+
+ // get "active" pads into an array
+ var padIter = appjet.cache.pads.meta.keySet().iterator();
+ var padList = [];
+ while (padIter.hasNext()) { padList.push(padIter.next()); }
+
+ var numStale = 0;
+
+ for (var i = 0; i < padList.length; i++) {
+ if (! _isDBWritable()) break;
+ var p = padList[i];
+ if (model.isPadLockHeld(p)) {
+ // skip it, don't want to lock up stale pad flusher
+ }
+ else {
+ accessPadGlobal(p, function(pad) {
+ if (pad.exists()) {
+ var padAge = (+new Date()) - pad._meta.status.lastAccess;
+ if (padAge > AGE_FOR_PAD_FLUSH_MS) {
+ writePadNow(pad, true);
+ numStale++;
+ }
+ }
+ }, "r");
+ }
+ }
+
+ log.info("taskCheckForStalePads: flushed "+numStale+" stale pads");
+}
+
+function notifyPadDirty(padId) {
+ var dbwriter = _dbwriter();
+ if (! dbwriter.pendingWrites.containsKey(padId)) {
+ dbwriter.pendingWrites.put(padId, "pending");
+ dbwriter.scheduledFor.put(padId, (+(new Date)+MIN_WRITE_INTERVAL_MS));
+ execution.scheduleTask("dbwriter", "writePad", MIN_WRITE_INTERVAL_MS, [padId]);
+ }
+}
+
+function scheduleFlushPad(padId, reason) {
+ execution.scheduleTask("dbwriter_infreq", "flushPad", 0, [padId, reason]);
+}
+
+/*function _dbwriterLoopBody(executor) {
+ try {
+ var info = writeAllToDB(executor);
+ if (!info.boring) {
+ log.info("DB writer: "+info.toSource());
+ }
+ java.lang.Thread.sleep(Math.max(0, MIN_WRITE_INTERVAL_MS - info.elapsed));
+ }
+ catch (e) {
+ _logException(e);
+ java.lang.Thread.sleep(MIN_WRITE_INTERVAL_MS);
+ }
+}
+
+function _startInThread(name, func) {
+ (new Thread(new Runnable({
+ run: function() {
+ func();
+ }
+ }), name)).start();
+}
+
+function killDBWriterThreadAndWait() {
+ appjet.cache.abortDBWriter = true;
+ while (appjet.cache.runningDBWriter) {
+ java.lang.Thread.sleep(100);
+ }
+}*/
+
+/*function writeAllToDB(executor, andFlush) {
+ if (!executor) {
+ executor = new ScheduledThreadPoolExecutor(NUM_WRITER_THREADS);
+ }
+
+ profiler.reset();
+ var startWriteTime = profiler.time();
+ var padCount = new AtomicInteger(0);
+ var writeCount = new AtomicInteger(0);
+ var removeCount = new AtomicInteger(0);
+
+ // get pads into an array
+ var padIter = appjet.cache.pads.meta.keySet().iterator();
+ var padList = [];
+ while (padIter.hasNext()) { padList.push(padIter.next()); }
+
+ var latch = new CountDownLatch(padList.length);
+
+ for (var i = 0; i < padList.length; i++) {
+ _spawnCall(executor, function(p) {
+ try {
+ var padWriteResult = {};
+ accessPadGlobal(p, function(pad) {
+ if (pad.exists()) {
+ padCount.getAndIncrement();
+ padWriteResult = writePad(pad, andFlush);
+ if (padWriteResult.didWrite) writeCount.getAndIncrement();
+ if (padWriteResult.didRemove) removeCount.getAndIncrement();
+ }
+ }, "r");
+ } catch (e) {
+ _logException(e);
+ } finally {
+ latch.countDown();
+ }
+ }, padList[i]);
+ }
+
+ // wait for them all to finish
+ latch.await();
+
+ var endWriteTime = profiler.time();
+ var elapsed = Math.round((endWriteTime - startWriteTime)/1000)/1000;
+ var interesting = (writeCount.get() > 0 || removeCount.get() > 0);
+
+ var obj = {padCount:padCount.get(), writeCount:writeCount.get(), elapsed:elapsed, removeCount:removeCount.get()};
+ if (! interesting) obj.boring = true;
+ if (interesting) {
+ profiler.record("writeAll", profiler.time()-startWriteTime);
+ profiler.print();
+ }
+
+ return obj;
+}*/
+
+function writePadNow(pad, andFlush) {
+ var didWrite = false;
+ var didRemove = false;
+
+ if (pad.exists()) {
+ var dbUpToDate = false;
+ if (pad._meta.status.dirty) {
+ /*log.info("Writing pad "+pad.getId());*/
+ pad._meta.status.dirty = false;
+ //var t1 = +new Date();
+ pad.writeToDB();
+ //var t2 = +new Date();
+ didWrite = true;
+
+ //log.info("Wrote pad "+pad.getId()+" in "+(t2-t1)+" ms.");
+
+ var now = +(new Date);
+ var sched = _dbwriter().scheduledFor.get(pad.getId());
+ if (sched) {
+ var delay = now - sched;
+ if (delay > MIN_WRITE_DELAY_NOTIFY_MS) {
+ log.warn("dbwriter["+pad.getId()+"] behind schedule by "+delay+"ms");
+ }
+ _dbwriter().scheduledFor.remove(pad.getId());
+ }
+ }
+ if (andFlush) {
+ // remove from cache
+ model.removeFromMemory(pad);
+ didRemove = true;
+ }
+ }
+ return {didWrite:didWrite, didRemove:didRemove};
+}
+
+/*function _spawnCall(executor, func, varargs) {
+ var args = Array.prototype.slice.call(arguments, 2);
+ var that = this;
+ executor.schedule(new Runnable({
+ run: function() {
+ func.apply(that, args);
+ }
+ }), 0, TimeUnit.MICROSECONDS);
+}*/
+
diff --git a/trunk/etherpad/src/etherpad/pad/easysync2migration.js b/trunk/etherpad/src/etherpad/pad/easysync2migration.js
new file mode 100644
index 0000000..c2a1523
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/easysync2migration.js
@@ -0,0 +1,675 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import("etherpad.collab.ace.easysync1");
+import("etherpad.collab.ace.easysync2");
+import("sqlbase.sqlbase");
+import("fastJSON");
+import("sqlbase.sqlcommon.*");
+import("etherpad.collab.ace.contentcollector.sanitizeUnicode");
+
+function _getPadStringArrayNumId(padId, arrayName) {
+ var stmnt = "SELECT NUMID FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " WHERE ("+btquote("ID")+" = ?)";
+
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setString(1, padId);
+ var resultSet = pstmnt.executeQuery();
+ return closing(resultSet, function() {
+ if (! resultSet.next()) {
+ return -1;
+ }
+ return resultSet.getInt(1);
+ });
+ });
+ });
+}
+
+function _getEntirePadStringArray(padId, arrayName) {
+ var numId = _getPadStringArrayNumId(padId, arrayName);
+ if (numId < 0) {
+ return [];
+ }
+
+ var stmnt = "SELECT PAGESTART, OFFSETS, DATA FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " WHERE ("+btquote("NUMID")+" = ?)";
+
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setInt(1, numId);
+ var resultSet = pstmnt.executeQuery();
+ return closing(resultSet, function() {
+ var array = [];
+ while (resultSet.next()) {
+ var pageStart = resultSet.getInt(1);
+ var lengthsString = resultSet.getString(2);
+ var dataString = resultSet.getString(3);
+ var dataIndex = 0;
+ var arrayIndex = pageStart;
+ lengthsString.split(',').forEach(function(len) {
+ if (len) {
+ len = Number(len);
+ array[arrayIndex] = dataString.substr(dataIndex, len);
+ dataIndex += len;
+ }
+ arrayIndex++;
+ });
+ }
+ return array;
+ });
+ });
+ });
+}
+
+function _overwriteEntirePadStringArray(padId, arrayName, array) {
+ var numId = _getPadStringArrayNumId(padId, arrayName);
+ if (numId < 0) {
+ // generate numId
+ withConnection(function(conn) {
+ var ps = conn.prepareStatement("INSERT INTO "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " ("+btquote("ID")+") VALUES (?)",
+ java.sql.Statement.RETURN_GENERATED_KEYS);
+ closing(ps, function() {
+ ps.setString(1, padId);
+ ps.executeUpdate();
+ var keys = ps.getGeneratedKeys();
+ if ((! keys) || (! keys.next())) {
+ throw new Error("Couldn't generate key for "+arrayName+" table for pad "+padId);
+ }
+ closing(keys, function() {
+ numId = keys.getInt(1);
+ });
+ });
+ });
+ }
+
+ withConnection(function(conn) {
+
+ var stmnt1 = "DELETE FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " WHERE ("+btquote("NUMID")+" = ?)";
+ var pstmnt1 = conn.prepareStatement(stmnt1);
+ closing(pstmnt1, function() {
+ pstmnt1.setInt(1, numId);
+ pstmnt1.executeUpdate();
+ });
+
+ var PAGE_SIZE = 20;
+ var numPages = Math.floor((array.length-1) / PAGE_SIZE + 1);
+
+ var PAGES_PER_BATCH = 20;
+ var curPage = 0;
+
+ while (curPage < numPages) {
+ var stmnt2 = "INSERT INTO "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " ("+btquote("NUMID")+", "+btquote("PAGESTART")+", "+btquote("OFFSETS")+
+ ", "+btquote("DATA")+") VALUES (?, ?, ?, ?)";
+ var pstmnt2 = conn.prepareStatement(stmnt2);
+ closing(pstmnt2, function() {
+ for(var n=0;n<PAGES_PER_BATCH && curPage < numPages;n++) {
+ var pageStart = curPage*PAGE_SIZE;
+ var r = pageStart;
+ var lengthPieces = [];
+ var dataPieces = [];
+ for(var i=0;i<PAGE_SIZE;i++) {
+ var str = (array[r] || '');
+ dataPieces.push(str);
+ lengthPieces.push(String(str.length || ''));
+ r++;
+ }
+ var lengthsString = lengthPieces.join(',');
+ var dataString = dataPieces.join('');
+ pstmnt2.setInt(1, numId);
+ pstmnt2.setInt(2, pageStart);
+ pstmnt2.setString(3, lengthsString);
+ pstmnt2.setString(4, dataString);
+ pstmnt2.addBatch();
+
+ curPage++;
+ }
+ pstmnt2.executeBatch();
+ });
+ }
+ });
+
+}
+
+function _getEntirePadJSONArray(padId, arrayName) {
+ var array = _getEntirePadStringArray(padId, arrayName);
+ for(var k in array) {
+ if (array[k]) {
+ array[k] = fastJSON.parse(array[k]);
+ }
+ }
+ return array;
+}
+
+function _overwriteEntirePadJSONArray(padId, arrayName, objArray) {
+ var array = [];
+ for(var k in objArray) {
+ if (objArray[k]) {
+ array[k] = fastJSON.stringify(objArray[k]);
+ }
+ }
+ _overwriteEntirePadStringArray(padId, arrayName, array);
+}
+
+function _getMigrationPad(padId) {
+ var oldRevs = _getEntirePadStringArray(padId, "revs");
+ var oldRevMeta = _getEntirePadJSONArray(padId, "revmeta");
+ var oldAuthors = _getEntirePadJSONArray(padId, "authors");
+ var oldMeta = sqlbase.getJSON("PAD_META", padId);
+
+ var oldPad = {
+ getHeadRevisionNumber: function() {
+ return oldMeta.head;
+ },
+ getRevisionChangesetString: function(r) {
+ return oldRevs[r];
+ },
+ getRevisionAuthor: function(r) {
+ return oldMeta.numToAuthor[oldRevMeta[r].a];
+ },
+ getId: function() { return padId; },
+ getKeyRevisionNumber: function(r) {
+ return Math.floor(r / oldMeta.keyRevInterval) * oldMeta.keyRevInterval;
+ },
+ getInternalRevisionText: function(r) {
+ if (r != oldPad.getKeyRevisionNumber(r)) {
+ throw new Error("Assertion error: "+r+" != "+oldPad.getKeyRevisionNumber(r));
+ }
+ return oldRevMeta[r].atext.text;
+ },
+ _meta: oldMeta,
+ getAuthorArrayEntry: function(n) {
+ return oldAuthors[n];
+ },
+ getRevMetaArrayEntry: function(r) {
+ return oldRevMeta[r];
+ }
+ };
+
+ var apool = new easysync2.AttribPool();
+ var newRevMeta = [];
+ var newAuthors = [];
+ var newRevs = [];
+ var metaPropsToDelete = [];
+
+ var newPad = {
+ pool: function() { return apool; },
+ setAuthorArrayEntry: function(n, obj) {
+ newAuthors[n] = obj;
+ },
+ setRevMetaArrayEntry: function(r, obj) {
+ newRevMeta[r] = obj;
+ },
+ setRevsArrayEntry: function(r, cs) {
+ newRevs[r] = cs;
+ },
+ deleteMetaProp: function(propName) {
+ metaPropsToDelete.push(propName);
+ }
+ };
+
+ function writeToDB() {
+ var newMeta = {};
+ for(var k in oldMeta) {
+ newMeta[k] = oldMeta[k];
+ }
+ metaPropsToDelete.forEach(function(p) {
+ delete newMeta[p];
+ });
+
+ sqlbase.putJSON("PAD_META", padId, newMeta);
+ sqlbase.putJSON("PAD_APOOL", padId, apool.toJsonable());
+
+ _overwriteEntirePadStringArray(padId, "revs", newRevs);
+ _overwriteEntirePadJSONArray(padId, "revmeta", newRevMeta);
+ _overwriteEntirePadJSONArray(padId, "authors", newAuthors);
+ }
+
+ return {oldPad:oldPad, newPad:newPad, writeToDB:writeToDB};
+}
+
+function migratePad(padId) {
+
+ var mpad = _getMigrationPad(padId);
+ var oldPad = mpad.oldPad;
+ var newPad = mpad.newPad;
+
+ var headRev = oldPad.getHeadRevisionNumber();
+ var txt = "\n";
+ var newChangesets = [];
+ var newChangesetAuthorNums = [];
+ var cumCs = easysync2.Changeset.identity(1);
+
+ var pool = newPad.pool();
+
+ var isExtraFinalNewline = false;
+
+ function authorToNewNum(author) {
+ return pool.putAttrib(['author',author||'']);
+ }
+
+ //S var oldTotalChangesetSize = 0;
+ //S var newTotalChangesetSize = 0;
+ //S function stringSize(str) {
+ //S return new java.lang.String(str).getBytes("UTF-8").length;
+ //S }
+
+ //P var diffTotals = [];
+ for(var r=0;r<=headRev;r++) {
+ //P var times = [];
+ //P times.push(+new Date);
+ var author = oldPad.getRevisionAuthor(r);
+ //P times.push(+new Date);
+ newChangesetAuthorNums.push(authorToNewNum(author));
+
+ var newCs, newText;
+ if (r == 0) {
+ newText = oldPad.getInternalRevisionText(0);
+ newCs = getInitialChangeset(newText, pool, author);
+ //S oldTotalChangesetSize += stringSize(pad.getRevisionChangesetString(0));
+ }
+ else {
+ var oldCsStr = oldPad.getRevisionChangesetString(r);
+ //S oldTotalChangesetSize += stringSize(oldCsStr);
+ //P times.push(+new Date);
+ var oldCs = easysync1.Changeset.decodeFromString(oldCsStr);
+ //P times.push(+new Date);
+
+ /*var newTextFromOldCs = oldCs.applyToText(txt);
+ if (newTextFromOldCs.charAt(newTextFromOldCs.length-1) != '\n') {
+ var e = new Error("Violation of final newline property at revision "+r);
+ e.finalNewlineMissing = true;
+ throw e;
+ }*/
+ //var newCsNewTxt1 = upgradeChangeset(oldCs, txt, pool, author);
+ var oldIsExtraFinalNewline = isExtraFinalNewline;
+ var newCsNewTxt2 = upgradeChangeset(oldCs, txt, pool, author, isExtraFinalNewline);
+ //P times.push(+new Date);
+ /*if (newCsNewTxt1[1] != newCsNewTxt2[1]) {
+ _putFile(newCsNewTxt1[1], "/tmp/file1");
+ _putFile(newCsNewTxt2[1], "/tmp/file2");
+ throw new Error("MISMATCH 1");
+ }
+ if (newCsNewTxt1[0] != newCsNewTxt2[0]) {
+ _putFile(newCsNewTxt1[0], "/tmp/file1");
+ _putFile(newCsNewTxt2[0], "/tmp/file2");
+ throw new Error("MISMATCH 0");
+ }*/
+ newCs = newCsNewTxt2[0];
+ newText = newCsNewTxt2[1];
+ isExtraFinalNewline = newCsNewTxt2[2];
+
+ /*if (oldIsExtraFinalNewline || isExtraFinalNewline) {
+ System.out.print("\nnewline fix for rev "+r+"/"+headRev+"... ");
+ }*/
+ }
+
+ var oldText = txt;
+ newChangesets.push(newCs);
+ txt = newText;
+ //System.out.println(easysync2.Changeset.toBaseTen(cumCs)+" * "+
+ //easysync2.Changeset.toBaseTen(newCs));
+ /*cumCs = easysync2.Changeset.checkRep(easysync2.Changeset.compose(cumCs, newCs));
+ if (easysync2.Changeset.applyToText(cumCs, "\n") != txt) {
+ throw new Error("cumCs mismatch");
+ }*/
+
+ //P times.push(+new Date);
+
+ easysync2.Changeset.checkRep(newCs);
+ //P times.push(+new Date);
+ var origText = txt;
+ if (isExtraFinalNewline) {
+ origText = origText.slice(0, -1);
+ }
+ if (r == oldPad.getKeyRevisionNumber(r)) {
+ // only check key revisions (and final outcome), for speed
+ if (oldPad.getInternalRevisionText(r) != origText) {
+ var expected = oldPad.getInternalRevisionText(r);
+ var actual = origText;
+ //_putFile(expected, "/tmp/file1");
+ //_putFile(actual, "/tmp/file2");
+ //_putFile(oldText, "/tmp/file3");
+ //java.lang.System.out.println(String(oldCs));
+ //java.lang.System.out.println(easysync2.Changeset.toBaseTen(newCs));
+ throw new Error("Migration mismatch, pad "+padId+", revision "+r);
+ }
+ }
+
+ //S newTotalChangesetSize += stringSize(newCs);
+
+ //P if (r > 0) {
+ //P var diffs = [];
+ //P for(var i=0;i<times.length-1;i++) {
+ //P diffs[i] = times[i+1] - times[i];
+ //P }
+ //P for(var i=0;i<diffs.length;i++) {
+ //P diffTotals[i] = (diffTotals[i] || 0) + diffs[i]*1000/headRev;
+ //P }
+ //P }
+ }
+ //P System.out.println(String(diffTotals));
+
+ //S System.out.println("New data is "+(newTotalChangesetSize/oldTotalChangesetSize*100)+
+ //S "% size of old data (average "+(newTotalChangesetSize/(headRev+1))+
+ //S " bytes instead of "+(oldTotalChangesetSize/(headRev+1))+")");
+
+ var atext = easysync2.Changeset.makeAText("\n");
+ for(var r=0; r<=headRev; r++) {
+ newPad.setRevsArrayEntry(r, newChangesets[r]);
+
+ atext = easysync2.Changeset.applyToAText(newChangesets[r], atext, pool);
+
+ var rm = oldPad.getRevMetaArrayEntry(r);
+ rm.a = newChangesetAuthorNums[r];
+ if (rm.atext) {
+ rm.atext = easysync2.Changeset.cloneAText(atext);
+ }
+ newPad.setRevMetaArrayEntry(r, rm);
+ }
+
+ var newAuthors = [];
+ var newAuthorDatas = [];
+ for(var k in oldPad._meta.numToAuthor) {
+ var n = Number(k);
+ var authorData = oldPad.getAuthorArrayEntry(n) || {};
+ var authorName = oldPad._meta.numToAuthor[n];
+ var newAuthorNum = pool.putAttrib(['author',authorName]);
+ newPad.setAuthorArrayEntry(newAuthorNum, authorData);
+ }
+
+ newPad.deleteMetaProp('numToAuthor');
+ newPad.deleteMetaProp('authorToNum');
+
+ mpad.writeToDB();
+}
+
+function getInitialChangeset(txt, pool, author) {
+ var txt2 = txt.substring(0, txt.length-1); // strip off final newline
+
+ var assem = easysync2.Changeset.smartOpAssembler();
+ assem.appendOpWithText('+', txt2, pool && author && [['author',author]], pool);
+ assem.endDocument();
+ return easysync2.Changeset.pack(1, txt2.length+1, assem.toString(), txt2);
+}
+
+function upgradeChangeset(cs, inputText, pool, author, isExtraNewlineInSource) {
+ var attribs = '';
+ if (pool && author) {
+ attribs = '*'+easysync2.Changeset.numToString(pool.putAttrib(['author', author]));
+ }
+
+ function keepLastCharacter(c) {
+ if (! c[c.length-1] && c[c.length-3] + c[c.length-2] >= (c.oldLen() - 1)) {
+ c[c.length-2] = c.oldLen() - c[c.length-3];
+ }
+ else {
+ c.push(c.oldLen() - 1, 1, "");
+ }
+ }
+
+ var isExtraNewlineInOutput = false;
+ if (isExtraNewlineInSource) {
+ cs[1] += 1; // oldLen ++
+ }
+ if ((cs[cs.length-1] && cs[cs.length-1].slice(-1) != '\n') ||
+ ((! cs[cs.length-1]) && inputText.charAt(cs[cs.length-3] + cs[cs.length-2] - 1) != '\n')) {
+ // new text won't end with newline!
+ if (isExtraNewlineInSource) {
+ keepLastCharacter(cs);
+ }
+ else {
+ cs[cs.length-1] += "\n";
+ }
+ cs[2] += 1; // newLen ++
+ isExtraNewlineInOutput = true;
+ }
+
+ var oldLen = cs.oldLen();
+ var newLen = cs.newLen();
+
+ // final-newline-preserving modifications to changeset {{{
+ // These fixes are required for changesets that don't respect the
+ // new rule that the final newline of the document not be touched,
+ // and also for changesets tweaked above. It is important that the
+ // fixed changesets obey all the constraints on version 1 changesets
+ // so that they may become valid version 2 changesets.
+ {
+ function collapsePotentialEmptyLastTake(c) {
+ if (c[c.length-2] == 0 && c.length > 6) {
+ if (! c[c.length-1]) {
+ // last strip doesn't take or insert now
+ c.length -= 3;
+ }
+ else {
+ // the last two strips should be merged
+ // e.g. fo\n -> rock\nbar\n: then in this block,
+ // "Changeset,3,9,0,0,r,1,1,ck,2,0,\nbar" becomes
+ // "Changeset,3,9,0,0,r,1,1,ck\nbar"
+ c[c.length-4] += c[c.length-1];
+ c.length -= 3;
+ }
+ }
+ }
+ var lastStripStart = cs[cs.length-3];
+ var lastStripTake = cs[cs.length-2];
+ var lastStripInsert = cs[cs.length-1];
+ if (lastStripStart + lastStripTake == oldLen && lastStripInsert) {
+ // an insert at end
+ // e.g. foo\n -> foo\nbar\n:
+ // "Changeset,4,8,0,4,bar\n" becomes "Changeset,4,8,0,3,\nbar,3,1,"
+ // first make the previous newline part of the insertion
+ cs[cs.length-2] -= 1;
+ cs[cs.length-1] = '\n'+cs[cs.length-1].slice(0,-1);
+ collapsePotentialEmptyLastTake(cs);
+ keepLastCharacter(cs);
+ }
+ else if (lastStripStart + lastStripTake < oldLen && ! lastStripInsert) {
+ // ends with pure deletion
+ cs[cs.length-2] -= 1;
+ collapsePotentialEmptyLastTake(cs);
+ keepLastCharacter(cs);
+ }
+ else if (lastStripStart + lastStripTake < oldLen) {
+ // ends with replacement
+ cs[cs.length-1] = cs[cs.length-1].slice(0,-1);
+ keepLastCharacter(cs);
+ }
+ }
+ // }}}
+
+ var ops = [];
+ var lastOpcode = '';
+ function appendOp(opcode, text, startChar, endChar) {
+ function num(n) {
+ return easysync2.Changeset.numToString(n);
+ }
+ var lines = 0;
+ var lastNewlineEnd = startChar;
+ for (;;) {
+ var index = text.indexOf('\n', lastNewlineEnd);
+ if (index < 0 || index >= endChar) {
+ break;
+ }
+ lines++;
+ lastNewlineEnd = index+1;
+ }
+ var a = (opcode == '+' ? attribs : '');
+ var multilineChars = (lastNewlineEnd - startChar);
+ var seqLength = endChar - startChar;
+ var op = '';
+ if (lines > 0) {
+ op = [a, '|', num(lines), opcode, num(multilineChars)].join('');
+ }
+ if (multilineChars < seqLength) {
+ op += [a, opcode, num(seqLength - multilineChars)].join('');
+ }
+ if (op) {
+ // we reorder a single - and a single +
+ if (opcode == '-' && lastOpcode == '+') {
+ ops.splice(ops.length-1, 0, op);
+ }
+ else {
+ ops.push(op);
+ lastOpcode = opcode;
+ }
+ }
+ }
+
+ var oldPos = 0;
+
+ var textPieces = [];
+ var charBankPieces = [];
+ cs.eachStrip(function(start, take, insert) {
+ if (start > oldPos) {
+ appendOp('-', inputText, oldPos, start);
+ }
+ if (take) {
+ if (start+take < oldLen || insert) {
+ appendOp('=', inputText, start, start+take);
+ }
+ textPieces.push(inputText.substring(start, start+take));
+ }
+ if (insert) {
+ appendOp('+', insert, 0, insert.length);
+ textPieces.push(insert);
+ charBankPieces.push(insert);
+ }
+ oldPos = start+take;
+ });
+ // ... and no final deletions after the newline fixing.
+
+ var newCs = easysync2.Changeset.pack(oldLen, newLen, ops.join(''),
+ sanitizeUnicode(charBankPieces.join('')));
+ var newText = textPieces.join('');
+
+ return [newCs, newText, isExtraNewlineInOutput];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// unicode issues: 5SaYQp7cKV
+
+// // hard-coded just for testing; any pad is allowed to have corruption.
+// var newlineCorruptedPads = [
+// '0OCGFKkjDv', '14dWjOiOxP', '1LL8XQCBjC', '1jMnjEEK6e', '21',
+// '23DytOPN7d', '32YzfdT2xS', '3E6GB7l7FZ', '3Un8qaCfJh', '3YAj3rC9em',
+// '3vY2eaHSw5', '4834RRTLlg', '4Fm1iVSTWI', '5NpTNqWHGC', '7FYNSdYQVa',
+// '7RZCbvgw1z', '8EVpyN6HyY', '8P5mPRxPVr', '8aHFRmLxKR', '8dsj9eGQfP',
+// 'BSoGobOJZZ', 'Bf0uVghKy0', 'C2f3umStKd', 'CHlu2CA8F3', 'D2WEwgvg1W',
+// 'DNLTpuP2wl', 'DwNpm2TDgu', 'EKPByZ3EGZ', 'FwQxu6UKQx', 'HUn9O34rFl',
+// 'JKZhxMo20E', 'JVjuukL42N', 'JVuBlWxaxL', 'Jmw5lPNYcl', 'KnZHz6jE2P',
+// 'Luyp6ylbgR', 'MB6lPoN1eI', 'McsCrQUM6c', 'NWIuVobIw9', 'OKERTLQCCn',
+// 'OchiOchi', 'OfhKHCB8jJ', 'OkM3Jv3XY9', 'PX5Z89mx29', 'PdmKQIvOEd',
+// 'R9NQNB66qt', 'RvULFSvCbV', 'RyLJC6Qo1x', 'SBlKLwr2Ag', 'SavD72Q9P7',
+// 'SfXyxseAeF', 'TTGZ4yO2PI', 'U3U7rT3d6w', 'UFmqpQIDAi', 'V7Or0QQk4m',
+// 'VPCM5ReAQm', 'VvIYHzIJUY', 'W0Ccc3BVGb', 'Wv3cGgSgjg', 'WwVPgaZUK5',
+// 'WyIFUJXfm5', 'XxESEsgQ6R', 'Yc5Yq3WCuU', 'ZRqCFaRx6h', 'ZepX6TLFbD',
+// 'bSeImT5po4', 'bqIlTkFDiH', 'btt9vNPSQ9', 'c97YJj8PSN', 'd9YV3sypKF',
+// 'eDzzkrwDRU', 'eFQJZWclzo', 'eaz44OhFDu', 'ehKkx1YpLA', 'ep',
+// 'foNq3v3e9T', 'form6rooma', 'fqhtIHG0Ii', 'fvZyCRZjv2', 'gZnadICPYV',
+// 'gvGXtMKhQk', 'h7AYuTxUOd', 'hc1UZSti3J', 'hrFQtae2jW', 'i8rENUZUMu',
+// 'iFW9dceEmh', 'iRNEc8SlOc', 'jEDsDgDlaK', 'jo8ngXlSJh', 'kgJrB9Gh2M',
+// 'klassennetz76da2661f8ceccfe74faf97d25a4b418',
+// 'klassennetzf06d4d8176d0804697d9650f836cb1f7', 'lDHgmfyiSu',
+// 'mA1cbvxFwA', 'mSJpW1th29', 'mXHAqv1Emu', 'monocles12', 'n0NhU3FxxT',
+// 'ng7AlzPb5b', 'ntbErnnuyz', 'oVnMO0dX80', 'omOTPVY3Gl', 'p5aNFCfYG9',
+// 'pYxjVCILuL', 'phylab', 'pjVBFmnhf1', 'qGohFW3Lbr', 'qYlbjeIHDs',
+// 'qgf4OwkFI6', 'qsi', 'rJQ09pRexM', 'snNjlS1aLC', 'tYKC53TDF9',
+// 'u1vZmL8Yjv', 'ur4sb7DBJB', 'vesti', 'w9NJegEAZt', 'wDwlSCby2s',
+// 'wGFJJRT514', 'wTgEoQGqng', 'xomMZGhius', 'yFEFYWBSvr', 'z7tGFKsGk6',
+// 'zIJWNK8Z4i', 'zNMGJYI7hq'];
+
+// function _time(f) {
+// var t1 = +(new Date);
+// f();
+// var t2 = +(new Date);
+// return t2 - t1;
+// }
+
+// function listAllRevisionCounts() {
+// var padList = sqlbase.getAllJSONKeys("PAD_META");
+// //padList.length = 10;
+// padList = padList.slice(68000, 68100);
+// padList.forEach(function(id) {
+// model.accessPadGlobal(id, function(pad) {
+// System.out.println((new java.lang.Integer(pad.getHeadRevisionNumber()).toString())+
+// " "+id);
+// dbwriter.writePadNow(pad, true);
+// }, 'r');
+// });
+// }
+
+// function verifyAllPads() {
+// //var padList = sqlbase.getAllJSONKeys("PAD_META");
+// //padList = newlineCorruptedPads;
+// var padList = ['0OCGFKkjDv'];
+// //padList = ['form6rooma'];
+// //padList.length = 10;
+// var numOks = 0;
+// var numErrors = 0;
+// var numNewlineBugs = 0;
+// var longestPad;
+// var longestPadTime = -1;
+// System.out.println(padList.length+" pads.");
+// var totalTime = _time(function() {
+// padList.forEach(function(id) {
+// model.accessPadGlobal(id, function(pad) {
+// var padTime = _time(function() {
+// System.out.print(id+"... ");
+// try {
+// verifyMigration(pad);
+// System.out.println("OK ("+(++numOks)+")");
+// }
+// catch (e) {
+// System.out.println("ERROR ("+(++numErrors)+")"+(e.finalNewlineMissing?" [newline]":""));
+// System.out.println(e.toString());
+// if (e.finalNewlineMissing) {
+// numNewlineBugs++;
+// }
+// }
+// });
+// if (padTime > longestPadTime) {
+// longestPadTime = padTime;
+// longestPad = id;
+// }
+// }, 'r');
+// });
+// });
+// System.out.println("finished verifyAllPads in "+(totalTime/1000)+" seconds.");
+// System.out.println(numOks+" OK");
+// System.out.println(numErrors+" ERROR");
+// System.out.println("Most time-consuming pad: "+longestPad+" / "+longestPadTime+" ms");
+// }
+
+// function _literal(v) {
+// if ((typeof v) == "string") {
+// return '"'+v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')+'"';
+// }
+// else return v.toSource();
+// }
+
+// function _putFile(str, path) {
+// var writer = new java.io.FileWriter(path);
+// writer.write(str);
+// writer.close();
+// }
diff --git a/trunk/etherpad/src/etherpad/pad/exporthtml.js b/trunk/etherpad/src/etherpad/pad/exporthtml.js
new file mode 100644
index 0000000..2512603
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/exporthtml.js
@@ -0,0 +1,383 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.collab.ace.easysync2.Changeset");
+
+function getPadPlainText(pad, revNum) {
+ var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) :
+ pad.atext());
+ var textLines = atext.text.slice(0,-1).split('\n');
+ var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
+ var apool = pad.pool();
+
+ var pieces = [];
+ for(var i=0;i<textLines.length;i++) {
+ var line = _analyzeLine(textLines[i], attribLines[i], apool);
+ if (line.listLevel) {
+ var numSpaces = line.listLevel*2-1;
+ var bullet = '*';
+ pieces.push(new Array(numSpaces+1).join(' '), bullet, ' ', line.text, '\n');
+ }
+ else {
+ pieces.push(line.text, '\n');
+ }
+ }
+
+ return pieces.join('');
+}
+
+function getPadHTML(pad, revNum) {
+ var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) :
+ pad.atext());
+ var textLines = atext.text.slice(0,-1).split('\n');
+ var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
+
+ var apool = pad.pool();
+
+ var tags = ['b','i','u','s','h1','h2','h3','h4','h5','h6'];
+ var props = ['bold','italic','underline','strikethrough','h1','h2','h3','h4','h5','h6'];
+ var anumMap = {};
+ props.forEach(function(propName, i) {
+ var propTrueNum = apool.putAttrib([propName,true], true);
+ if (propTrueNum >= 0) {
+ anumMap[propTrueNum] = i;
+ }
+ });
+
+ function getLineHTML(text, attribs) {
+ var propVals = [false, false, false];
+ var ENTER = 1;
+ var STAY = 2;
+ var LEAVE = 0;
+
+ // Use order of tags (b/i/u) as order of nesting, for simplicity
+ // and decent nesting. For example,
+ // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
+ // becomes
+ // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
+
+ var taker = Changeset.stringIterator(text);
+ var assem = Changeset.stringAssembler();
+
+ function emitOpenTag(i) {
+ assem.append('<');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
+ function emitCloseTag(i) {
+ assem.append('</');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
+
+ var urls = _findURLs(text);
+
+ var idx = 0;
+ function processNextChars(numChars) {
+ if (numChars <= 0) {
+ return;
+ }
+
+ var iter = Changeset.opIterator(Changeset.subattribution(attribs,
+ idx, idx+numChars));
+ idx += numChars;
+
+ while (iter.hasNext()) {
+ var o = iter.next();
+ var propChanged = false;
+ Changeset.eachAttribNumber(o.attribs, function(a) {
+ if (a in anumMap) {
+ var i = anumMap[a]; // i = 0 => bold, etc.
+ if (! propVals[i]) {
+ propVals[i] = ENTER;
+ propChanged = true;
+ }
+ else {
+ propVals[i] = STAY;
+ }
+ }
+ });
+ for(var i=0;i<propVals.length;i++) {
+ if (propVals[i] === true) {
+ propVals[i] = LEAVE;
+ propChanged = true;
+ }
+ else if (propVals[i] === STAY) {
+ propVals[i] = true; // set it back
+ }
+ }
+ // now each member of propVal is in {false,LEAVE,ENTER,true}
+ // according to what happens at start of span
+
+ if (propChanged) {
+ // leaving bold (e.g.) also leaves italics, etc.
+ var left = false;
+ for(var i=0;i<propVals.length;i++) {
+ var v = propVals[i];
+ if (! left) {
+ if (v === LEAVE) {
+ left = true;
+ }
+ }
+ else {
+ if (v === true) {
+ propVals[i] = STAY; // tag will be closed and re-opened
+ }
+ }
+ }
+
+ for(var i=propVals.length-1; i>=0; i--) {
+ if (propVals[i] === LEAVE) {
+ emitCloseTag(i);
+ propVals[i] = false;
+ }
+ else if (propVals[i] === STAY) {
+ emitCloseTag(i);
+ }
+ }
+ for(var i=0; i<propVals.length; i++) {
+ if (propVals[i] === ENTER || propVals[i] === STAY) {
+ emitOpenTag(i);
+ propVals[i] = true;
+ }
+ }
+ // propVals is now all {true,false} again
+ } // end if (propChanged)
+
+ var chars = o.chars;
+ if (o.lines) {
+ chars--; // exclude newline at end of line, if present
+ }
+ var s = taker.take(chars);
+
+ assem.append(_escapeHTML(s));
+ } // end iteration over spans in line
+
+ for(var i=propVals.length-1; i>=0; i--) {
+ if (propVals[i]) {
+ emitCloseTag(i);
+ propVals[i] = false;
+ }
+ }
+ } // end processNextChars
+
+ if (urls) {
+ urls.forEach(function(urlData) {
+ var startIndex = urlData[0];
+ var url = urlData[1];
+ var urlLength = url.length;
+ processNextChars(startIndex - idx);
+ assem.append('<a href="'+url.replace(/\"/g, '&quot;')+'">');
+ processNextChars(urlLength);
+ assem.append('</a>');
+ });
+ }
+ processNextChars(text.length - idx);
+
+ return _processSpaces(assem.toString());
+ } // end getLineHTML
+
+ var pieces = [];
+
+ // Need to deal with constraints imposed on HTML lists; can
+ // only gain one level of nesting at once, can't change type
+ // mid-list, etc.
+ // People might use weird indenting, e.g. skip a level,
+ // so we want to do something reasonable there. We also
+ // want to deal gracefully with blank lines.
+ var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...]
+ for(var i=0;i<textLines.length;i++) {
+ var line = _analyzeLine(textLines[i], attribLines[i], apool);
+ var lineContent = getLineHTML(line.text, line.aline);
+
+ if (line.listLevel || lists.length > 0) {
+ // do list stuff
+ var whichList = -1; // index into lists or -1
+ if (line.listLevel) {
+ whichList = lists.length;
+ for(var j=lists.length-1;j>=0;j--) {
+ if (line.listLevel <= lists[j][0]) {
+ whichList = j;
+ }
+ }
+ }
+
+ if (whichList >= lists.length) {
+ lists.push([line.listLevel, line.listTypeName]);
+ pieces.push('<ul><li>', lineContent || '<br/>');
+ }
+ else if (whichList == -1) {
+ if (line.text) {
+ // non-blank line, end all lists
+ pieces.push(new Array(lists.length+1).join('</li></ul\n>'));
+ lists.length = 0;
+ pieces.push(lineContent, '<br\n/>');
+ }
+ else {
+ pieces.push('<br/><br\n/>');
+ }
+ }
+ else {
+ while (whichList < lists.length-1) {
+ pieces.push('</li></ul\n>');
+ lists.length--;
+ }
+ pieces.push('</li\n><li>', lineContent || '<br/>');
+ }
+ }
+ else {
+ pieces.push(lineContent, '<br\n/>');
+ }
+ }
+ pieces.push(new Array(lists.length+1).join('</li></ul\n>'));
+
+ return pieces.join('');
+}
+
+function _analyzeLine(text, aline, apool) {
+ var line = {};
+
+ // identify list
+ var lineMarker = 0;
+ line.listLevel = 0;
+ if (aline) {
+ var opIter = Changeset.opIterator(aline);
+ if (opIter.hasNext()) {
+ var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
+ if (listType) {
+ lineMarker = 1;
+ listType = /([a-z]+)([12345678])/.exec(listType);
+ if (listType) {
+ line.listTypeName = listType[1];
+ line.listLevel = Number(listType[2]);
+ }
+ }
+ }
+ }
+ if (lineMarker) {
+ line.text = text.substring(1);
+ line.aline = Changeset.subattribution(aline, 1);
+ }
+ else {
+ line.text = text;
+ line.aline = aline;
+ }
+
+ return line;
+}
+
+function getPadHTMLDocument(pad, revNum, noDocType) {
+ var head = (noDocType?'':'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '+
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')+
+ '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">\n'+
+ (noDocType?'':
+ '<head>\n'+
+ '<meta http-equiv="Content-type" content="text/html; charset=utf-8" />\n'+
+ '<meta http-equiv="Content-Language" content="en-us" />\n'+
+ '<title>'+'/'+pad.getId()+'</title>\n'+
+ '<style type="text/css">h1,h2,h3,h4,h5,h6 { display: inline; }</style>\n' +
+ '</head>\n')+
+ '<body>';
+
+ var foot = '</body>\n</html>\n';
+
+ return head + getPadHTML(pad, revNum) + foot;
+}
+
+function _escapeHTML(s) {
+ var re = /[&<>]/g;
+ if (! re.MAP) {
+ // persisted across function calls!
+ re.MAP = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ };
+ }
+ return s.replace(re, function(c) { return re.MAP[c]; });
+}
+
+// copied from ACE
+function _processSpaces(s) {
+ var doesWrap = true;
+ if (s.indexOf("<") < 0 && ! doesWrap) {
+ // short-cut
+ return s.replace(/ /g, '&nbsp;');
+ }
+ var parts = [];
+ s.replace(/<[^>]*>?| |[^ <]+/g, function(m) { parts.push(m); });
+ if (doesWrap) {
+ var endOfLine = true;
+ var beforeSpace = false;
+ // last space in a run is normal, others are nbsp,
+ // end of line is nbsp
+ for(var i=parts.length-1;i>=0;i--) {
+ var p = parts[i];
+ if (p == " ") {
+ if (endOfLine || beforeSpace)
+ parts[i] = '&nbsp;';
+ endOfLine = false;
+ beforeSpace = true;
+ }
+ else if (p.charAt(0) != "<") {
+ endOfLine = false;
+ beforeSpace = false;
+ }
+ }
+ // beginning of line is nbsp
+ for(var i=0;i<parts.length;i++) {
+ var p = parts[i];
+ if (p == " ") {
+ parts[i] = '&nbsp;';
+ break;
+ }
+ else if (p.charAt(0) != "<") {
+ break;
+ }
+ }
+ }
+ else {
+ for(var i=0;i<parts.length;i++) {
+ var p = parts[i];
+ if (p == " ") {
+ parts[i] = '&nbsp;';
+ }
+ }
+ }
+ return parts.join('');
+}
+
+
+// copied from ACE
+var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+var _REGEX_SPACE = /\s/;
+var _REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+_REGEX_WORDCHAR.source+')');
+var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+_REGEX_URLCHAR.source+'*(?![:.,;])'+_REGEX_URLCHAR.source, 'g');
+
+// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
+function _findURLs(text) {
+ _REGEX_URL.lastIndex = 0;
+ var urls = null;
+ var execResult;
+ while ((execResult = _REGEX_URL.exec(text))) {
+ urls = (urls || []);
+ var startIndex = execResult.index;
+ var url = execResult[0];
+ urls.push([startIndex, url]);
+ }
+
+ return urls;
+}
diff --git a/trunk/etherpad/src/etherpad/pad/importhtml.js b/trunk/etherpad/src/etherpad/pad/importhtml.js
new file mode 100644
index 0000000..4a48c6f
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/importhtml.js
@@ -0,0 +1,230 @@
+/**
+ * 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.
+ */
+
+
+jimport("org.ccil.cowan.tagsoup.Parser");
+jimport("org.ccil.cowan.tagsoup.PYXWriter");
+jimport("java.io.StringReader");
+jimport("java.io.StringWriter");
+jimport("org.xml.sax.InputSource");
+
+import("etherpad.collab.ace.easysync2.{Changeset,AttribPool}");
+import("etherpad.collab.ace.contentcollector.makeContentCollector");
+import("etherpad.collab.collab_server");
+
+function setPadHTML(pad, html) {
+ var atext = htmlToAText(html, pad.pool());
+ collab_server.setPadAText(pad, atext);
+}
+
+function _html2pyx(html) {
+ var p = new Parser();
+ var w = new StringWriter();
+ var h = new PYXWriter(w);
+ p.setContentHandler(h);
+ var s = new InputSource();
+ s.setCharacterStream(new StringReader(html));
+ p.parse(s);
+ return w.toString().replace(/\r\n|\r|\n/g, '\n');
+}
+
+function _htmlBody2js(html) {
+ var pyx = _html2pyx(html);
+ var plines = pyx.split("\n");
+
+ function pyxUnescape(s) {
+ return s.replace(/\\t/g, '\t').replace(/\\/g, '\\');
+ }
+ var inAttrs = false;
+
+ var nodeStack = [];
+ var topNode = {};
+
+ var bodyNode = {name:"body"};
+
+ plines.forEach(function(pline) {
+ var t = pline.charAt(0);
+ var v = pline.substring(1);
+ if (inAttrs && t != 'A') {
+ inAttrs = false;
+ }
+ if (t == '?') { /* ignore */ }
+ else if (t == '(') {
+ var newNode = {name: v};
+ if (v.toLowerCase() == "body") {
+ bodyNode = newNode;
+ }
+ topNode.children = (topNode.children || []);
+ topNode.children.push(newNode);
+ nodeStack.push(topNode);
+ topNode = newNode;
+ inAttrs = true;
+ }
+ else if (t == 'A') {
+ var spaceIndex = v.indexOf(' ');
+ var key = v.substring(0, spaceIndex);
+ var value = pyxUnescape(v.substring(spaceIndex+1));
+ topNode.attrs = (topNode.attrs || {});
+ topNode.attrs['$'+key] = value;
+ }
+ else if (t == '-') {
+ if (v == "\\n") {
+ v = '\n';
+ }
+ else {
+ v = pyxUnescape(v);
+ }
+ if (v) {
+ topNode.children = (topNode.children || []);
+ if (topNode.children.length > 0 &&
+ ((typeof topNode.children[topNode.children.length-1]) == "string")) {
+ // coallesce
+ topNode.children.push(topNode.children.pop() + v);
+ }
+ else {
+ topNode.children.push(v);
+ }
+ }
+ }
+ else if (t == ')') {
+ topNode = nodeStack.pop();
+ }
+ });
+
+ return bodyNode;
+}
+
+function _trimDomNode(n) {
+ function isWhitespace(str) {
+ return /^\s*$/.test(str);
+ }
+ function trimBeginningOrEnd(n, endNotBeginning) {
+ var cc = n.children;
+ var backwards = endNotBeginning;
+ if (cc) {
+ var i = (backwards ? cc.length-1 : 0);
+ var done = false;
+ var hitActualText = false;
+ while (! done) {
+ if (! (backwards ? (i >= 0) : (i < cc.length-1))) {
+ done = true;
+ }
+ else {
+ var c = cc[i];
+ if ((typeof c) == "string") {
+ if (! isWhitespace(c)) {
+ // actual text
+ hitActualText = true;
+ break;
+ }
+ else {
+ // whitespace
+ cc[i] = '';
+ }
+ }
+ else {
+ // recurse
+ if (trimBeginningOrEnd(cc[i], endNotBeginning)) {
+ hitActualText = true;
+ break;
+ }
+ }
+ i += (backwards ? -1 : 1);
+ }
+ }
+ n.children = n.children.filter(function(x) { return !!x; });
+ return hitActualText;
+ }
+ return false;
+ }
+ trimBeginningOrEnd(n, false);
+ trimBeginningOrEnd(n, true);
+}
+
+function htmlToAText(html, apool) {
+ var body = _htmlBody2js(html);
+ _trimDomNode(body);
+
+ var dom = {
+ isNodeText: function(n) {
+ return (typeof n) == "string";
+ },
+ nodeTagName: function(n) {
+ return ((typeof n) == "object") && n.name;
+ },
+ nodeValue: function(n) {
+ return String(n);
+ },
+ nodeNumChildren: function(n) {
+ return (((typeof n) == "object") && n.children && n.children.length) || 0;
+ },
+ nodeChild: function(n, i) {
+ return (((typeof n) == "object") && n.children && n.children[i]) || null;
+ },
+ nodeProp: function(n, p) {
+ return (((typeof n) == "object") && n.attrs && n.attrs[p]) || null;
+ },
+ nodeAttr: function(n, a) {
+ return (((typeof n) == "object") && n.attrs && n.attrs[a]) || null;
+ },
+ optNodeInnerHTML: function(n) {
+ return null;
+ }
+ }
+
+ var cc = makeContentCollector(true, null, apool, dom);
+ for(var i=0; i<dom.nodeNumChildren(body); i++) {
+ var n = dom.nodeChild(body, i);
+ cc.collectContent(n);
+ }
+ cc.notifyNextNode(null);
+ var ccData = cc.finish();
+
+ var textLines = ccData.lines;
+ var attLines = ccData.lineAttribs;
+ for(var i=0;i<textLines.length;i++) {
+ var txt = textLines[i];
+ if (txt == " " || txt == "\xa0") {
+ // space or nbsp all alone on a line, remove
+ textLines[i] = "";
+ attLines[i] = "";
+ }
+ }
+
+ var text = textLines.join('\n')+'\n';
+ var attribs = _joinLineAttribs(attLines);
+ var atext = Changeset.makeAText(text, attribs);
+
+ return atext;
+}
+
+function _joinLineAttribs(lineAttribs) {
+ var assem = Changeset.smartOpAssembler();
+
+ var newline = Changeset.newOp('+');
+ newline.chars = 1;
+ newline.lines = 1;
+
+ lineAttribs.forEach(function(aline) {
+ var iter = Changeset.opIterator(aline);
+ while (iter.hasNext()) {
+ assem.append(iter.next());
+ }
+ assem.append(newline);
+ });
+
+ return assem.toString();
+} \ No newline at end of file
diff --git a/trunk/etherpad/src/etherpad/pad/model.js b/trunk/etherpad/src/etherpad/pad/model.js
new file mode 100644
index 0000000..9424f10
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/model.js
@@ -0,0 +1,651 @@
+/**
+ * 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.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("timer");
+import("sync");
+
+import("etherpad.collab.ace.easysync2.{Changeset,AttribPool}");
+import("etherpad.log");
+import("etherpad.pad.padevents");
+import("etherpad.pad.padutils");
+import("etherpad.pad.dbwriter");
+import("etherpad.pad.pad_migrations");
+import("etherpad.pad.pad_security");
+import("etherpad.collab.collab_server");
+import("cache_utils.syncedWithCache");
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+jimport("java.lang.System.out.println");
+
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("net.appjet.oui.GlobalSynchronizer");
+jimport("net.appjet.oui.exceptionlog");
+
+function onStartup() {
+ appjet.cache.pads = {};
+ appjet.cache.pads.meta = new ConcurrentHashMap();
+ appjet.cache.pads.temp = new ConcurrentHashMap();
+ appjet.cache.pads.revs = new ConcurrentHashMap();
+ appjet.cache.pads.revs10 = new ConcurrentHashMap();
+ appjet.cache.pads.revs100 = new ConcurrentHashMap();
+ appjet.cache.pads.revs1000 = new ConcurrentHashMap();
+ appjet.cache.pads.chat = new ConcurrentHashMap();
+ appjet.cache.pads.revmeta = new ConcurrentHashMap();
+ appjet.cache.pads.authors = new ConcurrentHashMap();
+ appjet.cache.pads.apool = new ConcurrentHashMap();
+}
+
+var _JSON_CACHE_SIZE = 10000;
+
+// to clear: appjet.cache.padmodel.modelcache.map.clear()
+function _getModelCache() {
+ return syncedWithCache('padmodel.modelcache', function(cache) {
+ if (! cache.map) {
+ cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE);
+ }
+ return cache.map;
+ });
+}
+
+function cleanText(txt) {
+ return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
+}
+
+/**
+ * Access a pad object, which is passed as an argument to
+ * the given padFunc, which is executed inside an exclusive lock,
+ * and return the result. If the pad doesn't exist, a wrapper
+ * object is still created and passed to padFunc, and it can
+ * be used to check whether the pad exists and create it.
+ *
+ * Note: padId is a GLOBAL id.
+ */
+function accessPadGlobal(padId, padFunc, rwMode) {
+ // this may make a nested call to accessPadGlobal, so do it first
+ pad_security.checkAccessControl(padId, rwMode);
+
+ // pad is never loaded into memory (made "active") unless it has been migrated.
+ // Migrations do not use accessPad, but instead access the database directly.
+ pad_migrations.ensureMigrated(padId);
+
+ var mode = (rwMode || "rw").toLowerCase();
+
+ if (! appjet.requestCache.padsAccessing) {
+ appjet.requestCache.padsAccessing = {};
+ }
+ if (appjet.requestCache.padsAccessing[padId]) {
+ // nested access to same pad
+ var p = appjet.requestCache.padsAccessing[padId];
+ var m = p._meta;
+ if (m && mode != "r") {
+ m.status.lastAccess = +new Date();
+ m.status.dirty = true;
+ }
+ return padFunc(p);
+ }
+
+ return doWithPadLock(padId, function() {
+ return sqlcommon.inTransaction(function() {
+ var meta = _getPadMetaData(padId); // null if pad doesn't exist yet
+
+ if (meta && ! meta.status) {
+ meta.status = { validated: false };
+ }
+
+ if (meta && mode != "r") {
+ meta.status.lastAccess = +new Date();
+ }
+
+ function getCurrentAText() {
+ var tempObj = pad.tempObj();
+ if (! tempObj.atext) {
+ tempObj.atext = pad.getInternalRevisionAText(meta.head);
+ }
+ return tempObj.atext;
+ }
+ function addRevision(theChangeset, author, optDatestamp) {
+ var atext = getCurrentAText();
+ var newAText = Changeset.applyToAText(theChangeset, atext, pad.pool());
+ Changeset.copyAText(newAText, atext); // updates pad.tempObj().atext!
+
+ var newRev = ++meta.head;
+
+ var revs = _getPadStringArray(padId, "revs");
+ revs.setEntry(newRev, theChangeset);
+
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ var thisRevMeta = {t: (optDatestamp || (+new Date())),
+ a: getNumForAuthor(author)};
+ if ((newRev % meta.keyRevInterval) == 0) {
+ thisRevMeta.atext = atext;
+ }
+ revmeta.setJSONEntry(newRev, thisRevMeta);
+
+ updateCoarseChangesets(true);
+ }
+ function getNumForAuthor(author, dontAddIfAbsent) {
+ return pad.pool().putAttrib(['author',author||''], dontAddIfAbsent);
+ }
+ function getAuthorForNum(n) {
+ // must return null if n is an attrib number that isn't an author
+ var pair = pad.pool().getAttrib(n);
+ if (pair && pair[0] == 'author') {
+ return pair[1];
+ }
+ return null;
+ }
+
+ function updateCoarseChangesets(onlyIfPresent) {
+ // this is fast to run if the coarse changesets
+ // are up-to-date or almost up-to-date;
+ // if there's no coarse changeset data,
+ // it may take a while.
+
+ if (! meta.coarseHeads) {
+ if (onlyIfPresent) {
+ return;
+ }
+ else {
+ meta.coarseHeads = {10:-1, 100:-1, 1000:-1};
+ }
+ }
+ var head = meta.head;
+ // once we reach head==9, coarseHeads[10] moves
+ // from -1 up to 0; at head==19 it moves up to 1
+ var desiredCoarseHeads = {
+ 10: Math.floor((head-9)/10),
+ 100: Math.floor((head-99)/100),
+ 1000: Math.floor((head-999)/1000)
+ };
+ var revs = _getPadStringArray(padId, "revs");
+ var revs10 = _getPadStringArray(padId, "revs10");
+ var revs100 = _getPadStringArray(padId, "revs100");
+ var revs1000 = _getPadStringArray(padId, "revs1000");
+ var fineArrays = [revs, revs10, revs100];
+ var coarseArrays = [revs10, revs100, revs1000];
+ var levels = [10, 100, 1000];
+ var dirty = false;
+ for(var z=0;z<3;z++) {
+ var level = levels[z];
+ var coarseArray = coarseArrays[z];
+ var fineArray = fineArrays[z];
+ while (meta.coarseHeads[level] < desiredCoarseHeads[level]) {
+ dirty = true;
+ // for example, if the current coarse head is -1,
+ // compose 0-9 inclusive of the finer level and call it 0
+ var x = meta.coarseHeads[level] + 1;
+ var cs = fineArray.getEntry(10 * x);
+ for(var i=1;i<=9;i++) {
+ cs = Changeset.compose(cs, fineArray.getEntry(10*x + i),
+ pad.pool());
+ }
+ coarseArray.setEntry(x, cs);
+ meta.coarseHeads[level] = x;
+ }
+ }
+ if (dirty) {
+ meta.status.dirty = true;
+ }
+ }
+
+ /////////////////// "Public" API starts here (functions used by collab_server or other modules)
+ var pad = {
+ // Operations that write to the data structure should
+ // set meta.dirty = true. Any pad access that isn't
+ // done in "read" mode also sets dirty = true.
+ getId: function() { return padId; },
+ exists: function() { return !!meta; },
+ create: function(optText) {
+ meta = {};
+ meta.head = -1; // incremented below by addRevision
+ pad.tempObj().atext = Changeset.makeAText("\n");
+ meta.padId = padId,
+ meta.keyRevInterval = 100;
+ meta.numChatMessages = 0;
+ var t = +new Date();
+ meta.status = { validated: true };
+ meta.status.lastAccess = t;
+ meta.status.dirty = true;
+ meta.supportsTimeSlider = true;
+
+ var firstChangeset = Changeset.makeSplice("\n", 0, 0,
+ cleanText(optText || ''));
+ addRevision(firstChangeset, '');
+
+ _insertPadMetaData(padId, meta);
+
+ sqlobj.insert("PAD_SQLMETA", {
+ id: padId, version: 2, creationTime: new Date(t), lastWriteTime: new Date(),
+ headRev: meta.head }); // headRev is not authoritative, just for info
+
+ padevents.onNewPad(pad);
+ },
+ destroy: function() { // you may want to collab_server.bootAllUsers first
+ padevents.onDestroyPad(pad);
+
+ _destroyPadStringArray(padId, "revs");
+ _destroyPadStringArray(padId, "revs10");
+ _destroyPadStringArray(padId, "revs100");
+ _destroyPadStringArray(padId, "revs1000");
+ _destroyPadStringArray(padId, "revmeta");
+ _destroyPadStringArray(padId, "chat");
+ _destroyPadStringArray(padId, "authors");
+ _removePadMetaData(padId);
+ _removePadAPool(padId);
+ sqlobj.deleteRows("PAD_SQLMETA", { id: padId });
+ meta = null;
+ },
+ writeToDB: function() {
+ var meta2 = {};
+ for(var k in meta) meta2[k] = meta[k];
+ delete meta2.status;
+ sqlbase.putJSON("PAD_META", padId, meta2);
+
+ _getPadStringArray(padId, "revs").writeToDB();
+ _getPadStringArray(padId, "revs10").writeToDB();
+ _getPadStringArray(padId, "revs100").writeToDB();
+ _getPadStringArray(padId, "revs1000").writeToDB();
+ _getPadStringArray(padId, "revmeta").writeToDB();
+ _getPadStringArray(padId, "chat").writeToDB();
+ _getPadStringArray(padId, "authors").writeToDB();
+ sqlbase.putJSON("PAD_APOOL", padId, pad.pool().toJsonable());
+
+ var props = { headRev: meta.head, lastWriteTime: new Date() };
+ _writePadSqlMeta(padId, props);
+ },
+ pool: function() {
+ return _getPadAPool(padId);
+ },
+ getHeadRevisionNumber: function() { return meta.head; },
+ getRevisionAuthor: function(r) {
+ var n = _getPadStringArray(padId, "revmeta").getJSONEntry(r).a;
+ return getAuthorForNum(Number(n));
+ },
+ getRevisionChangeset: function(r) {
+ return _getPadStringArray(padId, "revs").getEntry(r);
+ },
+ tempObj: function() { return _getPadTemp(padId); },
+ getKeyRevisionNumber: function(r) {
+ return Math.floor(r / meta.keyRevInterval) * meta.keyRevInterval;
+ },
+ getInternalRevisionAText: function(r) {
+ var cacheKey = "atext/C/"+r+"/"+padId;
+ var modelCache = _getModelCache();
+ var cachedValue = modelCache.get(cacheKey);
+ if (cachedValue) {
+ modelCache.touch(cacheKey);
+ //java.lang.System.out.println("HIT! "+cacheKey);
+ return Changeset.cloneAText(cachedValue);
+ }
+ //java.lang.System.out.println("MISS! "+cacheKey);
+
+ var revs = _getPadStringArray(padId, "revs");
+ var keyRev = pad.getKeyRevisionNumber(r);
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ var atext = revmeta.getJSONEntry(keyRev).atext;
+ var curRev = keyRev;
+ var targetRev = r;
+ var apool = pad.pool();
+ while (curRev < targetRev) {
+ curRev++;
+ var cs = pad.getRevisionChangeset(curRev);
+ atext = Changeset.applyToAText(cs, atext, apool);
+ }
+ modelCache.put(cacheKey, Changeset.cloneAText(atext));
+ return atext;
+ },
+ getInternalRevisionText: function(r, optInfoObj) {
+ var atext = pad.getInternalRevisionAText(r);
+ var text = atext.text;
+ if (optInfoObj) {
+ if (text.slice(-1) != "\n") {
+ optInfoObj.badLastChar = text.slice(-1);
+ }
+ }
+ return text;
+ },
+ getRevisionText: function(r, optInfoObj) {
+ var internalText = pad.getInternalRevisionText(r, optInfoObj);
+ return internalText.slice(0, -1);
+ },
+ atext: function() { return Changeset.cloneAText(getCurrentAText()); },
+ text: function() { return pad.atext().text; },
+ getRevisionDate: function(r) {
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ return new Date(revmeta.getJSONEntry(r).t);
+ },
+ // note: calls like appendRevision will NOT notify clients of the change!
+ // you must go through collab_server.
+ // Also, be sure to run cleanText() on any text to strip out carriage returns
+ // and other stuff.
+ appendRevision: function(theChangeset, author, optDatestamp) {
+ addRevision(theChangeset, author || '', optDatestamp);
+ },
+ appendChatMessage: function(obj) {
+ var index = meta.numChatMessages;
+ meta.numChatMessages++;
+ var chat = _getPadStringArray(padId, "chat");
+ chat.setJSONEntry(index, obj);
+ },
+ getNumChatMessages: function() {
+ return meta.numChatMessages;
+ },
+ getChatMessage: function(i) {
+ var chat = _getPadStringArray(padId, "chat");
+ return chat.getJSONEntry(i);
+ },
+ getPadOptionsObj: function() {
+ var data = pad.getDataRoot();
+ if (! data.padOptions) {
+ data.padOptions = {};
+ }
+ if ((! data.padOptions.guestPolicy) ||
+ (data.padOptions.guestPolicy == 'ask')) {
+ data.padOptions.guestPolicy = 'deny';
+ }
+ return data.padOptions;
+ },
+ getGuestPolicy: function() {
+ // allow/ask/deny
+ return pad.getPadOptionsObj().guestPolicy;
+ },
+ setGuestPolicy: function(policy) {
+ pad.getPadOptionsObj().guestPolicy = policy;
+ },
+ getDataRoot: function() {
+ var dataRoot = meta.dataRoot;
+ if (! dataRoot) {
+ dataRoot = {};
+ meta.dataRoot = dataRoot;
+ }
+ return dataRoot;
+ },
+ // returns an object, changes to which are not reflected
+ // in the DB; use setAuthorData for mutation
+ getAuthorData: function(author) {
+ var authors = _getPadStringArray(padId, "authors");
+ var n = getNumForAuthor(author, true);
+ if (n < 0) {
+ return null;
+ }
+ else {
+ return authors.getJSONEntry(n);
+ }
+ },
+ setAuthorData: function(author, data) {
+ var authors = _getPadStringArray(padId, "authors");
+ var n = getNumForAuthor(author);
+ authors.setJSONEntry(n, data);
+ },
+ adoptChangesetAttribs: function(cs, oldPool) {
+ return Changeset.moveOpsToNewPool(cs, oldPool, pad.pool());
+ },
+ eachATextAuthor: function(atext, func) {
+ var seenNums = {};
+ Changeset.eachAttribNumber(atext.attribs, function(n) {
+ if (! seenNums[n]) {
+ seenNums[n] = true;
+ var author = getAuthorForNum(n);
+ if (author) {
+ func(author, n);
+ }
+ }
+ });
+ },
+ getCoarseChangeset: function(start, numChangesets) {
+ updateCoarseChangesets();
+
+ if (!(numChangesets == 10 || numChangesets == 100 ||
+ numChangesets == 1000)) {
+ return null;
+ }
+ var level = numChangesets;
+ var x = Math.floor(start / level);
+ if (!(x >= 0 && x*level == start)) {
+ return null;
+ }
+
+ var cs = _getPadStringArray(padId, "revs"+level).getEntry(x);
+
+ if (! cs) {
+ return null;
+ }
+
+ return cs;
+ },
+ getSupportsTimeSlider: function() {
+ if (! ('supportsTimeSlider' in meta)) {
+ if (padutils.isProPadId(padId)) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ else {
+ return !! meta.supportsTimeSlider;
+ }
+ },
+ setSupportsTimeSlider: function(v) {
+ meta.supportsTimeSlider = v;
+ },
+ get _meta() { return meta; }
+ };
+
+ try {
+ padutils.setCurrentPad(padId);
+ appjet.requestCache.padsAccessing[padId] = pad;
+ return padFunc(pad);
+ }
+ finally {
+ padutils.clearCurrentPad();
+ delete appjet.requestCache.padsAccessing[padId];
+ if (meta) {
+ if (mode != "r") {
+ meta.status.dirty = true;
+ }
+ if (meta.status.dirty) {
+ dbwriter.notifyPadDirty(padId);
+ }
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Call an arbitrary function with no arguments inside an exclusive
+ * lock on a padId, and return the result.
+ */
+function doWithPadLock(padId, func) {
+ var lockName = "document/"+padId;
+ return sync.doWithStringLock(lockName, func);
+}
+
+function isPadLockHeld(padId) {
+ var lockName = "document/"+padId;
+ return GlobalSynchronizer.isHeld(lockName);
+}
+
+/**
+ * Get pad meta-data object, which is stored in SQL as JSON
+ * but cached in appjet.cache. Returns null if pad doesn't
+ * exist at all (does NOT create it). Requires pad lock.
+ */
+function _getPadMetaData(padId) {
+ var padMeta = appjet.cache.pads.meta.get(padId);
+ if (! padMeta) {
+ // not in cache
+ padMeta = sqlbase.getJSON("PAD_META", padId);
+ if (! padMeta) {
+ // not in SQL
+ padMeta = null;
+ }
+ else {
+ appjet.cache.pads.meta.put(padId, padMeta);
+ }
+ }
+ return padMeta;
+}
+
+/**
+ * Sets a pad's meta-data object, such as when creating
+ * a pad for the first time. Requires pad lock.
+ */
+function _insertPadMetaData(padId, obj) {
+ appjet.cache.pads.meta.put(padId, obj);
+}
+
+/**
+ * Removes a pad's meta data, writing through to the database.
+ * Used for the rare case of deleting a pad.
+ */
+function _removePadMetaData(padId) {
+ appjet.cache.pads.meta.remove(padId);
+ sqlbase.deleteJSON("PAD_META", padId);
+}
+
+function _getPadAPool(padId) {
+ var padAPool = appjet.cache.pads.apool.get(padId);
+ if (! padAPool) {
+ // not in cache
+ padAPool = new AttribPool();
+ padAPoolJson = sqlbase.getJSON("PAD_APOOL", padId);
+ if (padAPoolJson) {
+ // in SQL
+ padAPool.fromJsonable(padAPoolJson);
+ }
+ appjet.cache.pads.apool.put(padId, padAPool);
+ }
+ return padAPool;
+}
+
+/**
+ * Removes a pad's apool data, writing through to the database.
+ * Used for the rare case of deleting a pad.
+ */
+function _removePadAPool(padId) {
+ appjet.cache.pads.apool.remove(padId);
+ sqlbase.deleteJSON("PAD_APOOL", padId);
+}
+
+/**
+ * Get an object for a pad that's not persisted in storage,
+ * e.g. for tracking open connections. Creates object
+ * if necessary. Requires pad lock.
+ */
+function _getPadTemp(padId) {
+ var padTemp = appjet.cache.pads.temp.get(padId);
+ if (! padTemp) {
+ padTemp = {};
+ appjet.cache.pads.temp.put(padId, padTemp);
+ }
+ return padTemp;
+}
+
+/**
+ * Returns an object with methods for manipulating a string array, where name
+ * is something like "revs" or "chat". The object must be acquired and used
+ * all within a pad lock.
+ */
+function _getPadStringArray(padId, name) {
+ var padFoo = appjet.cache.pads[name].get(padId);
+ if (! padFoo) {
+ padFoo = {};
+ // writes go into writeCache, which is authoritative for reads;
+ // reads cause pages to be read into readCache
+ padFoo.readCache = {};
+ padFoo.writeCache = {};
+ appjet.cache.pads[name].put(padId, padFoo);
+ }
+ var tableName = "PAD_"+name.toUpperCase();
+ var self = {
+ getEntry: function(idx) {
+ var n = Number(idx);
+ if (padFoo.writeCache[n]) return padFoo.writeCache[n];
+ if (padFoo.readCache[n]) return padFoo.readCache[n];
+ sqlbase.getPageStringArrayElements(tableName, padId, n, padFoo.readCache);
+ return padFoo.readCache[n]; // null if not present in SQL
+ },
+ setEntry: function(idx, value) {
+ var n = Number(idx);
+ var v = String(value);
+ padFoo.writeCache[n] = v;
+ },
+ getJSONEntry: function(idx) {
+ var result = self.getEntry(idx);
+ if (! result) return result;
+ return fastJSON.parse(String(result));
+ },
+ setJSONEntry: function(idx, valueObj) {
+ self.setEntry(idx, fastJSON.stringify(valueObj));
+ },
+ writeToDB: function() {
+ sqlbase.putDictStringArrayElements(tableName, padId, padFoo.writeCache);
+ // copy key-vals of writeCache into readCache
+ var readCache = padFoo.readCache;
+ var writeCache = padFoo.writeCache;
+ for(var p in writeCache) {
+ readCache[p] = writeCache[p];
+ }
+ padFoo.writeCache = {};
+ }
+ };
+ return self;
+}
+
+/**
+ * Destroy a string array; writes through to the database. Must be
+ * called within a pad lock.
+ */
+function _destroyPadStringArray(padId, name) {
+ appjet.cache.pads[name].remove(padId);
+ var tableName = "PAD_"+name.toUpperCase();
+ sqlbase.clearStringArray(tableName, padId);
+}
+
+/**
+ * SELECT the row of PAD_SQLMETA for the given pad. Requires pad lock.
+ */
+function _getPadSqlMeta(padId) {
+ return sqlobj.selectSingle("PAD_SQLMETA", { id: padId });
+}
+
+function _writePadSqlMeta(padId, updates) {
+ sqlobj.update("PAD_SQLMETA", { id: padId }, updates);
+}
+
+
+// called from dbwriter
+function removeFromMemory(pad) {
+ // safe to call if all data is written to SQL, otherwise will lose data;
+ var padId = pad.getId();
+ appjet.cache.pads.meta.remove(padId);
+ appjet.cache.pads.revs.remove(padId);
+ appjet.cache.pads.revs10.remove(padId);
+ appjet.cache.pads.revs100.remove(padId);
+ appjet.cache.pads.revs1000.remove(padId);
+ appjet.cache.pads.chat.remove(padId);
+ appjet.cache.pads.revmeta.remove(padId);
+ appjet.cache.pads.apool.remove(padId);
+ collab_server.removeFromMemory(pad);
+}
+
+
diff --git a/trunk/etherpad/src/etherpad/pad/noprowatcher.js b/trunk/etherpad/src/etherpad/pad/noprowatcher.js
new file mode 100644
index 0000000..8eb2a92
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/noprowatcher.js
@@ -0,0 +1,110 @@
+/**
+ * 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.
+ */
+
+/*
+ * noprowatcher keeps track of when a pad has had no pro user
+ * in it for a certain period of time, after which all guests
+ * are booted.
+ */
+
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+import("etherpad.pad.padusers");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.model");
+import("cache_utils.syncedWithCache");
+import("execution");
+import("etherpad.sessions");
+
+function onStartup() {
+ execution.initTaskThreadPool("noprowatcher", 1);
+}
+
+function getNumProUsers(pad) {
+ var n = 0;
+ collab_server.getConnectedUsers(pad).forEach(function(info) {
+ if (! padusers.isGuest(info.userId)) {
+ n++; // found a non-guest
+ }
+ });
+ return n;
+}
+
+var _EMPTY_TIME = 60000;
+
+function checkPad(padOrPadId) {
+ if ((typeof padOrPadId) == "string") {
+ return model.accessPadGlobal(padOrPadId, function(pad) {
+ return checkPad(pad);
+ });
+ }
+ var pad = padOrPadId;
+
+ if (! padutils.isProPad(pad)) {
+ return; // public pad
+ }
+
+ if (pad.getGuestPolicy() == 'allow') {
+ return; // public access
+ }
+
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+
+ var globalPadId = pad.getId();
+
+ var numConnections = collab_server.getNumConnections(pad);
+ var numProUsers = getNumProUsers(pad);
+ syncedWithCache('noprowatcher.no_pros_since', function(noProsSince) {
+ if (! numConnections) {
+ // no connections, clear state and we're done
+ delete noProsSince[globalPadId];
+ }
+ else if (numProUsers) {
+ // pro users in pad, so we're not in a span of time with
+ // no pro users
+ delete noProsSince[globalPadId];
+ }
+ else {
+ // no pro users in pad
+ var since = noProsSince[globalPadId];
+ if (! since) {
+ // no entry in cache, that means last time we checked
+ // there were still pro users, but now there aren't
+ noProsSince[globalPadId] = +new Date;
+ execution.scheduleTask("noprowatcher", "noProWatcherCheckPad",
+ _EMPTY_TIME+1000, [globalPadId]);
+ }
+ else {
+ // already in a span of time with no pro users
+ if ((+new Date) - since > _EMPTY_TIME) {
+ // _EMPTY_TIME milliseconds since we first noticed no pro users
+ collab_server.bootAllUsersFromPad(pad, "unauth");
+ pad_security.revokeAllPadAccess(globalPadId);
+ }
+ }
+ }
+ });
+}
+
+function onUserJoin(pad, userInfo) {
+ checkPad(pad);
+}
+
+function onUserLeave(pad, userInfo) {
+ checkPad(pad);
+} \ No newline at end of file
diff --git a/trunk/etherpad/src/etherpad/pad/pad_migrations.js b/trunk/etherpad/src/etherpad/pad/pad_migrations.js
new file mode 100644
index 0000000..e81cf63
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/pad_migrations.js
@@ -0,0 +1,206 @@
+/**
+ * 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("etherpad.pad.model");
+import("etherpad.pad.easysync2migration");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("java.lang.System");
+jimport("java.util.ArrayList");
+jimport("java.util.Collections");
+
+function onStartup() {
+ if (! appjet.cache.pad_migrations) {
+ appjet.cache.pad_migrations = {};
+ }
+
+ // this part can be removed when all pads are migrated on pad.spline.inf.fu-berlin.de
+ //if (! pne_utils.isPNE()) {
+ // System.out.println("Building cache for live migrations...");
+ // initLiveMigration();
+ //}
+}
+
+function initLiveMigration() {
+
+ if (! appjet.cache.pad_migrations) {
+ appjet.cache.pad_migrations = {};
+ }
+ appjet.cache.pad_migrations.doingAnyLiveMigrations = true;
+ appjet.cache.pad_migrations.doingBackgroundLiveMigrations = true;
+ appjet.cache.pad_migrations.padMap = new ConcurrentHashMap();
+
+ // presence of a pad in padMap indicates migration is needed
+ var padMap = _padMap();
+ var migrationsNeeded = sqlobj.selectMulti("PAD_SQLMETA", {version: 1});
+ migrationsNeeded.forEach(function(obj) {
+ padMap.put(String(obj.id), {from: obj.version});
+ });
+}
+
+function _padMap() {
+ return appjet.cache.pad_migrations.padMap;
+}
+
+function _doingItLive() {
+ return !! appjet.cache.pad_migrations.doingAnyLiveMigrations;
+}
+
+function checkPadStatus(padId) {
+ if (! _doingItLive()) {
+ return "ready";
+ }
+ var info = _padMap().get(padId);
+ if (! info) {
+ return "ready";
+ }
+ else if (info.migrating) {
+ return "migrating";
+ }
+ else {
+ return "oldversion";
+ }
+}
+
+function ensureMigrated(padId, async) {
+ if (! _doingItLive()) {
+ return false;
+ }
+
+ var info = _padMap().get(padId);
+ if (! info) {
+ // pad is up-to-date
+ return false;
+ }
+ else if (async && info.migrating) {
+ // pad is already being migrated, don't wait on the lock
+ return false;
+ }
+
+ return model.doWithPadLock(padId, function() {
+ // inside pad lock...
+ var info = _padMap().get(padId);
+ if (!info) {
+ return false;
+ }
+ // migrate from version 1 to version 2 in a transaction
+ var migrateSucceeded = false;
+ try {
+ info.migrating = true;
+ log.info("Migrating pad "+padId+" from version 1 to version 2...");
+
+ var success = false;
+ var whichTry = 1;
+ while ((! success) && whichTry <= 3) {
+ success = sqlcommon.inTransaction(function() {
+ try {
+ easysync2migration.migratePad(padId);
+ sqlobj.update("PAD_SQLMETA", {id: padId}, {version: 2});
+ return true;
+ }
+ catch (e if (e.toString().indexOf("try restarting transaction") >= 0)) {
+ whichTry++;
+ return false;
+ }
+ });
+ if (! success) {
+ java.lang.Thread.sleep(Math.floor(Math.random()*200));
+ }
+ }
+ if (! success) {
+ throw new Error("too many retries");
+ }
+
+ migrateSucceeded = true;
+ log.info("Migrated pad "+padId+".");
+ _padMap().remove(padId);
+ }
+ finally {
+ info.migrating = false;
+ if (! migrateSucceeded) {
+ log.info("Migration failed for pad "+padId+".");
+ throw new Error("Migration failed for pad "+padId+".");
+ }
+ }
+ return true;
+ });
+}
+
+function numUnmigratedPads() {
+ if (! _doingItLive()) {
+ return 0;
+ }
+
+ return _padMap().size();
+}
+
+////////// BACKGROUND MIGRATIONS
+
+function _logPadMigration(runnerId, padNumber, padTotal, timeMs, fourCharResult, padId) {
+ log.custom("pad_migrations", {
+ runnerId: runnerId,
+ padNumber: Math.round(padNumber+1),
+ padTotal: Math.round(padTotal),
+ timeMs: Math.round(timeMs),
+ fourCharResult: fourCharResult,
+ padId: padId});
+}
+
+function _getNeededMigrationsArrayList(filter) {
+ var L = new ArrayList(_padMap().keySet());
+ for(var i=L.size()-1; i>=0; i--) {
+ if (! filter(String(L.get(i)))) {
+ L.remove(i);
+ }
+ }
+ return L;
+}
+
+function runBackgroundMigration(residue, modulus, runnerId) {
+ var L = _getNeededMigrationsArrayList(function(padId) {
+ return (padId.charCodeAt(0) % modulus) == residue;
+ });
+ Collections.shuffle(L);
+
+ var totalPads = L.size();
+ for(var i=0;i<totalPads;i++) {
+ if (! appjet.cache.pad_migrations.doingBackgroundLiveMigrations) {
+ break;
+ }
+ var padId = L.get(i);
+ var result = "FAIL";
+ var t1 = System.currentTimeMillis();
+ try {
+ if (ensureMigrated(padId, true)) {
+ result = " OK "; // migrated successfully
+ }
+ else {
+ result = " -- "; // no migration needed after all
+ }
+ }
+ catch (e) {
+ // e just says "migration failed", but presumably
+ // inTransaction() printed a stack trace.
+ // result == "FAIL", do nothing.
+ }
+ var t2 = System.currentTimeMillis();
+ _logPadMigration(runnerId, i, totalPads, t2 - t1, result, padId);
+ }
+}
diff --git a/trunk/etherpad/src/etherpad/pad/pad_security.js b/trunk/etherpad/src/etherpad/pad/pad_security.js
new file mode 100644
index 0000000..0ff8783
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/pad_security.js
@@ -0,0 +1,237 @@
+/**
+ * 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("etherpad.sessions.getSession");
+import("etherpad.sessions");
+
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_utils.isProDomainRequest");
+import("etherpad.pad.noprowatcher");
+
+//--------------------------------------------------------------------------------
+// granting session permanent access to pads (for the session)
+//--------------------------------------------------------------------------------
+
+function _grantSessionAccessTo(globalPadId) {
+ var userId = padusers.getUserId();
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ c[userId] = true;
+ });
+}
+
+function _doesSessionHaveAccessTo(globalPadId) {
+ var userId = padusers.getUserId();
+ return syncedWithCache("pad-auth."+globalPadId, function(c) {
+ return c[userId];
+ });
+}
+
+function revokePadUserAccess(globalPadId, userId) {
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ delete c[userId];
+ });
+}
+
+function revokeAllPadAccess(globalPadId) {
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ for (k in c) {
+ delete c[k];
+ }
+ });
+}
+
+//--------------------------------------------------------------------------------
+// knock/answer
+//--------------------------------------------------------------------------------
+
+function clearKnockStatus(userId, globalPadId) {
+ syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ delete c[userId];
+ });
+}
+
+// called by collab_server when accountholders approve or deny
+function answerKnock(userId, globalPadId, status) {
+ // status is either "approved" or "denied"
+ syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ // If two account-holders respond to the knock, keep the first one.
+ if (!c[userId]) {
+ c[userId] = status;
+ }
+ });
+}
+
+// returns "approved", "denied", or undefined
+function getKnockAnswer(userId, globalPadId) {
+ return syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ return c[userId];
+ });
+}
+
+//--------------------------------------------------------------------------------
+// main entrypoint called for every accessPad()
+//--------------------------------------------------------------------------------
+
+var _insideCheckAccessControl = false;
+
+function checkAccessControl(globalPadId, rwMode) {
+ if (!request.isDefined) {
+ return; // TODO: is this the right thing to do here?
+ // Empirical evidence indicates request.isDefined during comet requests,
+ // but not during tasks, which is the behavior we want.
+ }
+
+ if (_insideCheckAccessControl) {
+ // checkAccessControl is always allowed to access pads itself
+ return;
+ }
+ if (isProDomainRequest() && (request.path == "/ep/account/guest-knock")) {
+ return;
+ }
+ if (!isProDomainRequest() && (request.path == "/ep/admin/padinspector")) {
+ return;
+ }
+ if (isProDomainRequest() && (request.path == "/ep/padlist/all-pads.zip")) {
+ return;
+ }
+ try {
+ _insideCheckAccessControl = true;
+
+ if (!padutils.isProPadId(globalPadId)) {
+ // no access control on non-pro pads yet.
+ return;
+ }
+
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+ if (_doesSessionHaveAccessTo(globalPadId)) {
+ return;
+ }
+ _checkDomainSecurity(globalPadId);
+ _checkGuestSecurity(globalPadId);
+ _checkPasswordSecurity(globalPadId);
+
+ // remember that this user has access
+ _grantSessionAccessTo(globalPadId);
+ }
+ finally {
+ // this always runs, even on error or stop
+ _insideCheckAccessControl = false;
+ }
+}
+
+function _checkDomainSecurity(globalPadId) {
+ var padDomainId = padutils.getDomainId(globalPadId);
+ if (!padDomainId) {
+ return; // global pad
+ }
+ if (pro_utils.isProDomainRequest()) {
+ var requestDomainId = domains.getRequestDomainId();
+ if (requestDomainId != padDomainId) {
+ throw Error("Request cross-domain pad access not allowed.");
+ }
+ }
+}
+
+function _checkGuestSecurity(globalPadId) {
+ if (!getSession().guestPadAccess) {
+ getSession().guestPadAccess = {};
+ }
+
+ var padDomainId = padutils.getDomainId(globalPadId);
+ var isAccountHolder = pro_accounts.isAccountSignedIn();
+ if (isAccountHolder) {
+ if (getSessionProAccount().domainId != padDomainId) {
+ throw Error("Account cross-domain pad access not allowed.");
+ }
+ return; // OK
+ }
+
+ // Not an account holder ==> Guest
+
+ // returns either "allow", "ask", or "deny"
+ var guestPolicy = model.accessPadGlobal(globalPadId, function(p) {
+ if (!p.exists()) {
+ return "deny";
+ } else {
+ return p.getGuestPolicy();
+ }
+ });
+
+ var numProUsers = model.accessPadGlobal(globalPadId, function(pad) {
+ return noprowatcher.getNumProUsers(pad);
+ });
+
+ if (guestPolicy == "allow") {
+ return;
+ }
+ if (guestPolicy == "deny") {
+ pro_accounts.requireAccount("Guests are not allowed to join that pad. Please sign in.");
+ }
+ if (guestPolicy == "ask") {
+ if (numProUsers < 1) {
+ pro_accounts.requireAccount("This pad's security policy does not allow guests to join unless an account-holder is connected to the pad.");
+ }
+ var userId = padusers.getUserId();
+
+ // one of {"approved", "denied", undefined}
+ var knockAnswer = getKnockAnswer(userId, globalPadId);
+ if (knockAnswer == "approved") {
+ return;
+ } else {
+ var localPadId = padutils.globalToLocalId(globalPadId);
+ response.redirect('/ep/account/guest-sign-in?padId='+encodeURIComponent(localPadId));
+ }
+ }
+}
+
+function _checkPasswordSecurity(globalPadId) {
+ if (!getSession().padPasswordAuth) {
+ getSession().padPasswordAuth = {};
+ }
+ if (getSession().padPasswordAuth[globalPadId] == true) {
+ return;
+ }
+ var domainId = padutils.getDomainId(globalPadId);
+ var localPadId = globalPadId.split("$")[1];
+
+ if (stringutils.startsWith(request.path, "/ep/admin/recover-padtext")) {
+ return;
+ }
+
+ var p = pro_padmeta.accessProPad(globalPadId, function(propad) {
+ if (propad.exists()) {
+ return propad.getPassword();
+ } else {
+ return null;
+ }
+ });
+ if (p) {
+ response.redirect('/ep/pad/auth/'+localPadId+'?cont='+encodeURIComponent(request.url));
+ }
+}
+
diff --git a/trunk/etherpad/src/etherpad/pad/padevents.js b/trunk/etherpad/src/etherpad/pad/padevents.js
new file mode 100644
index 0000000..52b303c
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/padevents.js
@@ -0,0 +1,170 @@
+/**
+ * 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.
+ */
+
+// src/etherpad/events.js
+
+import("etherpad.licensing");
+import("etherpad.log");
+import("etherpad.pad.chatarchive");
+import("etherpad.pad.activepads");
+import("etherpad.pad.padutils");
+import("etherpad.sessions");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pad.padusers");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.noprowatcher");
+import("etherpad.collab.collab_server");
+jimport("java.lang.System.out.println");
+
+function onNewPad(pad) {
+ log.custom("padevents", {
+ type: "newpad",
+ padId: pad.getId()
+ });
+ pro_pad_db.onCreatePad(pad);
+}
+
+function onDestroyPad(pad) {
+ log.custom("padevents", {
+ type: "destroypad",
+ padId: pad.getId()
+ });
+ pro_pad_db.onDestroyPad(pad);
+}
+
+function onUserJoin(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ var name = userInfo.name || "unnamed";
+ log.custom("padevents", {
+ type: "userjoin",
+ padId: pad.getId(),
+ username: name,
+ ip: userInfo.ip,
+ userId: userInfo.userId
+ });
+ activepads.touch(pad.getId());
+ licensing.onUserJoin(userInfo);
+ log.onUserJoin(userInfo.userId);
+ padusers.notifyActive();
+ noprowatcher.onUserJoin(pad, userInfo);
+
+ });
+}
+
+function onUserLeave(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ var name = userInfo.name || "unnamed";
+ log.custom("padevents", {
+ type: "userleave",
+ padId: pad.getId(),
+ username: name,
+ ip: userInfo.ip,
+ userId: userInfo.userId
+ });
+ activepads.touch(pad.getId());
+ licensing.onUserLeave(userInfo);
+ noprowatcher.onUserLeave(pad, userInfo);
+
+ });
+}
+
+function onUserInfoChange(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ activepads.touch(pad.getId());
+
+ });
+}
+
+function onClientMessage(pad, senderUserInfo, msg) {
+ var padId = pad.getId();
+ activepads.touch(padId);
+
+ if (msg.type == "chat") {
+
+ chatarchive.onChatMessage(pad, senderUserInfo, msg);
+
+ var name = "unnamed";
+ if (senderUserInfo.name) {
+ name = senderUserInfo.name;
+ }
+
+ log.custom("chat", {
+ padId: padId,
+ userId: senderUserInfo.userId,
+ username: name,
+ text: msg.lineText
+ });
+ }
+ else if (msg.type == "padtitle") {
+ if (msg.title && padutils.isProPadId(pad.getId())) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ propad.setTitle(String(msg.title).substring(0, 80));
+ });
+ }
+ }
+ else if (msg.type == "padpassword") {
+ if (padutils.isProPadId(pad.getId())) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ propad.setPassword(msg.password || null);
+ });
+ }
+ }
+ else if (msg.type == "padoptions") {
+ // options object is a full set of options or just
+ // some options to change
+ var opts = msg.options;
+ var padOptions = pad.getPadOptionsObj();
+ if (opts.view) {
+ if (! padOptions.view) {
+ padOptions.view = {};
+ }
+ for(var k in opts.view) {
+ padOptions.view[k] = opts.view[k];
+ }
+ }
+ if (opts.guestPolicy) {
+ padOptions.guestPolicy = opts.guestPolicy;
+ if (opts.guestPolicy == 'deny') {
+ // boot guests!
+ collab_server.bootUsersFromPad(pad, "unauth", function(userInfo) {
+ return padusers.isGuest(userInfo.userId); }).forEach(function(userInfo) {
+ pad_security.revokePadUserAccess(padId, userInfo.userId); });
+ }
+ }
+ }
+ else if (msg.type == "guestanswer") {
+ if ((! msg.authId) || padusers.isGuest(msg.authId)) {
+ // not a pro user, forbid.
+ }
+ else {
+ pad_security.answerKnock(msg.guestId, padId, msg.answer);
+ }
+ }
+}
+
+function onEditPad(pad, authorId) {
+ log.callCatchingExceptions(function() {
+
+ pro_pad_db.onEditPad(pad, authorId);
+
+ });
+}
+
+
diff --git a/trunk/etherpad/src/etherpad/pad/padusers.js b/trunk/etherpad/src/etherpad/pad/padusers.js
new file mode 100644
index 0000000..f04f0eb
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/padusers.js
@@ -0,0 +1,397 @@
+/**
+ * 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("fastJSON");
+import("stringutils");
+import("jsutils.eachProperty");
+import("sync");
+import("etherpad.sessions");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("stringutils.randomHash");
+
+var _table = cachedSqlTable('pad_guests', 'pad_guests',
+ ['id', 'privateKey', 'userId'], processGuestRow);
+function processGuestRow(row) {
+ row.data = fastJSON.parse(row.data);
+}
+
+function notifySignIn() {
+ /*if (pro_accounts.isAccountSignedIn()) {
+ var proId = getUserId();
+ var guestId = _getGuestUserId();
+
+ var guestUser = _getGuestByKey('userId', guestId);
+ if (guestUser) {
+ var mods = {};
+ mods.data = guestUser.data;
+ // associate guest with proId
+ mods.data.replacement = proId;
+ // de-associate ET cookie with guest, otherwise
+ // the ET cookie would provide a semi-permanent way
+ // to effect changes under the pro account's name!
+ mods.privateKey = "replaced$"+_randomString(20);
+ _updateGuest('userId', guestId, mods);
+ }
+ }*/
+}
+
+function notifyActive() {
+ if (isGuest(getUserId())) {
+ _updateGuest('userId', getUserId(), {});
+ }
+}
+
+function notifyUserData(userData) {
+ var uid = getUserId();
+ if (isGuest(uid)) {
+ var data = _getGuestByKey('userId', uid).data;
+ if (userData.name) {
+ data.name = userData.name;
+ }
+ _updateGuest('userId', uid, {data: data});
+ }
+}
+
+function getUserId() {
+ if (pro_accounts.isAccountSignedIn()) {
+ return "p."+(getSessionProAccount().id);
+ }
+ else {
+ return getGuestUserId();
+ }
+}
+
+function getUserName() {
+ var uid = getUserId();
+ if (isGuest(uid)) {
+ var fromSession = sessions.getSession().guestDisplayName;
+ return fromSession || _getGuestByKey('userId', uid).data.name || null;
+ }
+ else {
+ return getSessionProAccount().fullName;
+ }
+}
+
+function getAccountIdForProAuthor(uid) {
+ if (uid.indexOf("p.") == 0) {
+ return Number(uid.substring(2));
+ }
+ else {
+ return -1;
+ }
+}
+
+function getNameForUserId(uid) {
+ if (isGuest(uid)) {
+ return _getGuestByKey('userId', uid).data.name || null;
+ }
+ else {
+ var accountNum = getAccountIdForProAuthor(uid);
+ if (accountNum < 0) {
+ return null;
+ }
+ else {
+ return pro_accounts.getAccountById(accountNum).fullName;
+ }
+ }
+}
+
+function isGuest(userId) {
+ return /^g/.test(userId);
+}
+
+function getGuestUserId() {
+ // cache the userId in the requestCache,
+ // for efficiency and consistency
+ var c = appjet.requestCache;
+ if (c.padGuestUserId === undefined) {
+ c.padGuestUserId = _computeGuestUserId();
+ }
+ return c.padGuestUserId;
+}
+
+function _getGuestTrackerId() {
+ // get ET cookie
+ var tid = sessions.getTrackingId();
+ if (tid == '-') {
+ // no tracking cookie? not a normal request?
+ return null;
+ }
+
+ // get domain ID
+ var domain = "-";
+ if (pro_utils.isProDomainRequest()) {
+ // e.g. "3"
+ domain = String(domains.getRequestDomainId());
+ }
+
+ // combine them
+ return domain+"$"+tid;
+}
+
+function _insertGuest(obj) {
+ // only requires 'userId' in obj
+
+ obj.createdDate = new Date;
+ obj.lastActiveDate = new Date;
+ if (! obj.data) {
+ obj.data = {};
+ }
+ if ((typeof obj.data) == "object") {
+ obj.data = fastJSON.stringify(obj.data);
+ }
+ if (! obj.privateKey) {
+ // private keys must be unique
+ obj.privateKey = "notracker$"+_randomString(20);
+ }
+
+ return _table.insert(obj);
+}
+
+function _getGuestByKey(keyColumn, value) {
+ return _table.getByKey(keyColumn, value);
+}
+
+function _updateGuest(keyColumn, value, obj) {
+ var obj2 = {};
+ eachProperty(obj, function(k,v) {
+ if (k == "data" && (typeof v) == "object") {
+ obj2.data = fastJSON.stringify(v);
+ }
+ else {
+ obj2[k] = v;
+ }
+ });
+
+ obj2.lastActiveDate = new Date;
+
+ _table.updateByKey(keyColumn, value, obj2);
+}
+
+function _newGuestUserId() {
+ return "g."+_randomString(16);
+}
+
+function _computeGuestUserId() {
+ // always returns some userId
+
+ var privateKey = _getGuestTrackerId();
+
+ if (! privateKey) {
+ // no tracking cookie, pretend there is one
+ privateKey = randomHash(16);
+ }
+
+ var userFromTracker = _table.getByKey('privateKey', privateKey);
+ if (userFromTracker) {
+ // we know this guy
+ return userFromTracker.userId;
+ }
+
+ // generate userId
+ var userId = _newGuestUserId();
+ var guest = {userId:userId, privateKey:privateKey};
+ var data = {};
+ guest.data = data;
+
+ var prefsCookieData = _getPrefsCookieData();
+ if (prefsCookieData) {
+ // found an old prefs cookie with an old userId
+ var oldUserId = prefsCookieData.userId;
+ // take the name and preferences
+ if ('name' in prefsCookieData) {
+ data.name = prefsCookieData.name;
+ }
+ /*['fullWidth','viewZoom'].forEach(function(pref) {
+ if (pref in prefsCookieData) {
+ data.prefs[pref] = prefsCookieData[pref];
+ }
+ });*/
+ }
+
+ _insertGuest(guest);
+ return userId;
+}
+
+function _getPrefsCookieData() {
+ // get userId from old prefs cookie if possible,
+ // but don't allow modern usernames
+
+ var prefsCookie = request.cookies['prefs'];
+ if (! prefsCookie) {
+ return null;
+ }
+ if (prefsCookie.charAt(0) != '%') {
+ return null;
+ }
+ try {
+ var cookieData = fastJSON.parse(unescape(prefsCookie));
+ // require one to three digits followed by dot at beginning of userId
+ if (/^[0-9]{1,3}\./.test(String(cookieData.userId))) {
+ return cookieData;
+ }
+ }
+ catch (e) {
+ return null;
+ }
+
+ return null;
+}
+
+function _randomString(len) {
+ // use only numbers and lowercase letters
+ var pieces = [];
+ for(var i=0;i<len;i++) {
+ pieces.push(Math.floor(Math.random()*36).toString(36).slice(-1));
+ }
+ return pieces.join('');
+}
+
+
+function cachedSqlTable(cacheName, tableName, keyColumns, processFetched) {
+ // Keeps a cache of sqlobj rows for the case where
+ // you want to select one row at a time by a single column
+ // at a time, taken from some set of key columns.
+ // The cache maps (keyColumn, value), e.g. ("id", 4) or
+ // ("secondaryKey", "foo123"), to an object, and each
+ // object is either present for all keyColumns
+ // (e.g. "id", "secondaryKey") or none.
+
+ if ((typeof keyColumns) == "string") {
+ keyColumns = [keyColumns];
+ }
+ processFetched = processFetched || (function(o) {});
+
+ function getCache() {
+ // this function is normally fast, only slow when cache
+ // needs to be created for the first time
+ var cache = appjet.cache[cacheName];
+ if (cache) {
+ return cache;
+ }
+ else {
+ // initialize in a synchronized block (double-checked locking);
+ // uses same lock as cache_utils.syncedWithCache would use.
+ sync.doWithStringLock("cache/"+cacheName, function() {
+ if (! appjet.cache[cacheName]) {
+ // values expire after 10 minutes
+ appjet.cache[cacheName] =
+ new net.appjet.common.util.ExpiringMapping(10*60*1000);
+ }
+ });
+ return appjet.cache[cacheName];
+ }
+ }
+
+ function cacheKey(keyColumn, value) {
+ // e.g. "id$4"
+ return keyColumn+"$"+String(value);
+ }
+
+ function getFromCache(keyColumn, value) {
+ return getCache().get(cacheKey(keyColumn, value));
+ }
+ function putInCache(obj) {
+ var cache = getCache();
+ // put in cache, keyed on all keyColumns we care about
+ keyColumns.forEach(function(keyColumn) {
+ cache.put(cacheKey(keyColumn, obj[keyColumn]), obj);
+ });
+ }
+ function touchInCache(obj) {
+ var cache = getCache();
+ keyColumns.forEach(function(keyColumn) {
+ cache.touch(cacheKey(keyColumn, obj[keyColumn]));
+ });
+ }
+ function removeObjFromCache(obj) {
+ var cache = getCache();
+ keyColumns.forEach(function(keyColumn) {
+ cache.remove(cacheKey(keyColumn, obj[keyColumn]));
+ });
+ }
+ function removeFromCache(keyColumn, value) {
+ var cached = getFromCache(keyColumn, value);
+ if (cached) {
+ removeObjFromCache(cached);
+ }
+ }
+
+ var self = {
+ clearCache: function() {
+ getCache().clear();
+ },
+ getByKey: function(keyColumn, value) {
+ // get cached object, if any
+ var cached = getFromCache(keyColumn, value);
+ if (! cached) {
+ // nothing in cache for this query, fetch from SQL
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ var fetched = sqlobj.selectSingle(tableName, keyToValue);
+ if (fetched) {
+ processFetched(fetched);
+ // fetched something, stick it in the cache
+ putInCache(fetched);
+ }
+ return fetched;
+ }
+ else {
+ // touch cached object and return
+ touchInCache(cached);
+ return cached;
+ }
+ },
+ updateByKey: function(keyColumn, value, obj) {
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ sqlobj.updateSingle(tableName, keyToValue, obj);
+ // remove old object from caches but
+ // don't put obj in cache, because it
+ // is likely a partial object
+ removeFromCache(keyColumn, value);
+ },
+ insert: function(obj) {
+ var returnVal = sqlobj.insert(tableName, obj);
+ // remove old object from caches but
+ // don't put obj in the cache; it doesn't
+ // have all values, e.g. for auto-generated ids
+ removeObjFromCache(obj);
+ return returnVal;
+ },
+ deleteByKey: function(keyColumn, value) {
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ sqlobj.deleteRows(tableName, keyToValue);
+ removeFromCache(keyColumn, value);
+ }
+ };
+ return self;
+}
+
+function _getClientIp() {
+ return (request.isDefined && request.clientIp) || '';
+}
+
+function getUserIdCreatedDate(userId) {
+ var record = sqlobj.selectSingle('pad_cookie_userids', {id: userId});
+ if (! record) { return; } // hm. weird case.
+ return record.createdDate;
+}
diff --git a/trunk/etherpad/src/etherpad/pad/padutils.js b/trunk/etherpad/src/etherpad/pad/padutils.js
new file mode 100644
index 0000000..3ffe70c
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/padutils.js
@@ -0,0 +1,154 @@
+/**
+ * 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("stringutils");
+
+import("etherpad.control.pro.account_control");
+
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pad.model");
+import("etherpad.sessions.getSession");
+
+jimport("java.lang.System.out.println");
+
+
+function setCurrentPad(p) {
+ appjet.context.attributes().update("currentPadId", p);
+}
+
+function clearCurrentPad() {
+ appjet.context.attributes()['$minus$eq']("currentPadId");
+}
+
+function getCurrentPad() {
+ var padOpt = appjet.context.attributes().get("currentPadId");
+ if (padOpt.isEmpty()) return null;
+ return padOpt.get();
+}
+
+function _parseCookie(text) {
+ try {
+ var cookieData = fastJSON.parse(unescape(text));
+ return cookieData;
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function getPrefsCookieData() {
+ var prefsCookie = request.cookies['prefs'];
+ if (!prefsCookie) {
+ return null;
+ }
+
+ return _parseCookie(prefsCookie);
+}
+
+function getPrefsCookieUserId() {
+ var cookieData = getPrefsCookieData();
+ if (! cookieData) {
+ return null;
+ }
+ return cookieData.userId || null;
+}
+
+/**
+ * Not valid to call this function outisde a HTTP request.
+ */
+function accessPadLocal(localPadId, fn, rwMode) {
+ if (!request.isDefined) {
+ throw Error("accessPadLocal() cannot run outside an HTTP request.");
+ }
+ var globalPadId = getGlobalPadId(localPadId);
+ var fnwrap = function(pad) {
+ pad.getLocalId = function() {
+ return getLocalPadId(pad);
+ };
+ return fn(pad);
+ }
+ return model.accessPadGlobal(globalPadId, fnwrap, rwMode);
+}
+
+/**
+ * Not valid to call this function outisde a HTTP request.
+ */
+function getGlobalPadId(localPadId) {
+ if (!request.isDefined) {
+ throw Error("getGlobalPadId() cannot run outside an HTTP request.");
+ }
+ if (pro_utils.isProDomainRequest()) {
+ return makeGlobalId(domains.getRequestDomainId(), localPadId);
+ } else {
+ // pad.spline.inf.fu-berlin.de pads
+ return localPadId;
+ }
+}
+
+function makeGlobalId(domainId, localPadId) {
+ return [domainId, localPadId].map(String).join('$');
+}
+
+function globalToLocalId(globalId) {
+ var parts = globalId.split('$');
+ if (parts.length == 1) {
+ return parts[0];
+ } else {
+ return parts[1];
+ }
+}
+
+function getLocalPadId(pad) {
+ var globalId = pad.getId();
+ return globalToLocalId(globalId);
+}
+
+function isProPadId(globalPadId) {
+ return (globalPadId.indexOf("$") > 0);
+}
+
+function isProPad(pad) {
+ return isProPadId(pad.getId());
+}
+
+function getDomainId(globalPadId) {
+ var parts = globalPadId.split("$");
+ if (parts.length < 2) {
+ return null;
+ } else {
+ return Number(parts[0]);
+ }
+}
+
+function makeValidLocalPadId(str) {
+ return str.replace(/[^a-zA-Z0-9\-]/g, '-');
+}
+
+function getProDisplayTitle(localPadId, title) {
+ if (title) {
+ return title;
+ }
+ if (stringutils.isNumeric(localPadId)) {
+ return ("Untitled "+localPadId);
+ } else {
+ return (localPadId);
+ }
+}
+
diff --git a/trunk/etherpad/src/etherpad/pad/revisions.js b/trunk/etherpad/src/etherpad/pad/revisions.js
new file mode 100644
index 0000000..c7c84e8
--- /dev/null
+++ b/trunk/etherpad/src/etherpad/pad/revisions.js
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("jsutils.cmp");
+import("stringutils");
+
+import("etherpad.utils.*");
+
+jimport("java.lang.System.out.println");
+
+/* revisionList is an array of revisionInfo structures.
+ *
+ * Each revisionInfo structure looks like:
+ *
+ * {
+ * timestamp: a number unix timestamp
+ * label: string
+ * savedBy: string of author name
+ * savedById: string id of the author
+ * revNum: revision number in the edit history
+ * id: the view id of the (formerly the id of the StorableObject)
+ * }
+ */
+
+/* returns array */
+function _getRevisionsArray(pad) {
+ var dataRoot = pad.getDataRoot();
+ if (!dataRoot.savedRevisions) {
+ dataRoot.savedRevisions = [];
+ }
+ dataRoot.savedRevisions.sort(function(a,b) {
+ return cmp(b.timestamp, a.timestamp);
+ });
+ return dataRoot.savedRevisions;
+}
+
+function _getPadRevisionById(pad, savedRevId) {
+ var revs = _getRevisionsArray(pad);
+ var rev;
+ for(var i=0;i<revs.length;i++) {
+ if (revs[i].id == savedRevId) {
+ rev = revs[i];
+ break;
+ }
+ }
+ return rev || null;
+}
+
+/*----------------------------------------------------------------*/
+/* public functions */
+/*----------------------------------------------------------------*/
+
+function getRevisionList(pad) {
+ return _getRevisionsArray(pad);
+}
+
+function saveNewRevision(pad, savedBy, savedById, revisionNumber, optIP, optTimestamp, optId) {
+ var revArray = _getRevisionsArray(pad);
+ var rev = {
+ timestamp: (optTimestamp || (+(new Date))),
+ label: null,
+ savedBy: savedBy,
+ savedById: savedById,
+ revNum: revisionNumber,
+ ip: (optIP || request.clientAddr),
+ id: (optId || stringutils.randomString(10)) // *probably* unique
+ };
+ revArray.push(rev);
+ rev.label = "Revision "+revArray.length;
+ return rev;
+}
+
+function setLabel(pad, savedRevId, userId, newLabel) {
+ var rev = _getPadRevisionById(pad, savedRevId);
+ if (!rev) {
+ throw new Error("revision does not exist: "+savedRevId);
+ }
+ /*if (rev.savedById != userId) {
+ throw new Error("cannot label someone else's revision.");
+ }
+ if (((+new Date) - rev.timestamp) > (24*60*60*1000)) {
+ throw new Error("revision is too old to label: "+savedRevId);
+ }*/
+ rev.label = newLabel;
+}
+
+function getStoredRevision(pad, savedRevId) {
+ return _getPadRevisionById(pad, savedRevId);
+}
+