/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import("fastJSON");
import("etherpad.utils.render404");
import("etherpad.pad.model");
import("etherpad.collab.collab_server");
import("etherpad.collab.ace.easysync2.*");
import("jsutils.eachProperty");
function _urlCache() {
if (!appjet.cache.historyUrlCache) {
appjet.cache.historyUrlCache = {};
}
return appjet.cache.historyUrlCache;
}
function _replyWithJSONAndCache(obj) {
obj.apiversion = _VERSION;
var output = fastJSON.stringify(obj);
_urlCache()[request.path] = output;
response.write(output);
response.stop();
}
function _replyWithJSON(obj) {
obj.apiversion = _VERSION;
response.write(fastJSON.stringify(obj));
response.stop();
}
function _error(msg, num) {
_replyWithJSON({error: String(msg), errornum: num});
}
var _VERSION = 1;
var _ERROR_REVISION_NUMBER_TOO_LARGE = 14;
function _do_text(padId, r) {
if (! padId) render404();
model.accessPadGlobal(padId, function(pad) {
if (! pad.exists()) {
render404();
}
if (r > pad.getHeadRevisionNumber()) {
_error("Revision number too large", _ERROR_REVISION_NUMBER_TOO_LARGE);
}
var text = pad.getInternalRevisionText(r);
text = _censorText(text);
_replyWithJSONAndCache({ text: text });
});
}
function _do_stat(padId) {
var obj = {};
if (! padId) {
obj.exists = false;
}
else {
model.accessPadGlobal(padId, function(pad) {
if (! pad.exists()) {
obj.exists = false;
}
else {
obj.exists = true;
obj.latestRev = pad.getHeadRevisionNumber();
}
});
}
_replyWithJSON(obj);
}
function _censorText(text) {
// may not change length of text
return text.replace(/(http:\/\/pad.spline.inf.fu-berlin.de\/)(\w+)/g, function(url, u1, u2) {
return u1 + u2.replace(/\w/g, '-');
});
}
function _do_changes(padId, first, last) {
if (! padId) render404();
var charPool = [];
var changeList = [];
function charPoolText(txt) {
charPool.push(txt);
return _encodeVarInt(txt.length);
}
model.accessPadGlobal(padId, function(pad) {
if (first > pad.getHeadRevisionNumber() || last > pad.getHeadRevisionNumber()) {
_error("Revision number too large", _ERROR_REVISION_NUMBER_TOO_LARGE);
}
var curAText = Changeset.makeAText("\n");
if (first > 0) {
curAText = pad.getInternalRevisionAText(first - 1);
}
curAText.text = _censorText(curAText.text);
var lastTimestamp = null;
for(var r=first;r<=last;r++) {
var binRev = [];
var timestamp = +pad.getRevisionDate(r);
binRev.push(_encodeTimeStamp(timestamp, lastTimestamp));
lastTimestamp = timestamp;
binRev.push(_encodeVarInt(1)); // fake author
var c = pad.getRevisionChangeset(r);
var splices = Changeset.toSplices(c);
splices.forEach(function (splice) {
var startChar = splice[0];
var endChar = splice[1];
var newText = splice[2];
oldText = curAText.text.substring(startChar, endChar);
if (oldText.length == 0) {
binRev.push('+');
binRev.push(_encodeVarInt(startChar));
binRev.push(charPoolText(newText));
}
else if (newText.length == 0) {
binRev.push('-');
binRev.push(_encodeVarInt(startChar));
binRev.push(charPoolText(oldText));
}
else {
binRev.push('*');
binRev.push(_encodeVarInt(startChar));
binRev.push(charPoolText(oldText));
binRev.push(charPoolText(newText));
}
});
changeList.push(binRev.join(''));
curAText = Changeset.applyToAText(c, curAText, pad.pool());
}
_replyWithJSONAndCache({charPool: charPool.join(''), changes: changeList.join(',')});
});
}
function render_history(padOpaqueRef, rest) {
if (_urlCache()[request.path]) {
response.write(_urlCache()[request.path]);
response.stop();
return true;
}
var padId;
if (padOpaqueRef == "CSi1xgbFXl" || padOpaqueRef == "13sentences") {
// made-up, hard-coded opaque ref, should be a table for these
padId = "jbg5HwzUX8";
}
else if (padOpaqueRef == "dO1j7Zf34z" || padOpaqueRef == "foundervisa") {
// made-up, hard-coded opaque ref, should be a table for these
padId = "3hS7kQyDXG";
}
else {
padId = null;
}
var regexResult;
if ((regexResult = /^stat$/.exec(rest))) {
_do_stat(padId);
}
else if ((regexResult = /^text\/(\d+)$/.exec(rest))) {
var r = Number(regexResult[1]);
_do_text(padId, r);
}
else if ((regexResult = /^changes\/(\d+)-(\d+)$/.exec(rest))) {
_do_changes(padId, Number(regexResult[1]), Number(regexResult[2]));
}
else {
return false;
}
}
function _encodeVarInt(num) {
var n = +num;
if (isNaN(n)) {
throw new Error("Can't encode non-number "+num);
}
var chars = [];
var done = false;
while (! done) {
if (n < 32) done = true;
var nd = (n % 32);
if (chars.length > 0) {
// non-first, will become non-last digit
nd = (nd | 32);
}
chars.push(_BASE64_DIGITS[nd]);
n = Math.floor(n / 32)
}
return chars.reverse().join('');
}
var _BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._";
function _encodeTimeStamp(tMillis, baseMillis) {
var t = Math.floor(tMillis/1000);
var base = Math.floor(baseMillis/1000);
var absolute = ["+", t];
var resultPair = absolute;
if (((typeof base) == "number") && base <= t) {
var relative = ["", t - base];
if (relative[1] < absolute[1]) {
resultPair = relative;
}
}
return resultPair[0] + _encodeVarInt(resultPair[1]);
}