diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/control/pad')
4 files changed, 1666 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js new file mode 100644 index 0000000..5af7ed0 --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js @@ -0,0 +1,280 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import("etherpad.helpers"); +import("etherpad.pad.model"); +import("etherpad.pad.padutils"); +import("etherpad.utils.*"); +import("fastJSON"); +import("etherpad.collab.server_utils.*"); +import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}"); +import("cache_utils.syncedWithCache"); +import("etherpad.log"); +jimport("net.appjet.common.util.LimitedSizeMapping"); + +import("stringutils"); +import("stringutils.sprintf"); + +var _JSON_CACHE_SIZE = 10000; + +// to clear: appjet.cache.pad_changeset_control.jsoncache.map.clear() +function _getJSONCache() { + return syncedWithCache('pad_changeset_control.jsoncache', function(cache) { + if (! cache.map) { + cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE); + } + return cache.map; + }); +} + +var _profiler = { + t: 0, + laps: [], + active: false, + start: function() { + _profiler.t = +new Date; + _profiler.laps = []; + //_profiler.active = true; + }, + lap: function(name) { + if (! _profiler.active) return; + var t2 = +new Date; + _profiler.laps.push([name, t2 - _profiler.t]); + }, + dump: function(info) { + if (! _profiler.active) return; + function padright(s, len) { + s = String(s); + return s + new Array(Math.max(0,len-s.length+1)).join(' '); + } + var str = padright(info,20)+": "; + _profiler.laps.forEach(function(e) { + str += padright(e.join(':'), 8); + }); + java.lang.System.out.println(str); + }, + stop: function() { + _profiler.active = false; + } +}; + +function onRequest() { + _profiler.start(); + + var parts = request.path.split('/'); + // TODO(kroo): create a mapping between padId and read-only id + var urlId = parts[4]; + var padId = parseUrlId(urlId).localPadId; + // var revisionId = parts[5]; + + padutils.accessPadLocal(padId, function(pad) { + if (! pad.exists() && pad.getSupportsTimeSlider()) { + response.forbid(); + } + }, 'r'); + + // use the query string to specify start and end revision numbers + var startRev = parseInt(request.params["s"]); + var endRev = startRev + 100 * parseInt(request.params["g"]); + var granularity = parseInt(request.params["g"]); + + _profiler.lap('A'); + var changesetsJson = + getCacheableChangesetInfoJSON(padId, startRev, endRev, granularity); + _profiler.lap('X'); + + //TODO(kroo): set content-type to javascript + response.write(changesetsJson); + _profiler.lap('J'); + if (request.acceptsGzip) { + response.setGzip(true); + } + + _profiler.lap('Z'); + _profiler.dump(startRev+'/'+granularity+'/'+endRev); + _profiler.stop(); + + return true; +} + +function getCacheableChangesetInfoJSON(padId, startNum, endNum, granularity) { + padutils.accessPadLocal(padId, function(pad) { + var lastRev = pad.getHeadRevisionNumber(); + if (endNum > lastRev+1) { + endNum = lastRev+1; + } + endNum = Math.floor(endNum / granularity)*granularity; + }, 'r'); + + var cacheKey = "C/"+startNum+"/"+endNum+"/"+granularity+"/"+ + padutils.getGlobalPadId(padId); + + var cache = _getJSONCache(); + + var cachedJson = cache.get(cacheKey); + if (cachedJson) { + cache.touch(cacheKey); + //java.lang.System.out.println("HIT! "+cacheKey); + return cachedJson; + } + else { + var result = getChangesetInfo(padId, startNum, endNum, granularity); + var json = fastJSON.stringify(result); + cache.put(cacheKey, json); + //java.lang.System.out.println("MISS! "+cacheKey); + return json; + } +} + +// uses changesets whose numbers are between startRev (inclusive) +// and endRev (exclusive); 0 <= startNum < endNum +function getChangesetInfo(padId, startNum, endNum, granularity) { + var forwardsChangesets = []; + var backwardsChangesets = []; + var timeDeltas = []; + var apool = new AttribPool(); + + var callId = stringutils.randomString(10); + + log.custom("getchangesetinfo", {event: "start", callId:callId, + padId:padId, startNum:startNum, + endNum:endNum, granularity:granularity}); + + // This function may take a while and avoids holding a lock on the pad. + // Though the pad may change during execution of this function, + // after we retrieve the HEAD revision number, all other accesses + // are unaffected by new revisions being added to the pad. + + var lastRev; + padutils.accessPadLocal(padId, function(pad) { + lastRev = pad.getHeadRevisionNumber(); + }, 'r'); + + if (endNum > lastRev+1) { + endNum = lastRev+1; + } + endNum = Math.floor(endNum / granularity)*granularity; + + var lines; + padutils.accessPadLocal(padId, function(pad) { + lines = _getPadLines(pad, startNum-1); + }, 'r'); + _profiler.lap('L'); + + var compositeStart = startNum; + while (compositeStart < endNum) { + var whileBodyResult = padutils.accessPadLocal(padId, function(pad) { + _profiler.lap('c0'); + if (compositeStart + granularity > endNum) { + return "break"; + } + var compositeEnd = compositeStart + granularity; + var forwards = _composePadChangesets(pad, compositeStart, compositeEnd); + _profiler.lap('c1'); + var backwards = Changeset.inverse(forwards, lines.textlines, + lines.alines, pad.pool()); + + _profiler.lap('c2'); + Changeset.mutateAttributionLines(forwards, lines.alines, pad.pool()); + _profiler.lap('c3'); + Changeset.mutateTextLines(forwards, lines.textlines); + _profiler.lap('c4'); + + var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.pool(), apool); + _profiler.lap('c5'); + var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.pool(), apool); + _profiler.lap('c6'); + function revTime(r) { + var date = pad.getRevisionDate(r); + var s = Math.floor((+date)/1000); + //java.lang.System.out.println("time "+r+": "+s); + return s; + } + + var t1, t2; + if (compositeStart == 0) { + t1 = revTime(0); + } + else { + t1 = revTime(compositeStart - 1); + } + t2 = revTime(compositeEnd - 1); + timeDeltas.push(t2 - t1); + + _profiler.lap('c7'); + forwardsChangesets.push(forwards2); + backwardsChangesets.push(backwards2); + + compositeStart += granularity; + }, 'r'); + if (whileBodyResult == "break") { + break; + } + } + + log.custom("getchangesetinfo", {event: "finish", callId:callId, + padId:padId, startNum:startNum, + endNum:endNum, granularity:granularity}); + + return { forwardsChangesets:forwardsChangesets, + backwardsChangesets:backwardsChangesets, + apool: apool.toJsonable(), + actualEndNum: endNum, + timeDeltas: timeDeltas }; +} + +// Compose a series of consecutive changesets from a pad. +// precond: startNum < endNum +function _composePadChangesets(pad, startNum, endNum) { + if (endNum - startNum > 1) { + var csFromPad = pad.getCoarseChangeset(startNum, endNum - startNum); + if (csFromPad) { + //java.lang.System.out.println("HIT! "+startNum+"-"+endNum); + return csFromPad; + } + else { + //java.lang.System.out.println("MISS! "+startNum+"-"+endNum); + } + //java.lang.System.out.println("composePadChangesets: "+startNum+','+endNum); + } + var changeset = pad.getRevisionChangeset(startNum); + for(var r=startNum+1; r<endNum; r++) { + var cs = pad.getRevisionChangeset(r); + changeset = Changeset.compose(changeset, cs, pad.pool()); + } + return changeset; +} + +// Get arrays of text lines and attribute lines for a revision +// of a pad. +function _getPadLines(pad, revNum) { + var atext; + _profiler.lap('PL0'); + if (revNum >= 0) { + atext = pad.getInternalRevisionAText(revNum); + } + else { + atext = Changeset.makeAText("\n"); + } + _profiler.lap('PL1'); + var result = {}; + result.textlines = Changeset.splitTextLines(atext.text); + _profiler.lap('PL2'); + result.alines = Changeset.splitAttributionLines(atext.attribs, + atext.text); + _profiler.lap('PL3'); + return result; +}
\ No newline at end of file diff --git a/trunk/etherpad/src/etherpad/control/pad/pad_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_control.js new file mode 100644 index 0000000..3c32202 --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/pad/pad_control.js @@ -0,0 +1,780 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import("funhtml.*"); +import("comet"); +import("email.sendEmail"); +import("fastJSON"); +import("jsutils.eachProperty"); +import("sqlbase.sqlbase"); +import("stringutils.{toHTML,md5}"); +import("stringutils"); + +import("etherpad.collab.collab_server"); +import("etherpad.debug.dmesg"); +import("etherpad.globals.*"); +import("etherpad.helpers"); +import("etherpad.licensing"); +import("etherpad.quotas"); +import("etherpad.log"); +import("etherpad.log.{logRequest,logException}"); +import("etherpad.sessions"); +import("etherpad.sessions.getSession"); +import("etherpad.utils.*"); +import("etherpad.pro.pro_padmeta"); +import("etherpad.pro.pro_pad_db"); +import("etherpad.pro.pro_utils"); +import("etherpad.pro.pro_accounts.getSessionProAccount"); +import("etherpad.pro.domains"); +import("etherpad.pro.pro_config"); +import("etherpad.pne.pne_utils"); +import("etherpad.pro.pro_quotas"); + +import("etherpad.pad.revisions"); +import("etherpad.pad.chatarchive"); +import("etherpad.pad.model"); +import("etherpad.pad.padutils"); +import("etherpad.pad.padusers"); +import("etherpad.control.pad.pad_view_control"); +import("etherpad.control.pad.pad_changeset_control"); +import("etherpad.control.pad.pad_importexport_control"); +import("etherpad.collab.readonly_server"); + +import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}"); + +jimport("java.lang.System.out.println"); + +var DISABLE_PAD_CREATION = false; + +function onStartup() { + sqlbase.createJSONTable("PAD_DIAGNOSTIC"); +} + +function onRequest() { + + // TODO: take a hard look at /ep/pad/FOO/BAR/ dispatching. + // Perhaps standardize on /ep/pad/<pad-id>/foo + if (request.path.indexOf('/ep/pad/auth/') == 0) { + if (request.isGet) { + return render_auth_get(); + } + if (request.isPost) { + return render_auth_post(); + } + } + + if (pro_utils.isProDomainRequest()) { + pro_quotas.perRequestBillingCheck(); + } + + var disp = new Dispatcher(); + disp.addLocations([ + [PrefixMatcher('/ep/pad/view/'), forward(pad_view_control)], + [PrefixMatcher('/ep/pad/changes/'), forward(pad_changeset_control)], + [PrefixMatcher('/ep/pad/impexp/'), forward(pad_importexport_control)], + [PrefixMatcher('/ep/pad/export/'), pad_importexport_control.renderExport] + ]); + return disp.dispatch(); +} + +//---------------------------------------------------------------- +// utils +//---------------------------------------------------------------- + +function getDefaultPadText() { + if (pro_utils.isProDomainRequest()) { + return pro_config.getConfig().defaultPadText; + } + return renderTemplateAsString("misc/pad_default.ejs", {padUrl: request.url.split("?", 1)[0]}); +} + +function assignName(pad, userId) { + if (padusers.isGuest(userId)) { + // use pad-specific name if possible + var userData = pad.getAuthorData(userId); + var nm = (userData && userData.name) || padusers.getUserName() || null; + + // don't let name guest typed in last once we've assigned a name + // for this pad, so the user can change it + delete getSession().guestDisplayName; + + return nm; + } + else { + return padusers.getUserName(); + } +} + +function assignColorId(pad, userId) { + // use pad-specific color if possible + var userData = pad.getAuthorData(userId); + if (userData && ('colorId' in userData)) { + return userData.colorId; + } + + // assign random unique color + function r(n) { + return Math.floor(Math.random() * n); + } + var colorsUsed = {}; + var users = collab_server.getConnectedUsers(pad); + var availableColors = []; + users.forEach(function(u) { + colorsUsed[u.colorId] = true; + }); + for (var i = 0; i < COLOR_PALETTE.length; i++) { + if (!colorsUsed[i]) { + availableColors.push(i); + } + } + if (availableColors.length > 0) { + return availableColors[r(availableColors.length)]; + } else { + return r(COLOR_PALETTE.length); + } +} + +function _getPrivs() { + return { + maxRevisions: quotas.getMaxSavedRevisionsPerPad() + }; +} + +//---------------------------------------------------------------- +// linkfile (a file that users can save that redirects them to +// a particular pad; auto-download) +//---------------------------------------------------------------- +function render_linkfile() { + var padId = request.params.padId; + + renderHtml("pad/pad_download_link.ejs", { + padId: padId + }); + + response.setHeader("Content-Disposition", "attachment; filename=\""+padId+".html\""); +} + +//---------------------------------------------------------------- +// newpad +//---------------------------------------------------------------- + +function render_newpad() { + var session = getSession(); + var padId; + + if (pro_utils.isProDomainRequest()) { + padId = pro_pad_db.getNextPadId(); + } else { + padId = randomUniquePadId(); + } + + session.instantCreate = padId; + response.redirect("/"+padId); +} + +// Tokbox +function render_newpad_xml_post() { + var localPadId; + if (pro_utils.isProDomainRequest()) { + localPadId = pro_pad_db.getNextPadId(); + } else { + localPadId = randomUniquePadId(); + } + // <RAFTER> + if (DISABLE_PAD_CREATION) { + if (! pro_utils.isProDomainRequest()) { + utils.render500(); + return; + } + } + // </RAFTER> + + padutils.accessPadLocal(localPadId, function(pad) { + if (!pad.exists()) { + pad.create(getDefaultPadText()); + } + }); + response.setContentType('text/plain; charset=utf-8'); + response.write([ + '<newpad>', + '<url>http://'+request.host+'/'+localPadId+'</url>', + '</newpad>' + ].join('\n')); +} + +//---------------------------------------------------------------- +// pad +//---------------------------------------------------------------- + +function _createIfNecessary(localPadId, pad) { + if (pad.exists()) { + delete getSession().instantCreate; + return; + } + // make sure localPadId is valid. + var validPadId = padutils.makeValidLocalPadId(localPadId); + if (localPadId != validPadId) { + response.redirect('/'+validPadId); + } + // <RAFTER> + if (DISABLE_PAD_CREATION) { + if (! pro_utils.isProDomainRequest()) { + response.redirect("/ep/pad/create?padId="+encodeURIComponent(localPadId)); + return; + } + } + // </RAFTER> + // tokbox may use createImmediately + if (request.params.createImmediately || getSession().instantCreate == localPadId) { + pad.create(getDefaultPadText()); + delete getSession().instantCreate; + return; + } + response.redirect("/ep/pad/create?padId="+encodeURIComponent(localPadId)); +} + +function _promptForMobileDevices(pad) { + // TODO: also work with blackbery and windows mobile and others + if (request.userAgent.isIPhone() && (!request.params.skipIphoneCheck)) { + renderHtml("pad/pad_iphone_body.ejs", {padId: pad.getLocalId()}); + response.stop(); + } +} + +function _checkPadQuota(pad) { + var numConnectedUsers = collab_server.getNumConnections(pad); + var maxUsersPerPad = quotas.getMaxSimultaneousPadEditors(pad.getId()); + + if (numConnectedUsers >= maxUsersPerPad) { + log.info("rendered-padfull"); + renderFramed('pad/padfull_body.ejs', + {maxUsersPerPad: maxUsersPerPad, padId: pad.getLocalId()}); + response.stop(); + } + + if (pne_utils.isPNE()) { + if (!licensing.canSessionUserJoin()) { + renderFramed('pad/total_users_exceeded.ejs', { + userQuota: licensing.getActiveUserQuota(), + activeUserWindowHours: licensing.getActiveUserWindowHours() + }); + response.stop(); + } + } +} + +function _checkIfDeleted(pad) { + // TODO: move to access control check on access? + if (pro_utils.isProDomainRequest()) { + pro_padmeta.accessProPad(pad.getId(), function(propad) { + if (propad.exists() && propad.isDeleted()) { + renderNoticeString("This pad has been deleted."); + response.stop(); + } + }); + } +} + +function render_pad(localPadId) { + var proTitle = null, documentBarTitle, initialPassword = null; + var isPro = isProDomainRequest(); + var userId = padusers.getUserId(); + + var opts = {}; + var globalPadId; + + if (isPro) { + pro_quotas.perRequestBillingCheck(); + } + + padutils.accessPadLocal(localPadId, function(pad) { + globalPadId = pad.getId(); + request.cache.globalPadId = globalPadId; + _createIfNecessary(localPadId, pad); + _promptForMobileDevices(pad); + _checkPadQuota(pad); + _checkIfDeleted(pad); + + if (request.params.inviteTo) { + getSession().nameGuess = request.params.inviteTo; + response.redirect('/'+localPadId); + } + var displayName; + if (request.params.displayName) { // tokbox + displayName = String(request.params.displayName); + } + else { + displayName = assignName(pad, userId); + } + + if (isProDomainRequest()) { + pro_padmeta.accessProPadLocal(localPadId, function(propad) { + proTitle = propad.getDisplayTitle(); + initialPassword = propad.getPassword(); + }); + } + documentBarTitle = (proTitle || "Public Pad"); + + var specialKey = request.params.specialKey || + (sessions.isAnEtherpadAdmin() ? collab_server.getSpecialKey('invisible') : + null); + if (request.params.fullScreen) { // tokbox, embedding + opts.fullScreen = true; + } + if (request.params.tokbox) { + opts.tokbox = true; + } + if (request.params.sidebar) { + opts.sidebar = Boolean(Number(request.params.sidebar)); + } + + helpers.addClientVars({ + padId: localPadId, + globalPadId: globalPadId, + userAgent: request.headers["User-Agent"], + collab_client_vars: collab_server.getCollabClientVars(pad), + debugEnabled: request.params.djs, + clientIp: request.clientAddr, + colorPalette: COLOR_PALETTE, + nameGuess: (getSession().nameGuess || null), + initialRevisionList: revisions.getRevisionList(pad), + serverTimestamp: +(new Date), + accountPrivs: _getPrivs(), + chatHistory: chatarchive.getRecentChatBlock(pad, 30), + numConnectedUsers: collab_server.getNumConnections(pad), + isProPad: isPro, + initialTitle: documentBarTitle, + initialPassword: initialPassword, + initialOptions: pad.getPadOptionsObj(), + userIsGuest: padusers.isGuest(userId), + userId: userId, + userName: displayName, + userColor: assignColorId(pad, userId), + specialKey: specialKey, + specialKeyTranslation: collab_server.translateSpecialKey(specialKey), + opts: opts + }); + }); + + var isProUser = (isPro && ! padusers.isGuest(userId)); + + var isFullWidth = false; + var hideSidebar = false; + var cookiePrefs = padutils.getPrefsCookieData(); + if (cookiePrefs) { + isFullWidth = !! cookiePrefs.fullWidth; + hideSidebar = !! cookiePrefs.hideSidebar; + } + if (opts.fullScreen) { + isFullWidth = true; + if (opts.tokbox) { + hideSidebar = true; + } + } + if ('sidebar' in opts) { + hideSidebar = ! opts.sidebar; + } + var bodyClass = (isFullWidth ? "fullwidth" : "limwidth")+ + " "+(isPro ? "propad" : "nonpropad")+" "+ + (isProUser ? "prouser" : "nonprouser"); + + var cookiePrefsToSet = {fullWidth:isFullWidth, hideSidebar:hideSidebar}; + helpers.addClientVars({cookiePrefsToSet: cookiePrefsToSet}); + + renderHtml("pad/pad_body2.ejs", + {localPadId:localPadId, + pageTitle:toHTML(proTitle || localPadId), + initialTitle:toHTML(documentBarTitle), + bodyClass: bodyClass, + hasOffice: hasOffice(), + isPro: isPro, + isProAccountHolder: isProUser, + account: getSessionProAccount(), // may be falsy + toHTML: toHTML, + prefs: {isFullWidth:isFullWidth, hideSidebar:hideSidebar}, + signinUrl: '/ep/account/sign-in?cont='+ + encodeURIComponent(request.url), + fullSuperdomain: pro_utils.getFullSuperdomainHost() + }); + return true; +} + +function render_create_get() { + var padId = request.params.padId; + // <RAFTER> + var template = (DISABLE_PAD_CREATION && ! pro_utils.isProDomainRequest()) ? + "pad/create_body_rafter.ejs" : + "pad/create_body.ejs"; + // </RAFTER> + renderFramed(template, {padId: padId, + fullSuperdomain: pro_utils.getFullSuperdomainHost()}); +} + +function render_create_post() { + var padId = request.params.padId; + getSession().instantCreate = padId; + response.redirect("/"+padId); +} + +//---------------------------------------------------------------- +// saverevision +//---------------------------------------------------------------- + +function render_saverevision_post() { + var padId = request.params.padId; + var savedBy = request.params.savedBy; + var savedById = request.params.savedById; + var revNum = request.params.revNum; + var privs = _getPrivs(); + padutils.accessPadLocal(padId, function(pad) { + if (! pad.exists()) { response.notFound(); } + var currentRevs = revisions.getRevisionList(pad); + if (currentRevs.length >= privs.maxRevisions) { + response.forbid(); + } + var savedRev = revisions.saveNewRevision(pad, savedBy, savedById, + revNum); + readonly_server.broadcastNewRevision(pad, savedRev); + response.setContentType('text/x-json'); + response.write(fastJSON.stringify(revisions.getRevisionList(pad))); + }); +} + +function render_saverevisionlabel_post() { + var userId = request.params.userId; + var padId = request.params.padId; + var revId = request.params.revId; + var newLabel = request.params.newLabel; + padutils.accessPadLocal(padId, function(pad) { + revisions.setLabel(pad, revId, userId, newLabel); + response.setContentType('text/x-json'); + response.write(fastJSON.stringify(revisions.getRevisionList(pad))); + }); +} + +function render_getrevisionatext_get() { + var padId = request.params.padId; + var revId = request.params.revId; + var result = null; + + var rev = padutils.accessPadLocal(padId, function(pad) { + var r = revisions.getStoredRevision(pad, revId); + var forWire = collab_server.getATextForWire(pad, r.revNum); + result = {atext:forWire.atext, apool:forWire.apool, + historicalAuthorData:forWire.historicalAuthorData}; + return r; + }, "r"); + + response.setContentType('text/plain; charset=utf-8'); + response.write(fastJSON.stringify(result)); +} + +//---------------------------------------------------------------- +// reconnect +//---------------------------------------------------------------- + +function _recordDiagnosticInfo(padId, diagnosticInfoJson) { + + var diagnosticInfo = {}; + try { + diagnosticInfo = fastJSON.parse(diagnosticInfoJson); + } catch (ex) { + log.warn("Error parsing diagnosticInfoJson: "+ex); + diagnosticInfo = {error: "error parsing JSON"}; + } + + // ignore userdups, unauth + if (diagnosticInfo.disconnectedMessage == "userdup" || + diagnosticInfo.disconnectedMessage == "unauth") { + return; + } + + var d = new Date(); + + diagnosticInfo.date = +d; + diagnosticInfo.strDate = String(d); + diagnosticInfo.clientAddr = request.clientAddr; + diagnosticInfo.padId = padId; + diagnosticInfo.headers = {}; + eachProperty(request.headers, function(k,v) { + diagnosticInfo.headers[k] = v; + }); + + var uid = diagnosticInfo.uniqueId; + + sqlbase.putJSON("PAD_DIAGNOSTIC", (diagnosticInfo.date)+"-"+uid, diagnosticInfo); + +} + +function recordMigratedDiagnosticInfo(objArray) { + objArray.forEach(function(obj) { + sqlbase.putJSON("PAD_DIAGNOSTIC", (obj.date)+"-"+obj.uniqueId, obj); + }); +} + +function render_reconnect() { + var localPadId = request.params.padId; + var globalPadId = padutils.getGlobalPadId(localPadId); + var userId = (padutils.getPrefsCookieUserId() || undefined); + var hasClientErrors = false; + var uniqueId; + try { + var obj = fastJSON.parse(request.params.diagnosticInfo); + uniqueId = obj.uniqueId; + errorMessage = obj.disconnectedMessage; + hasClientErrors = obj.collabDiagnosticInfo.errors.length > 0; + } catch (e) { + // guess it doesn't have errors. + } + + log.custom("reconnect", {globalPadId: globalPadId, userId: userId, + uniqueId: uniqueId, + hasClientErrors: hasClientErrors, + errorMessage: errorMessage }); + + try { + _recordDiagnosticInfo(globalPadId, request.params.diagnosticInfo); + } catch (ex) { + log.warn("Error recording diagnostic info: "+ex+" / "+request.params.diagnosticInfo); + } + + try { + _applyMissedChanges(localPadId, request.params.missedChanges); + } catch (ex) { + log.warn("Error applying missed changes: "+ex+" / "+request.params.missedChanges); + } + + response.redirect('/'+localPadId); +} + +/* posted asynchronously by the client as soon as reconnect dialogue appears. */ +function render_connection_diagnostic_info_post() { + var localPadId = request.params.padId; + var globalPadId = padutils.getGlobalPadId(localPadId); + var userId = (padutils.getPrefsCookieUserId() || undefined); + var hasClientErrors = false; + var uniqueId; + var errorMessage; + try { + var obj = fastJSON.parse(request.params.diagnosticInfo); + uniqueId = obj.uniqueId; + errorMessage = obj.disconnectedMessage; + hasClientErrors = obj.collabDiagnosticInfo.errors.length > 0; + } catch (e) { + // guess it doesn't have errors. + } + log.custom("disconnected_autopost", {globalPadId: globalPadId, userId: userId, + uniqueId: uniqueId, + hasClientErrors: hasClientErrors, + errorMessage: errorMessage}); + + try { + _recordDiagnosticInfo(globalPadId, request.params.diagnosticInfo); + } catch (ex) { + log.warn("Error recording diagnostic info: "+ex+" / "+request.params.diagnosticInfo); + } + response.setContentType('text/plain; charset=utf-8'); + response.write("OK"); +} + +function _applyMissedChanges(localPadId, missedChangesJson) { + var missedChanges; + try { + missedChanges = fastJSON.parse(missedChangesJson); + } catch (ex) { + log.warn("Error parsing missedChangesJson: "+ex); + return; + } + + padutils.accessPadLocal(localPadId, function(pad) { + if (pad.exists()) { + collab_server.applyMissedChanges(pad, missedChanges); + } + }); +} + +//---------------------------------------------------------------- +// feedback +//---------------------------------------------------------------- + +function render_feedback_post() { + var feedback = request.params.feedback; + var localPadId = request.params.padId; + var globalPadId = padutils.getGlobalPadId(localPadId); + var username = request.params.username; + var email = request.params.email; + var subject = 'EtherPad Feedback from '+request.clientAddr+' / '+globalPadId+' / '+username; + + if (feedback.indexOf("@") > 0) { + subject = "@ "+subject; + } + + feedback += "\n\n--\n"; + feedback += ("User Agent: "+request.headers['User-Agent'] + "\n"); + feedback += ("Session Referer: "+getSession().initialReferer + "\n"); + feedback += ("Email: "+email+"\n"); + + // log feedback + var userId = padutils.getPrefsCookieUserId(); + log.custom("feedback", { + globalPadId: globalPadId, + userId: userId, + email: email, + username: username, + feedback: request.params.feedback}); + + sendEmail( + 'feedback@pad.spline.inf.fu-berlin.de', + 'feedback@pad.spline.inf.fu-berlin.de', + subject, + {}, + feedback + ); + response.write("OK"); +} + +//---------------------------------------------------------------- +// emailinvite +//---------------------------------------------------------------- + +function render_emailinvite_post() { + var toEmails = String(request.params.toEmails).split(','); + var padId = String(request.params.padId); + var username = String(request.params.username); + var subject = String(request.params.subject); + var message = String(request.params.message); + + log.custom("padinvite", + {toEmails: toEmails, padId: padId, username: username, + subject: subject, message: message}); + + var fromAddr = '"EtherPad" <noreply@pad.spline.inf.fu-berlin.de>'; + // client enforces non-empty subject and message + var subj = '[EtherPad] '+subject; + var body = renderTemplateAsString('email/padinvite.ejs', + {body: message}); + var headers = {}; + var proAccount = getSessionProAccount(); + if (proAccount) { + headers['Reply-To'] = proAccount.email; + } + + response.setContentType('text/plain; charset=utf-8'); + try { + sendEmail(toEmails, fromAddr, subj, headers, body); + response.write("OK"); + } catch (e) { + logException(e); + response.setStatusCode(500); + response.write("Error"); + } +} + +//---------------------------------------------------------------- +// time-slider +//---------------------------------------------------------------- +function render_slider() { + var parts = request.path.split('/'); + var padOpaqueRef = parts[4]; + + helpers.addClientVars({padOpaqueRef:padOpaqueRef}); + + renderHtml("pad/padslider_body.ejs", { + // properties go here + }); + + return true; +} + +//---------------------------------------------------------------- +// auth +//---------------------------------------------------------------- + +function render_auth_get() { + var parts = request.path.split('/'); + var localPadId = parts[4]; + var errDiv; + if (getSession().padPassErr) { + errDiv = DIV({style: "border: 1px solid #fcc; background: #ffeeee; padding: 1em; margin: 1em 0;"}, + B(getSession().padPassErr)); + delete getSession().padPassErr; + } else { + errDiv = DIV(); + } + renderFramedHtml(function() { + return DIV({className: "fpcontent"}, + DIV({style: "margin: 1em;"}, + errDiv, + FORM({style: "border: 1px solid #ccc; padding: 1em; background: #fff6cc;", + action: request.path+'?'+request.query, + method: "post"}, + LABEL(B("Please enter the password required to access this pad:")), + BR(), BR(), + INPUT({type: "text", name: "password"}), INPUT({type: "submit", value: "Submit"}) + /*DIV(BR(), "Or ", A({href: '/ep/account/sign-in'}, "sign in"), ".")*/ + )), + DIV({style: "padding: 0 1em;"}, + P({style: "color: #444;"}, + "If you have forgotten a pad's password, contact your site administrator.", + " Site administrators can recover lost pad text through the \"Admin\" tab.") + ) + ); + }); + return true; +} + +function render_auth_post() { + var parts = request.path.split('/'); + var localPadId = parts[4]; + var domainId = domains.getRequestDomainId(); + if (!getSession().padPasswordAuth) { + getSession().padPasswordAuth = {}; + } + var currentPassword = pro_padmeta.accessProPadLocal(localPadId, function(propad) { + return propad.getPassword(); + }); + if (request.params.password == currentPassword) { + var globalPadId = padutils.getGlobalPadId(localPadId); + getSession().padPasswordAuth[globalPadId] = true; + } else { + getSession().padPasswordAuth[globalPadId] = false; + getSession().padPassErr = "Incorrect password."; + } + var cont = request.params.cont; + if (!cont) { + cont = '/'+localPadId; + } + response.redirect(cont); +} + +//---------------------------------------------------------------- +// chathistory +//---------------------------------------------------------------- + +function render_chathistory_get() { + var padId = request.params.padId; + var start = Number(request.params.start || 0); + var end = Number(request.params.end || 0); + var result = null; + + var rev = padutils.accessPadLocal(padId, function(pad) { + result = chatarchive.getChatBlock(pad, start, end); + }, "r"); + + response.setContentType('text/plain; charset=utf-8'); + response.write(fastJSON.stringify(result)); +} + diff --git a/trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js new file mode 100644 index 0000000..b7e5f4d --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js @@ -0,0 +1,319 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import("jsutils.arrayToSet"); +import("stringutils.{toHTML,md5}"); +import("stringutils"); +import("sync"); +import("varz"); + +import("etherpad.control.pad.pad_view_control.getRevisionInfo"); +import("etherpad.helpers"); +import("etherpad.importexport.importexport"); +import("etherpad.log"); +import("etherpad.pad.model"); +import("etherpad.pad.padutils"); +import("etherpad.pad.importhtml"); +import("etherpad.pad.exporthtml"); +import("etherpad.sessions"); +import("etherpad.sessions.getSession"); +import("etherpad.utils.{render404,renderFramedError}"); +import("etherpad.collab.server_utils"); + +function _log(obj) { + log.custom("import-export", obj); +} + +//--------------------------------------- +// utilities +//--------------------------------------- + +function _getPadTextBytes(padId, revNum) { + if (revNum === undefined) { + return null; + } + return padutils.accessPadLocal(padId, function(pad) { + if (pad.exists()) { + var txt = exporthtml.getPadPlainText(pad, revNum); + return (new java.lang.String(txt)).getBytes("UTF-8"); + } else { + return null; + } + }, 'r'); +} + +function _getPadHtmlBytes(padId, revNum, noDocType) { + if (revNum === undefined) { + return null; + } + var html = padutils.accessPadLocal(padId, function(pad) { + if (pad.exists()) { + return exporthtml.getPadHTMLDocument(pad, revNum, noDocType); + } + }); + if (html) { + return (new java.lang.String(html)).getBytes("UTF-8"); + } else { + return null; + } +} + +function _getFileExtension(fileName, def) { + if (fileName.lastIndexOf('.') > 0) { + return fileName.substr(fileName.lastIndexOf('.')+1); + } else { + return def; + } +} + +function _guessFileType(contentType, fileName) { + function _f(str) { return function() { return str; }} + var unchangedExtensions = + arrayToSet(['txt', 'htm', 'html', 'doc', 'docx', 'rtf', 'pdf', 'odt']); + var textExtensions = + arrayToSet(['js', 'scala', 'java', 'c', 'cpp', 'log', 'h', 'htm', 'html', 'css', 'php', 'xhtml', + 'dhtml', 'jsp', 'asp', 'sh', 'bat', 'pl', 'py']); + var contentTypes = { + 'text/plain': 'txt', + 'text/html': 'html', + 'application/msword': 'doc', + 'application/vnd.oasis.opendocument.text': 'odt', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', + 'text/rtf': 'rtf', + 'application/pdf': 'pdf' + } + + var ext = _getFileExtension(fileName); + if (ext) { + if (unchangedExtensions[ext]) { + return ext; + } else if (textExtensions[ext]) { + return 'txt'; + } + } + if (contentType in contentTypes) { + return contentTypes[contentType] + } + // unknown type, nothing to return. + _log({type: "warning", error: "unknown-type", contentType: contentType, fileName: fileName}); +} + +function _noteExportFailure() { + varz.incrementInt("export-failed"); +} + +function _noteImportFailure() { + varz.incrementInt("import-failed"); +} + +//--------------------------------------- +// export +//--------------------------------------- + +// handles /ep/pad/export/* +function renderExport() { + var parts = request.path.split('/'); + var padId = server_utils.parseUrlId(parts[4]).localPadId; + var revisionId = parts[5]; + var rev = null; + var format = request.params.format || 'txt'; + + if (! request.cache.skipAccess) { + _log({type: "request", direction: "export", format: format}); + rev = getRevisionInfo(padId, revisionId); + if (! rev) { + render404(); + } + request.cache.skipAccess = true; + } + + var result = _exportToFormat(padId, revisionId, (rev || {}).revNum, format); + if (result === true) { + response.stop(); + } else { + renderFramedError(result); + } + return true; +} + +function _exportToFormat(padId, revisionId, revNum, format) { + var bytes = _doExportConversion(format, + function() { return _getPadTextBytes(padId, revNum); }, + function(noDocType) { return _getPadHtmlBytes(padId, revNum, noDocType); }); + if (! bytes) { + return "Unable to convert file for export... try a different format?" + } else if (typeof(bytes) == 'string') { + return bytes + } else { + response.setContentType(importexport.formats[format]); + response.setHeader("Content-Disposition", "attachment; filename=\""+padId+"-"+revisionId+"."+format+"\""); + response.writeBytes(bytes); + return true; + } +} + + +function _doExportConversion(format, getTextBytes, getHtmlBytes) { + if (! (format in importexport.formats)) { + return false; + } + var bytes; + var srcFormat; + + if (format == 'txt') { + bytes = getTextBytes(); + srcFormat = 'txt'; + } else { + bytes = getHtmlBytes(format == 'doc' || format == 'odt'); + srcFormat = 'html'; + } + if (bytes == null) { + bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 0); + } + + try { + var ret = importexport.convertFile(srcFormat, format, bytes); + if (typeof(ret) == 'string') { + _log({type: "error", error: "export-failed", format: format, message: ret}); + _noteExportFailure(); + return ret; + } + bytes = ret; + } catch (e) { + if (e.javaException instanceof org.mortbay.jetty.RetryRequest) { + throw e.javaException + } + if (e.javaException || e.rhinoException) { + net.appjet.oui.exceptionlog.apply(e.javaException || e.rhinoException); + } + bytes = null; + } + if (bytes == null || bytes.length == 0) { + _log({type: "error", error: "export-failed", format: format, message: ret}); + _noteExportFailure(); + return false; + } + return bytes; +} + +//--------------------------------------- +// import +//--------------------------------------- + +function _getImportInfo(key) { + var session = getSession(); + sync.callsyncIfTrue(session, function() { return ! ('importexport' in session) }, + function() { + session.importexport = {}; + }); + var tokens = session.importexport; + sync.callsyncIfTrue(tokens, function() { return ! (key in tokens) }, + function() { + tokens[key] = {}; + }); + return tokens[key]; +} + +function render_import() { + function _r(code) { + response.setContentType("text/html"); + response.write("<html><body><script>try{parent.document.domain}catch(e){document.domain=document.domain}\n"+code+"</script></body></html>"); + response.stop(); + } + + if (! request.isPost) { + response.stop(); + } + + var padId = decodeURIComponent(request.params.padId); + if (! padId) { + response.stop(); + } + + var file = request.files.file; + if (! file) { + _r('parent.pad.handleImportExportFrameCall("importFailed", "Please select a file to import.")'); + } + + var bytes = file.bytes; + var type = _guessFileType(file.contentType, file.filesystemName); + + _log({type: "request", direction: "import", format: type}); + + if (! type) { + type = _getFileExtension(file.filesystemName, "no file extension found"); + _r('parent.pad.handleImportExportFrameCall("importFailed", "'+importexport.errorUnsupported(type)+'")'); + } + + var token = md5(bytes); + var state = _getImportInfo(token); + state.bytes = bytes; + state.type = type; + + _r("parent.pad.handleImportExportFrameCall('importSuccessful', '"+token+"')"); +} + + +function render_import2() { + var token = request.params.token; + + function _r(txt) { + response.write(txt); + response.stop(); + } + + if (! token) { _r("fail"); } + + var state = _getImportInfo(token); + if (! state.type || ! state.bytes) { _r("fail"); } + + var newBytes; + try { + newBytes = importexport.convertFile(state.type, "html", state.bytes); + } catch (e) { + if (e.javaException instanceof org.mortbay.jetty.RetryRequest) { + throw e.javaException; + } + net.appjet.oui.exceptionlog.apply(e); + throw e; + } + + if (typeof(newBytes) == 'string') { + _log({type: "error", error: "import-failed", format: state.type, message: newBytes}); + _noteImportFailure(); + _r("msg:"+newBytes); + } + + if (! newBytes || newBytes.length == 0) { + _r("fail"); + } + + var newHTML; + try { + newHTML = String(new java.lang.String(newBytes, "UTF-8")); + } catch (e) { + _r("fail"); + } + + if (! request.params.padId) { _r("fail"); } + padutils.accessPadLocal(request.params.padId, function(pad) { + if (! pad.exists()) { + _r("fail"); + } + importhtml.setPadHTML(pad, newHTML); + }); + _r("ok"); +} diff --git a/trunk/etherpad/src/etherpad/control/pad/pad_view_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_view_control.js new file mode 100644 index 0000000..0606d2c --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/pad/pad_view_control.js @@ -0,0 +1,287 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import("etherpad.helpers"); +import("etherpad.pad.model"); +import("etherpad.pad.padusers"); +import("etherpad.pad.padutils"); +import("etherpad.pad.exporthtml"); +import("etherpad.pro.pro_accounts"); +import("etherpad.pro.pro_padmeta"); +import("etherpad.utils.*"); +import("etherpad.pad.revisions"); +import("stringutils.toHTML"); +import("etherpad.collab.server_utils.*"); +import("etherpad.collab.collab_server.buildHistoricalAuthorDataMapForPadHistory"); +import("etherpad.collab.collab_server.getATextForWire"); +import("etherpad.control.pad.pad_changeset_control.getChangesetInfo"); +import("etherpad.globals"); +import("fastJSON"); +import("etherpad.collab.ace.easysync2.Changeset"); +import("etherpad.collab.ace.linestylefilter.linestylefilter"); +import("etherpad.collab.ace.domline.domline"); + +//---------------------------------------------------------------- +// view (viewing a static revision of a pad) +//---------------------------------------------------------------- + +function onRequest() { + var parts = request.path.split('/'); + // TODO(kroo): create a mapping between padId and read-only id + var readOnlyIdOrLocalPadId = parts[4]; + var parseResult = parseUrlId(readOnlyIdOrLocalPadId); + var isReadOnly = parseResult.isReadOnly; + var viewId = parseResult.viewId; + var localPadId = parseResult.localPadId; + var globalPadId = parseResult.globalPadId; + var roPadId = parseResult.roPadId; + var revisionId = parts[5]; + + var rev = getRevisionInfo(localPadId, revisionId); + if (! rev) { + return false; + } + + if (request.params.pt == 1) { + var padText = padutils.accessPadLocal(localPadId, function(pad) { + return pad.getRevisionText(rev.revNum); + }, 'r'); + + response.setContentType('text/plain; charset=utf-8'); + response.write(padText); + } else { + var padContents, totalRevs, atextForWire, savedRevisions; + var supportsSlider; + padutils.accessPadLocal(localPadId, function(pad) { + padContents = [_getPadHTML(pad, rev.revNum), + pad.getRevisionText(rev.revNum)]; + totalRevs = pad.getHeadRevisionNumber(); + atextForWire = getATextForWire(pad, rev.revNum); + savedRevisions = revisions.getRevisionList(pad); + supportsSlider = pad.getSupportsTimeSlider(); + }, 'r'); + + var _add = function(dict, anotherdict) { + for(var key in anotherdict) { + dict[key] = anotherdict[key]; + } + return dict; + } + + var getAdaptiveChangesetsArray = function(array, start, granularity) { + array = array || []; + start = start || 0; + granularity = granularity || Math.pow(10, Math.floor(Math.log(totalRevs+1) / Math.log(10))); + var changeset = _add(getChangesetInfo(localPadId, start, totalRevs+1, granularity), { + start: start, + granularity: Math.floor(granularity) + }); + array.push(changeset); + if(changeset.actualEndNum != totalRevs+1 && granularity > 1) + getAdaptiveChangesetsArray(array, changeset.actualEndNum, Math.floor(granularity / 10)); + return array; + } + var initialChangesets = []; + if (supportsSlider) { + initialChangesets = getAdaptiveChangesetsArray( + [ + _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 1000)*1000, Math.floor(rev.revNum / 1000)*1000+1000, 100), { + start: Math.floor(rev.revNum / 1000)*1000, + granularity: 100 + }), + _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 100)*100, Math.floor(rev.revNum / 100)*100+100, 10), { + start: Math.floor(rev.revNum / 100)*100, + granularity: 10 + }), + _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 10)*10, Math.floor(rev.revNum / 10)*10+10, 1), { + start: Math.floor(rev.revNum / 10)*10, + granularity: 1 + })] + ); + } + + var zpad = function(str, length) { + str = str+""; + while(str.length < length) + str = '0'+str; + return str; + }; + var dateFormat = function(savedWhen) { + var date = new Date(savedWhen); + var month = zpad(date.getMonth()+1,2); + var day = zpad(date.getDate(),2); + var year = (date.getFullYear()); + var hours = zpad(date.getHours(),2); + var minutes = zpad(date.getMinutes(),2); + var seconds = zpad(date.getSeconds(),2); + return ([month,'/',day,'/',year,' ',hours,':',minutes,':',seconds].join("")); + }; + + var proTitle = null; + var initialPassword = null; + if (isProDomainRequest()) { + pro_padmeta.accessProPadLocal(localPadId, function(propad) { + proTitle = propad.getDisplayTitle(); + initialPassword = propad.getPassword(); + }); + } + var documentBarTitle = (proTitle || "Public Pad"); + + var padHTML = padContents[0]; + var padText = padContents[1]; + + var historicalAuthorData = padutils.accessPadLocal(localPadId, function(pad) { + return buildHistoricalAuthorDataMapForPadHistory(pad); + }, 'r'); + + helpers.addClientVars({ + viewId: viewId, + initialPadContents: padText, + revNum: rev.revNum, + totalRevs: totalRevs, + initialChangesets: initialChangesets, + initialStyledContents: atextForWire, + savedRevisions: savedRevisions, + currentTime: rev.timestamp, + sliderEnabled: (!appjet.cache.killSlider) && request.params.slider != 0, + supportsSlider: supportsSlider, + historicalAuthorData: historicalAuthorData, + colorPalette: globals.COLOR_PALETTE, + padIdForUrl: readOnlyIdOrLocalPadId, + fullWidth: request.params.fullScreen == 1, + disableRightBar: request.params.sidebar == 0, + }); + + var userId = padusers.getUserId(); + var isPro = isProDomainRequest(); + var isProUser = (isPro && ! padusers.isGuest(userId)); + + var bodyClass = ["limwidth", + (isPro ? "propad" : "nonpropad"), + (isProUser ? "prouser" : "nonprouser")].join(" "); + + renderHtml("pad/padview_body.ejs", { + bodyClass: bodyClass, + isPro: isPro, + isProAccountHolder: isProUser, + account: pro_accounts.getSessionProAccount(), + signinUrl: '/ep/account/sign-in?cont='+ + encodeURIComponent(request.url), + padId: readOnlyIdOrLocalPadId, + padTitle: documentBarTitle, + rlabel: rev.label, + padHTML: padHTML, + padText: padText, + savedBy: rev.savedBy, + savedIp: rev.ip, + savedWhen: rev.timestamp, + toHTML: toHTML, + revisionId: revisionId, + dateFormat: dateFormat(rev.timestamp), + readOnly: isReadOnly, + roPadId: roPadId, + hasOffice: hasOffice() + }); + } + + return true; +} + +function getRevisionInfo(localPadId, revisionId) { + var rev = padutils.accessPadLocal(localPadId, function(pad) { + if (!pad.exists()) { + return null; + } + var r; + if (revisionId == "latest") { + // a "fake" revision for HEAD + var headRevNum = pad.getHeadRevisionNumber(); + r = { + revNum: headRevNum, + label: "Latest text of pad "+localPadId, + savedBy: null, + savedIp: null, + timestamp: +pad.getRevisionDate(headRevNum) + }; + } else if (revisionId == "autorecover") { + var revNum = _findLastGoodRevisionInPad(pad); + r = { + revNum: revNum, + label: "Auto-recovered text of pad "+localPadId, + savedBy: null, + savedIp: null, + timestamp: +pad.getRevisionDate(revNum) + }; + } else if(revisionId.indexOf("rev.") === 0) { + var revNum = parseInt(revisionId.split(".")[1]); + var latest = pad.getHeadRevisionNumber(); + if(revNum > latest) + revNum = latest; + r = { + revNum: revNum, + label: "Version " + revNum, + savedBy: null, + savedIp: null, + timestamp: +pad.getRevisionDate(revNum) + } + + } else { + r = revisions.getStoredRevision(pad, revisionId); + } + if (!r) { + return null; + } + return r; + }, "r"); + return rev; +} + +function _findLastGoodRevisionInPad(pad) { + var revNum = pad.getHeadRevisionNumber(); + function valueOrNullOnError(f) { + try { return f(); } catch (e) { return null; } + } + function isAcceptable(strOrNull) { + return (strOrNull && strOrNull.length > 20); + } + while (revNum > 0 && + ! isAcceptable(valueOrNullOnError(function() { return pad.getRevisionText(revNum); }))) { + revNum--; + } + return revNum; +} + +function _getPadHTML(pad, revNum) { + var atext = pad.getInternalRevisionAText(revNum); + var textlines = Changeset.splitTextLines(atext.text); + var alines = Changeset.splitAttributionLines(atext.attribs, + atext.text); + + var pieces = []; + var apool = pad.pool(); + for(var i=0;i<textlines.length;i++) { + var line = textlines[i]; + var aline = alines[i]; + var emptyLine = (line == '\n'); + var domInfo = domline.createDomLine(! emptyLine, true); + linestylefilter.populateDomLine(line, aline, apool, domInfo); + domInfo.prepareForAdd(); + var node = domInfo.node; + pieces.push('<div class="', node.className, '">', + node.innerHTML, '</div>\n'); + } + return pieces.join(''); +}
\ No newline at end of file |