aboutsummaryrefslogtreecommitdiffstats
path: root/trunk/etherpad/src/etherpad/control/pad
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/etherpad/src/etherpad/control/pad')
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js280
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_control.js780
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js319
-rw-r--r--trunk/etherpad/src/etherpad/control/pad/pad_view_control.js287
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