diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/pad')
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/activepads.js | 52 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/chatarchive.js | 67 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/dbwriter.js | 338 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/easysync2migration.js | 675 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/exporthtml.js | 383 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/importhtml.js | 230 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/model.js | 651 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/noprowatcher.js | 110 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/pad_migrations.js | 206 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/pad_security.js | 237 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/padevents.js | 170 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/padusers.js | 397 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/padutils.js | 154 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pad/revisions.js | 103 |
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, '"')+'">'); + 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 = { + '&': '&', + '<': '<', + '>': '>', + }; + } + 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, ' '); + } + 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] = ' '; + 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] = ' '; + break; + } + else if (p.charAt(0) != "<") { + break; + } + } + } + else { + for(var i=0;i<parts.length;i++) { + var p = parts[i]; + if (p == " ") { + parts[i] = ' '; + } + } + } + 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); +} + |