remove dav.js (gone) and remote.js (resources now in resource.js)

This commit is contained in:
Dan Mills 2008-11-03 14:38:34 -08:00
parent 542bee77cd
commit 2ffe3b8d8f
2 changed files with 0 additions and 1217 deletions

View File

@ -1,493 +0,0 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Bookmarks Sync.
*
* The Initial Developer of the Original Code is Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2007
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Dan Mills <thunder@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
const EXPORTED_SYMBOLS = ['DAV', 'DAVCollection'];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://weave/log4moz.js");
Cu.import("resource://weave/constants.js");
Cu.import("resource://weave/util.js");
Cu.import("resource://weave/identity.js");
Cu.import("resource://weave/async.js");
Function.prototype.async = Async.sugar;
Utils.lazy(this, 'DAV', DAVCollection);
let DAVLocks = {default: null};
/*
* DAV object
* Abstracts the raw DAV commands
*/
function DAVCollection(baseURL, defaultPrefix) {
this.baseURL = baseURL;
this.defaultPrefix = defaultPrefix;
this._identity = 'DAV:default';
this._log = Log4Moz.Service.getLogger("Service.DAV");
this._log.level =
Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.dav")];
}
DAVCollection.prototype = {
__dp: null,
get _dp() {
if (!this.__dp)
this.__dp = Cc["@mozilla.org/xmlextras/domparser;1"].
createInstance(Ci.nsIDOMParser);
return this.__dp;
},
get identity() { return this._identity; },
set identity(value) { this._identity = value; },
get baseURL() {
return this._baseURL;
},
set baseURL(value) {
if (value && value[value.length-1] != '/')
value = value + '/';
this._baseURL = value;
},
get defaultPrefix() {
return this._defaultPrefix;
},
set defaultPrefix(value) {
if (value && value[value.length-1] != '/')
value = value + '/';
if (value && value[0] == '/')
value = value.slice(1);
if (!value)
value = '';
this._defaultPrefix = value;
},
get locked() {
return !this._allowLock || (DAVLocks['default'] &&
DAVLocks['default'].token);
},
_allowLock: true,
get allowLock() this._allowLock,
set allowLock(value) {
this._allowLock = value;
},
_makeRequest: function DC__makeRequest(op, path, headers, data) {
let self = yield;
let ret;
this._log.debug(op + " request for " + (path? path : 'root folder'));
if (!path || path[0] != '/')
path = this._defaultPrefix + path; // if relative: prepend default prefix
else
path = path.slice(1); // if absolute: remove leading slash
// path at this point should have no leading slash.
if (this._lastProgress)
throw "Request already in progress";
else
this._lastProgress = Date.now();
let xhrCb = self.cb;
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
// check for stalled connections
let listener = new Utils.EventListener(this._timeoutCb(request, xhrCb));
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(listener, CONNECTION_TIMEOUT,
timer.TYPE_REPEATING_SLACK);
request.onload = xhrCb;
request.onerror = xhrCb;
request.onprogress =Utils.bind2(this, this._onProgress);
request.mozBackgroundRequest = true;
request.open(op, this._baseURL + path, true);
// Force cache validation
let channel = request.channel;
channel = channel.QueryInterface(Ci.nsIRequest);
let loadFlags = channel.loadFlags;
loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS;
channel.loadFlags = loadFlags;
let key;
for (key in headers) {
if (key == 'Authorization')
this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
else
this._log.trace("HTTP Header " + key + ": " + headers[key]);
request.setRequestHeader(key, headers[key]);
}
let event = yield request.send(data);
timer.cancel();
this._lastProgress = null;
self.done(event.target);
},
_onProgress: function DC__onProgress(event) {
this._lastProgress = Date.now();
},
_timeoutCb: function DC__timeoutCb(request, callback) {
return function() {
if (Date.now() - this._lastProgress > CONNECTION_TIMEOUT) {
this._log.warn("Connection timed out");
request.abort();
callback({target:{status:-1}});
}
};
},
get _defaultHeaders() {
let h = {'Content-type': 'text/plain'},
id = ID.get(this.identity),
lock = DAVLocks['default'];
if (id)
h['Authorization'] = 'Basic ' + btoa(id.username + ":" + id.password);
if (lock)
h['If'] = "<" + lock.URL + "> (<" + lock.token + ">)";
return h;
},
// mkdir -p
_mkcol: function DC__mkcol(path) {
let self = yield;
let ok = true;
try {
let components = path.split('/');
let path2 = '';
for (let i = 0; i < components.length; i++) {
// trailing slashes will cause an empty path component at the end
if (components[i] == '')
continue;
path2 = path2 + components[i];
// check if it exists first
this._makeRequest.async(this, self.cb, "GET", path2 + "/", this._defaultHeaders);
let ret = yield;
if (ret.status != 404) {
this._log.trace("Skipping creation of path " + path2 +
" (got status " + ret.status + ")");
} else {
this._log.debug("Creating path: " + path2);
this._makeRequest.async(this, self.cb, "MKCOL", path2,
this._defaultHeaders);
ret = yield;
if (ret.status != 201) {
this._log.debug(ret.responseText);
throw 'request failed: ' + ret.status;
}
}
// add slash *after* the request, trailing slashes cause a 412!
path2 = path2 + "/";
}
} catch (e) {
this._log.error("Could not create directory on server");
this._log.error("Exception caught: " + (e.message? e.message : e) +
" - " + (e.location? e.location : ""));
ok = false;
}
self.done(ok);
},
GET: function DC_GET(path, onComplete) {
return this._makeRequest.async(this, onComplete, "GET", path,
this._defaultHeaders);
},
POST: function DC_POST(path, data, onComplete) {
return this._makeRequest.async(this, onComplete, "POST", path,
this._defaultHeaders, data);
},
formPost: function DC_formPOST(path, data, onComplete) {
let headers = {'Content-type': 'application/x-www-form-urlencoded'};
headers.__proto__ = this._defaultHeaders;
return this._makeRequest.async(this, onComplete, "POST", path,
headers, data);
},
PUT: function DC_PUT(path, data, onComplete) {
return this._makeRequest.async(this, onComplete, "PUT", path,
this._defaultHeaders, data);
},
DELETE: function DC_DELETE(path, onComplete) {
return this._makeRequest.async(this, onComplete, "DELETE", path,
this._defaultHeaders);
},
MKCOL: function DC_MKCOL(path, onComplete) {
return this._mkcol.async(this, onComplete, path);
},
PROPFIND: function DC_PROPFIND(path, data, onComplete) {
let headers = {'Content-type': 'text/xml; charset="utf-8"',
'Depth': '0'};
headers.__proto__ = this._defaultHeaders;
return this._makeRequest.async(this, onComplete, "PROPFIND", path,
headers, data);
},
LOCK: function DC_LOCK(path, data, onComplete) {
let headers = {'Content-type': 'text/xml; charset="utf-8"',
'Depth': 'infinity',
'Timeout': 'Second-600'};
headers.__proto__ = this._defaultHeaders;
return this._makeRequest.async(this, onComplete, "LOCK", path, headers, data);
},
UNLOCK: function DC_UNLOCK(path, onComplete) {
let headers = {'Lock-Token': '<' + DAVLocks['default'].token + '>'};
headers.__proto__ = this._defaultHeaders;
return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers);
},
// Get all files
listFiles: function DC_listFiles(path) {
let self = yield;
if (!path)
path = "";
let headers = {'Content-type': 'text/xml; charset="utf-8"',
'Depth': '1'};
headers.__proto__ = this._defaultHeaders;
this._makeRequest.async(this, self.cb, "PROPFIND", path, headers,
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
"<D:propfind xmlns:D='DAV:'><D:prop/></D:propfind>");
let resp = yield;
Utils.ensureStatus(resp.status, "propfind failed");
let ret = [];
try {
let elts = Utils.xpath(resp.responseXML, '//D:href');
// FIXME: shouldn't depend on the first one being the root
let root = elts.iterateNext();
root = root.textContent;
let elt;
while (elt = elts.iterateNext())
ret.push(elt.textContent.replace(root, ''));
} catch (e) {}
self.done(ret);
},
// Login / Logout
checkLogin: function DC_checkLogin(username, password) {
let self = yield;
this._log.debug("checkLogin called for user " + username);
let headers = {
'Content-type' : 'text/plain',
'Authorization' : 'Basic ' + btoa(username + ":" + password)
};
let lock = DAVLocks['default'];
if (lock)
headers['If'] = "<" + lock.URL + "> (<" + lock.token + ">)";
// Make a call to make sure it's working
this._makeRequest.async(this, self.cb, "GET", "", headers);
let resp = yield;
this._log.debug("checkLogin got response status " + resp.status);
self.done(resp.status);
},
// Locking
_getActiveLock: function DC__getActiveLock() {
let self = yield;
let ret = null;
this._log.debug("Getting active lock token");
this.PROPFIND("lock",
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
"<D:propfind xmlns:D='DAV:'>" +
" <D:prop><D:lockdiscovery/></D:prop>" +
"</D:propfind>", self.cb);
let resp = yield;
if (resp.status < 200 || resp.status >= 300) {
self.done(false);
yield;
}
let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href');
let token = tokens.iterateNext();
if (token)
ret = token.textContent;
if (ret)
this._log.trace("Found an active lock token");
else
this._log.trace("No active lock token found");
self.done({URL: this._baseURL, token: ret});
},
lock: function DC_lock() {
let self = yield;
let resp;
try {
this._log.trace("Acquiring lock");
if (this.locked) {
this._log.debug("Lock called, but we are already locked");
return;
}
this._allowLock = false;
resp = yield this.LOCK("lock",
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
"<D:lockinfo xmlns:D=\"DAV:\">\n" +
" <D:locktype><D:write/></D:locktype>\n" +
" <D:lockscope><D:exclusive/></D:lockscope>\n" +
"</D:lockinfo>", self.cb);
if (!Utils.checkStatus(resp.status))
return;
let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href');
let token = tokens.iterateNext();
if (token) {
DAVLocks['default'] = {
URL: this._baseURL,
token: token.textContent
};
}
if (DAVLocks['default']) {
this._log.trace("Lock acquired");
self.done(DAVLocks['default']);
}
} catch (e) {
this._log.error("Could not acquire lock");
if (resp.responseText)
this._log.error("Server response to LOCK:\n" + resp.responseText);
throw e;
} finally {
this._allowLock = true;
}
},
unlock: function DC_unlock() {
let self = yield;
this._log.trace("Releasing lock");
if (!this.locked) {
this._log.debug("Unlock called, but we don't hold a token right now");
self.done(true);
return;
}
try {
let resp = yield this.UNLOCK("lock", self.cb);
if (Utils.checkStatus(resp.status)) {
this._log.trace("Lock released");
self.done(true);
} else {
this._log.trace("Failed to release lock");
self.done(false);
}
} catch (e) {
throw e;
} finally {
// Do this unconditionally, since code that calls unlock() doesn't
// really have much of an option if unlock fails. The only thing
// to do is wait for it to time out (and hope it didn't really
// fail)
if (DAVLocks['default'])
delete DAVLocks['default'];
}
},
forceUnlock: function DC_forceUnlock() {
let self = yield;
let unlocked = true;
this._log.debug("Forcibly releasing any server locks");
this._getActiveLock.async(this, self.cb);
DAVLocks['default'] = yield;
if (!DAVLocks['default']) {
this._log.debug("No server lock found");
self.done(true);
yield;
}
this._log.trace("Server lock found, unlocking");
this.unlock.async(this, self.cb);
unlocked = yield;
if (unlocked)
this._log.trace("Lock released");
else
this._log.trace("No lock released");
self.done(unlocked);
}
};

View File

@ -1,724 +0,0 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Bookmarks Sync.
*
* The Initial Developer of the Original Code is Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2007
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Dan Mills <thunder@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
const EXPORTED_SYMBOLS = ['Resource', 'JsonFilter', 'CryptoFilter',
'Keychain', 'RemoteStore'];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://weave/log4moz.js");
Cu.import("resource://weave/constants.js");
Cu.import("resource://weave/util.js");
Cu.import("resource://weave/crypto.js");
Cu.import("resource://weave/async.js");
Cu.import("resource://weave/identity.js");
Cu.import("resource://weave/dav.js");
Cu.import("resource://weave/stores.js");
Function.prototype.async = Async.sugar;
function RequestException(resource, action, request) {
this._resource = resource;
this._action = action;
this._request = request;
this.location = Components.stack.caller;
}
RequestException.prototype = {
get resource() { return this._resource; },
get action() { return this._action; },
get request() { return this._request; },
get status() { return this._request.status; },
toString: function ReqEx_toString() {
return "Could not " + this._action + " resource " + this._resource.path +
" (" + this._request.status + ")";
}
};
function Resource(path) {
this._init(path);
}
Resource.prototype = {
get identity() { return this._identity; },
set identity(value) { this._identity = value; },
get dav() { return this._dav; },
set dav(value) { this._dav = value; },
get path() { return this._path; },
set path(value) {
this._dirty = true;
this._path = value;
},
get data() { return this._data; },
set data(value) {
this._dirty = true;
this._data = value;
},
__os: null,
get _os() {
if (!this.__os)
this.__os = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
return this.__os;
},
get lastRequest() { return this._lastRequest; },
get downloaded() { return this._downloaded; },
get dirty() { return this._dirty; },
pushFilter: function Res_pushFilter(filter) {
this._filters.push(filter);
},
popFilter: function Res_popFilter() {
return this._filters.pop();
},
clearFilters: function Res_clearFilters() {
this._filters = [];
},
_init: function Res__init(path) {
this._identity = null; // unused
this._dav = null; // unused
this._path = path;
this._data = null;
this._downloaded = false;
this._dirty = false;
this._filters = [];
this._lastRequest = null;
this._log = Log4Moz.Service.getLogger("Service.Resource");
},
// note: this is unused, and it's not clear whether it's useful or not
_sync: function Res__sync() {
let self = yield;
let ret;
// If we've set the locally stored value, upload it. If we
// haven't, and we haven't yet downloaded this resource, then get
// it. Otherwise do nothing (don't try to get it every time)
if (this.dirty) {
this.put(self.cb);
ret = yield;
} else if (!this.downloaded) {
this.get(self.cb);
ret = yield;
}
self.done(ret);
},
sync: function Res_sync(onComplete) {
this._sync.async(this, onComplete);
},
_request: function Res__request(action, data) {
let self = yield;
let listener, timer;
let iter = 0;
if ("PUT" == action) {
for each (let filter in this._filters) {
data = yield filter.beforePUT.async(filter, self.cb, data);
}
}
while (true) {
switch (action) {
case "GET":
DAV.GET(this.path, self.cb);
break;
case "PUT":
DAV.PUT(this.path, data, self.cb);
break;
case "DELETE":
DAV.DELETE(this.path, self.cb);
break;
default:
throw "Unknown request action for Resource";
}
this._lastRequest = yield;
if (action == "DELETE" &&
Utils.checkStatus(this._lastRequest.status, null, [[200,300],404])) {
this._dirty = false;
this._data = null;
break;
} else if (Utils.checkStatus(this._lastRequest.status)) {
this._log.debug(action + " request successful");
this._dirty = false;
if (action == "GET")
this._data = this._lastRequest.responseText;
//else if (action == "PUT")
// this._data = data; // wrong! (because of filters)
break;
} else if (action == "GET" && this._lastRequest.status == 404) {
throw new RequestException(this, action, this._lastRequest);
} else if (iter >= 10) {
// iter too big? bail
throw new RequestException(this, action, this._lastRequest);
} else {
// wait for a bit and try again
if (!timer) {
listener = new Utils.EventListener(self.cb);
timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
}
yield timer.initWithCallback(listener, iter * iter * 1000,
timer.TYPE_ONE_SHOT);
iter++;
}
}
if ("GET" == action) {
let filters = this._filters.slice(); // reverse() mutates, so we copy
for each (let filter in filters.reverse()) {
this._data = yield filter.afterGET.async(filter, self.cb, this._data);
}
}
self.done(this._data);
},
get: function Res_get(onComplete) {
this._request.async(this, onComplete, "GET");
},
put: function Res_put(onComplete, data) {
if ("undefined" == typeof(data))
data = this._data;
this._request.async(this, onComplete, "PUT", data);
},
delete: function Res_delete(onComplete) {
this._request.async(this, onComplete, "DELETE");
}
};
function ResourceSet(basePath) {
this._init(basePath);
}
ResourceSet.prototype = {
__proto__: new Resource(),
_init: function ResSet__init(basePath) {
this.__proto__.__proto__._init.call(this);
this._basePath = basePath;
this._log = Log4Moz.Service.getLogger("Service.ResourceSet");
},
_hack: function ResSet__hack(action, id, data) {
let self = yield;
let savedData = this._data;
if ("PUT" == action)
this._data = data;
this._path = this._basePath + id;
yield this._request.async(this, self.cb, action, data);
let newData = this._data;
this._data = savedData;
if (this._data == null)
this._data = {};
this._data[id] = newData;
self.done(this._data[id]);
},
get: function ResSet_get(onComplete, id) {
this._hack.async(this, onComplete, "GET", id);
},
put: function ResSet_put(onComplete, id, data) {
this._hack.async(this, onComplete, "PUT", id, data);
},
delete: function ResSet_delete(onComplete, id) {
this._hack.async(this, onComplete, "DELETE", id);
}
};
function ResourceFilter() {
this._log = Log4Moz.Service.getLogger("Service.ResourceFilter");
}
ResourceFilter.prototype = {
beforePUT: function ResFilter_beforePUT(data) {
let self = yield;
this._log.debug("Doing absolutely nothing")
self.done(data);
},
afterGET: function ResFilter_afterGET(data) {
let self = yield;
this._log.debug("Doing absolutely nothing")
self.done(data);
}
};
function JsonFilter() {
this._log = Log4Moz.Service.getLogger("Service.JsonFilter");
}
JsonFilter.prototype = {
__proto__: new ResourceFilter(),
__os: null,
get _os() {
if (!this.__os)
this.__os = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
return this.__os;
},
get _json() {
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
this.__defineGetter__("_json", function() json);
return this._json;
},
beforePUT: function JsonFilter_beforePUT(data) {
let self = yield;
this._log.debug("Encoding data as JSON");
this._os.notifyObservers(null, "weave:service:sync:status", "stats.encoding-json");
self.done(this._json.encode(data));
},
afterGET: function JsonFilter_afterGET(data) {
let self = yield;
this._log.debug("Decoding JSON data");
this._os.notifyObservers(null, "weave:service:sync:status", "stats.decoding-json");
self.done(this._json.decode(data));
}
};
function CryptoFilter(identity) {
this._identity = identity;
this._log = Log4Moz.Service.getLogger("Service.CryptoFilter");
}
CryptoFilter.prototype = {
__proto__: new ResourceFilter(),
get _os() {
let os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
this.__defineGetter__("_os", function() os);
return os;
},
beforePUT: function CryptoFilter_beforePUT(data) {
let self = yield;
this._log.debug("Encrypting data");
this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting");
let ret = yield Crypto.encryptData.async(Crypto, self.cb, data, this._identity);
self.done(ret);
},
afterGET: function CryptoFilter_afterGET(data) {
let self = yield;
this._log.debug("Decrypting data");
this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting");
let ret = yield Crypto.decryptData.async(Crypto, self.cb, data, this._identity);
self.done(ret);
}
};
function Keychain(prefix) {
this._init(prefix);
}
Keychain.prototype = {
__proto__: new Resource(),
_init: function Keychain__init(prefix) {
this.__proto__.__proto__._init.call(this, prefix + "keys.json");
this.pushFilter(new JsonFilter());
},
_initialize: function Keychain__initialize(identity) {
let self = yield;
let wrappedSymkey;
if ("none" != Utils.prefs.getCharPref("encryption")) {
this._os.notifyObservers(null, "weave:service:sync:status", "status.generating-random-key");
yield Crypto.randomKeyGen.async(Crypto, self.cb, identity);
// Wrap (encrypt) this key with the user's public key.
let idRSA = ID.get('WeaveCryptoID');
this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting-key");
wrappedSymkey = yield Crypto.wrapKey.async(Crypto, self.cb,
identity.bulkKey, idRSA);
}
let keys = {ring: {}, bulkIV: identity.bulkIV};
this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-key");
keys.ring[identity.username] = wrappedSymkey;
yield this.put(self.cb, keys);
},
initialize: function Keychain_initialize(onComplete, identity) {
this._initialize.async(this, onComplete, identity);
},
_getKeyAndIV: function Keychain__getKeyAndIV(identity) {
let self = yield;
this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-keyring");
yield this.get(self.cb);
if (!this.data || !this.data.ring || !this.data.ring[identity.username])
throw "Keyring does not contain a key for this user";
// Unwrap (decrypt) the key with the user's private key.
this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting-key");
let idRSA = ID.get('WeaveCryptoID');
let symkey = yield Crypto.unwrapKey.async(Crypto, self.cb,
this.data.ring[identity.username], idRSA);
let iv = this.data.bulkIV;
identity.bulkKey = symkey;
identity.bulkIV = iv;
},
_setKey: function KeyChain__setKey(bulkID, newID) {
/* FIXME!: It's possible that the keyring is changed on the server
after we do a GET. Then we're just uploading this new local keyring,
thereby losing any changes made on the server keyring since this GET.
Also, if this.data was not instantiated properly (i.e. you're
using KeyChain directly instead of getting it from the engine),
you run the risk of wiping the server-side keychain.
*/
let self = yield;
this.get(self.cb);
yield;
let wrappedKey = yield Crypto.wrapKey.async(Crypto, self.cb,
bulkID.bulkKey, newID);
this.data.ring[newID.username] = wrappedKey;
this.put(self.cb, this.data);
yield;
},
getKeyAndIV: function Keychain_getKeyAndIV(onComplete, identity) {
this._getKeyAndIV.async(this, onComplete, identity);
},
setKey: function Keychain_setKey(onComplete, bulkID, newID) {
this._setKey.async(this, onComplete, bulkID, newID);
}
};
function RemoteStore(engine) {
this._engine = engine;
this._log = Log4Moz.Service.getLogger("Service.RemoteStore");
}
RemoteStore.prototype = {
get serverPrefix() this._engine.serverPrefix,
get engineId() this._engine.engineId,
__os: null,
get _os() {
if (!this.__os)
this.__os = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
return this.__os;
},
get status() {
let status = new Resource(this.serverPrefix + "status.json");
status.pushFilter(new JsonFilter());
this.__defineGetter__("status", function() status);
return status;
},
get keys() {
let keys = new Keychain(this.serverPrefix);
this.__defineGetter__("keys", function() keys);
return keys;
},
get _snapshot() {
let snapshot = new Resource(this.serverPrefix + "snapshot.json");
snapshot.pushFilter(new JsonFilter());
snapshot.pushFilter(new CryptoFilter(this._engine.engineId));
this.__defineGetter__("_snapshot", function() snapshot);
return snapshot;
},
get _deltas() {
let deltas = new ResourceSet(this.serverPrefix + "deltas/");
deltas.pushFilter(new JsonFilter());
deltas.pushFilter(new CryptoFilter(this._engine.engineId));
this.__defineGetter__("_deltas", function() deltas);
return deltas;
},
_openSession: function RStore__openSession(lastSyncSnap) {
let self = yield;
if (!this.serverPrefix || !this.engineId)
throw "Cannot initialize RemoteStore: engine has no server prefix or crypto ID";
this.status.data = null;
this.keys.data = null;
this._snapshot.data = null;
this._deltas.data = null;
this._lastSyncSnap = lastSyncSnap;
let ret = yield DAV.MKCOL(this.serverPrefix + "deltas", self.cb);
if (!ret)
throw "Could not create remote folder";
this._log.debug("Downloading status file");
this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-status");
yield this.status.get(self.cb);
this._log.debug("Downloading status file... done");
// Bail out if the server has a newer format version than we can parse
if (this.status.data.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) {
this._log.error("Server uses storage format v" +
this.status.data.formatVersion +
", this client understands up to v" +
ENGINE_STORAGE_FORMAT_VERSION);
throw "Incompatible remote store format";
}
if (this.status.data.GUID != lastSyncSnap.GUID) {
this._log.trace("Remote GUID: " + this.status.data.GUID);
this._log.trace("Local GUID: " + lastSyncSnap.GUID);
this._log.debug("Server wipe since last sync, resetting last sync snapshot");
lastSyncSnap.wipe();
lastSyncSnap.GUID = this.status.data.GUID;
// yield this._store.resetGUIDs(self.cb); // XXX not sure if this is really needed (and it needs to be done from the engine if so)
}
this._log.info("Last sync snapshot version: " + lastSyncSnap.version);
this._log.info("Server maxVersion: " + this.status.data.maxVersion);
if ("none" != Utils.prefs.getCharPref("encryption"))
yield this.keys.getKeyAndIV(self.cb, this.engineId);
},
openSession: function RStore_openSession(onComplete, lastSyncSnap) {
this._openSession.async(this, onComplete, lastSyncSnap);
},
closeSession: function RStore_closeSession() {
this.status.data = null;
this.keys.data = null;
this._snapshot.data = null;
this._deltas.data = null;
this._lastSyncSnap = null;
},
// Does a fresh upload of the given snapshot to a new store
// FIXME: add 'metadata' arg here like appendDelta's
_initialize: function RStore__initialize(snapshot) {
let self = yield;
yield this.keys.initialize(self.cb, this.engineId);
this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-snapshot");
yield this._snapshot.put(self.cb, snapshot.data);
let c = 0;
for (GUID in snapshot.data)
c++;
this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-status");
yield this.status.put(self.cb,
{GUID: snapshot.GUID,
formatVersion: ENGINE_STORAGE_FORMAT_VERSION,
snapVersion: snapshot.version,
maxVersion: snapshot.version,
snapEncryption: Crypto.defaultAlgorithm,
deltasEncryption: Crypto.defaultAlgorithm,
itemCount: c});
this._log.info("Full upload to server successful");
},
initialize: function RStore_initialize(onComplete, snapshot) {
this._initialize.async(this, onComplete, snapshot);
},
// Removes server files - you may want to run initialize() after this
// FIXME: might want to do a PROPFIND instead (to catch all deltas in one go)
_wipe: function Engine__wipe() {
let self = yield;
this._log.debug("Deleting remote store data");
yield this.status.delete(self.cb);
yield this.keys.delete(self.cb);
yield this._snapshot.delete(self.cb);
//yield this._deltas.delete(self.cb);
this._log.debug("Server files deleted");
},
wipe: function Engine_wipe(onComplete) {
this._wipe.async(this, onComplete)
},
// Gets the latest server snapshot by downloading all server files
// (snapshot + deltas)
_getLatestFromScratch: function RStore__getLatestFromScratch() {
let self = yield;
this._log.info("Downloading all server data from scratch");
this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-snapshot");
let snap = new SnapshotStore();
snap.data = yield this._snapshot.get(self.cb);
this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas");
let status = this.status.data;
for (let id = status.snapVersion + 1; id <= status.maxVersion; id++) {
let delta = yield this._deltas.get(self.cb, id);
yield snap.applyCommands.async(snap, self.cb, delta);
}
self.done(snap.data);
},
// Gets the latest server snapshot by downloading only the necessary
// deltas from the given snapshot (but may fall back to a full download)
_getLatestFromSnap: function RStore__getLatestFromSnap() {
let self = yield;
let deltas, snap = new SnapshotStore();
snap.version = this.status.data.maxVersion;
if (!this._lastSyncSnap ||
this._lastSyncSnap.version < this.status.data.snapVersion) {
this._log.trace("Getting latest from scratch (last sync snap too old)");
snap.data = yield this._getLatestFromScratch.async(this, self.cb);
self.done(snap.data);
return;
} else if (this._lastSyncSnap.version >= this.status.data.snapVersion &&
this._lastSyncSnap.version < this.status.data.maxVersion) {
this._log.debug("Using last sync snapshot as starting point for server snapshot");
snap.data = Utils.deepCopy(this._lastSyncSnap.data);
this._log.info("Downloading server deltas");
this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas");
deltas = [];
let min = this._lastSyncSnap.version + 1;
let max = this.status.data.maxVersion;
for (let id = min; id <= max; id++) {
let delta = yield this._deltas.get(self.cb, id);
deltas.push(delta);
}
} else if (this._lastSyncSnap.version == this.status.data.maxVersion) {
this._log.debug("Using last sync snapshot as server snapshot (snap version == max version)");
this._log.trace("Local snapshot version == server maxVersion");
snap.data = Utils.deepCopy(this._lastSyncSnap.data);
deltas = [];
} else { // this._lastSyncSnap.version > this.status.data.maxVersion
this._log.error("Server snapshot is older than local snapshot");
throw "Server snapshot is older than local snapshot";
}
try {
for (var i = 0; i < deltas.length; i++) {
yield snap.applyCommands.async(snap, self.cb, deltas[i]);
}
} catch (e) {
this._log.warn("Error applying remote deltas to saved snapshot, attempting a full download");
this._log.debug("Exception: " + Utils.exceptionStr(e));
this._log.trace("Stack:\n" + Utils.stackTrace(e));
snap.data = yield this._getLatestFromScratch.async(this, self.cb);
}
self.done(snap.data);
},
// get the latest server snapshot. If a snapshot is given, try to
// download only the necessary deltas to get to the latest
_wrap: function RStore__wrap() {
let self = yield;
let ret = yield this._getLatestFromSnap.async(this, self.cb);
self.done(ret);
},
wrap: function RStore_wrap(onComplete) {
this._wrap.async(this, onComplete);
},
// Adds a new set of changes (a delta) to this store
_appendDelta: function RStore__appendDelta(snapshot, delta, metadata) {
let self = yield;
if (metadata) {
for (let key in metadata)
this.status.data[key] = metadata[key];
}
let c = 0;
for (item in snapshot.data)
c++;
this.status.data.itemCount = c;
let id = ++this.status.data.maxVersion;
// upload the delta even if we upload a new snapshot, so other clients
// can be spared of a full re-download
this._os.notifyObservers(null, "weave:service:sync:status",
"status.uploading-deltas");
yield this._deltas.put(self.cb, id, delta);
// if we have more than KEEP_DELTAS, then upload a new snapshot
// this allows us to remove old deltas
if ((id - this.status.data.snapVersion) > KEEP_DELTAS) {
this._os.notifyObservers(null, "weave:service:sync:status",
"status.uploading-snapshot");
yield this._snapshot.put(self.cb, snapshot.data);
this.status.data.snapVersion = id;
}
// XXX we could define another constant here
// (e.g. KEEP_MAX_DELTAS) to define when to actually start
// deleting deltas from the server. However, we can do this more
// efficiently server-side
// finally, upload a new status file
this._os.notifyObservers(null, "weave:service:sync:status",
"status.uploading-status");
yield this.status.put(self.cb);
},
appendDelta: function RStore_appendDelta(onComplete, snapshot, delta, metadata) {
this._appendDelta.async(this, onComplete, snapshot, delta, metadata);
}
};