aboutsummaryrefslogblamecommitdiffstats
path: root/etherpad/src/etherpad/control/historycontrol.js
blob: a78cfadd462447af77d62dc50519cf2207af04f7 (plain) (tree)























































































                                                                            
                                                                                               








































































































































                                                                                         
/**
 * 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]);
}