зеркало из https://github.com/mozilla/gecko-dev.git
remove sync cores, they are no longer used; remove sharing code from bookmarks (we can bring it back when we need it again, needs work); have the engine give hints to the store about when to wrap all items and cache them (to make subsequent calls to wrap one item fast); move serializeItem/getAllIDs into the store
This commit is contained in:
Родитель
55df62217e
Коммит
e11a3250a5
|
@ -52,7 +52,6 @@ Cu.import("resource://weave/resource.js");
|
|||
Cu.import("resource://weave/clientData.js");
|
||||
Cu.import("resource://weave/identity.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/trackers.js");
|
||||
Cu.import("resource://weave/async.js");
|
||||
|
||||
|
@ -136,19 +135,13 @@ Engine.prototype = {
|
|||
|
||||
get score() this._tracker.score,
|
||||
|
||||
// _core, _store, and tracker need to be overridden in subclasses
|
||||
// _store, and tracker need to be overridden in subclasses
|
||||
get _store() {
|
||||
let store = new Store();
|
||||
this.__defineGetter__("_store", function() store);
|
||||
return store;
|
||||
},
|
||||
|
||||
get _core() {
|
||||
let core = new SyncCore(this._store);
|
||||
this.__defineGetter__("_core", function() core);
|
||||
return core;
|
||||
},
|
||||
|
||||
get _tracker() {
|
||||
let tracker = new Tracker();
|
||||
this.__defineGetter__("_tracker", function() tracker);
|
||||
|
@ -294,15 +287,15 @@ SyncEngine.prototype = {
|
|||
},
|
||||
|
||||
// Create a new record starting from an ID
|
||||
// Calls _serializeItem to get the actual item, but sometimes needs to be
|
||||
// Calls _store.wrapItem to get the actual item, but sometimes needs to be
|
||||
// overridden anyway (to alter parentid or other properties outside the payload)
|
||||
_createRecord: function SyncEngine__newCryptoWrapper(id, encrypt) {
|
||||
_createRecord: function SyncEngine__createRecord(id, encrypt) {
|
||||
let self = yield;
|
||||
|
||||
let record = new CryptoWrapper();
|
||||
record.uri = this.engineURL + id;
|
||||
record.encryption = this.cryptoMetaURL;
|
||||
record.cleartext = yield this._serializeItem.async(this, self.cb, id);
|
||||
record.cleartext = this._store.wrapItem(id);
|
||||
|
||||
if (record.cleartext && record.cleartext.parentid)
|
||||
record.parentid = record.cleartext.parentid;
|
||||
|
@ -313,18 +306,6 @@ SyncEngine.prototype = {
|
|||
self.done(record);
|
||||
},
|
||||
|
||||
// Serialize an item. This will become the encrypted field in the payload of
|
||||
// a record. Needs to be overridden in a subclass
|
||||
_serializeItem: function SyncEngine__serializeItem(id) {
|
||||
let self = yield;
|
||||
self.done({});
|
||||
},
|
||||
|
||||
_getAllIDs: function SyncEngine__getAllIDs() {
|
||||
let self = yield;
|
||||
self.done({});
|
||||
},
|
||||
|
||||
// Check if a record is "like" another one, even though the IDs are different,
|
||||
// in that case, we'll change the ID of the local item to match
|
||||
// Probably needs to be overridden in a subclass, to change which criteria
|
||||
|
@ -406,19 +387,27 @@ SyncEngine.prototype = {
|
|||
this._tracker.clearChangedIDs();
|
||||
|
||||
// now add all current ones
|
||||
let all = yield this._getAllIDs.async(this, self.cb);
|
||||
let all = this._store.getAllIDs();
|
||||
for (let id in all) {
|
||||
this._tracker.changedIDs[id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// generate queue from changed items list
|
||||
|
||||
// XXX should have a heuristic like this, but then we need to be able to
|
||||
// serialize each item by itself, something our stores can't currently do
|
||||
//if (this._tracker.changedIDs.length >= 30)
|
||||
this._store.cacheItemsHint();
|
||||
|
||||
// NOTE we want changed items -> outgoing -> server to be as atomic as
|
||||
// possible, so we clear the changed IDs after we upload the changed records
|
||||
// NOTE2 don't encrypt, we'll do that before uploading instead
|
||||
for (let id in this._tracker.changedIDs) {
|
||||
this.outgoing.push(yield this._createRecord.async(this, self.cb, id, false));
|
||||
}
|
||||
|
||||
this._store.clearItemCacheHint();
|
||||
},
|
||||
|
||||
// Generate outgoing records
|
||||
|
|
|
@ -59,658 +59,14 @@ Cu.import("resource://weave/util.js");
|
|||
Cu.import("resource://weave/crypto.js");
|
||||
Cu.import("resource://weave/async.js");
|
||||
Cu.import("resource://weave/engines.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
Cu.import("resource://weave/trackers.js");
|
||||
Cu.import("resource://weave/identity.js");
|
||||
Cu.import("resource://weave/xmpp/xmppClient.js");
|
||||
Cu.import("resource://weave/notifications.js");
|
||||
Cu.import("resource://weave/sharing.js");
|
||||
Cu.import("resource://weave/resource.js");
|
||||
|
||||
Function.prototype.async = Async.sugar;
|
||||
|
||||
function BookmarksSharingManager(engine) {
|
||||
this._init(engine);
|
||||
}
|
||||
BookmarksSharingManager.prototype = {
|
||||
__annoSvc: null,
|
||||
get _annoSvc() {
|
||||
if (!this.__anoSvc)
|
||||
this.__annoSvc = Cc["@mozilla.org/browser/annotation-service;1"].
|
||||
getService(Ci.nsIAnnotationService);
|
||||
return this.__annoSvc;
|
||||
},
|
||||
|
||||
__bms: null,
|
||||
get _bms() {
|
||||
if (!this.__bms)
|
||||
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
return this.__bms;
|
||||
},
|
||||
|
||||
__myUsername: null,
|
||||
get _myUsername() {
|
||||
if (!this.__myUsername)
|
||||
this.__myUsername = ID.get('WeaveID').username;
|
||||
return this.__myUsername;
|
||||
},
|
||||
|
||||
_init: function SharingManager__init(engine) {
|
||||
this._engine = engine;
|
||||
this._log = Log4Moz.repository.getLogger("Bookmark Share");
|
||||
if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) {
|
||||
this._log.info( "Starting XMPP client for bookmark engine..." );
|
||||
this._startXmppClient.async(this);
|
||||
}
|
||||
},
|
||||
|
||||
_startXmppClient: function BmkSharing__startXmppClient() {
|
||||
// To be called asynchronously.
|
||||
let self = yield;
|
||||
|
||||
// Get serverUrl and realm of the jabber server from preferences:
|
||||
let serverUrl = Utils.prefs.getCharPref( "xmpp.server.url" );
|
||||
let realm = Utils.prefs.getCharPref( "xmpp.server.realm" );
|
||||
|
||||
/* Username/password for XMPP are the same as the ones for Weave,
|
||||
so read them from the weave identity: */
|
||||
let clientName = this._myUsername;
|
||||
let clientPassword = ID.get('WeaveID').password;
|
||||
|
||||
let transport = new HTTPPollingTransport( serverUrl, false, 15000 );
|
||||
let auth = new PlainAuthenticator();
|
||||
/* LONGTERM TODO would prefer to use MD5Authenticator instead,
|
||||
once we get it working, but since we are connecting over SSL, the
|
||||
Plain auth is probably fine for now. */
|
||||
this._xmppClient = new XmppClient( clientName,
|
||||
realm,
|
||||
clientPassword,
|
||||
transport,
|
||||
auth );
|
||||
let bmkSharing = this;
|
||||
let messageHandler = {
|
||||
handle: function ( messageText, from ) {
|
||||
/* The callback function for incoming xmpp messages.
|
||||
We expect message text to be in the form of:
|
||||
<command> <serverpath> <foldername>.
|
||||
Where command is one of:
|
||||
"share" (sender offers to share directory dir with us)
|
||||
or "stop" (sender has stopped sharing directory dir with us.)
|
||||
or "accept" (sharee has accepted our offer to share our dir.)
|
||||
or "decline" (sharee has declined our offer to share our dir.)
|
||||
|
||||
Folder name is the human-readable name of the bookmark folder
|
||||
being shared (it can contain spaces). serverpath is the path
|
||||
on the server to the directory where the data is stored:
|
||||
only the machine seese this, and it can't have spaces.
|
||||
*/
|
||||
let words = messageText.split(" ");
|
||||
let commandWord = words[0];
|
||||
let serverPath = words[1];
|
||||
let directoryName = words.slice(2).join(" ");
|
||||
if ( commandWord == "share" ) {
|
||||
bmkSharing._incomingShareOffer(from, serverPath, folderName);
|
||||
} else if ( commandWord == "stop" ) {
|
||||
bmkSharing._log.info("User " + user + " withdrew " + folderName);
|
||||
bmkSharing._stopIncomingShare(user, serverPath, folderName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._xmppClient.registerMessageHandler( messageHandler );
|
||||
this._xmppClient.connect( realm, self.cb );
|
||||
yield;
|
||||
if ( this._xmppClient._connectionStatus == this._xmppClient.FAILED ) {
|
||||
this._log.warn( "Weave can't log in to xmpp server: xmpp disabled." );
|
||||
} else if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) {
|
||||
this._log.info( "Weave logged into xmpp OK." );
|
||||
}
|
||||
self.done();
|
||||
},
|
||||
|
||||
_incomingShareOffer: function BmkSharing__incomingShareOffer(user,
|
||||
serverPath,
|
||||
folderName) {
|
||||
/* Called when we receive an offer from another user to share a
|
||||
folder. Set up a notification telling our user about the share offer
|
||||
and allowing them to accept or reject it.
|
||||
*/
|
||||
this._log.info("User " + user + " offered to share folder " + folderName);
|
||||
|
||||
let bmkSharing = this;
|
||||
let acceptButton = new NotificationButton(
|
||||
"Accept Share",
|
||||
"a",
|
||||
function() {
|
||||
// This is what happens when they click the Accept button:
|
||||
bmkSharing._log.info("Accepted bookmark share from " + user);
|
||||
bmkSharing._createIncomingShare(user, serverPath, folderName);
|
||||
bmkSharing.updateAllIncomingShares();
|
||||
return false;
|
||||
}
|
||||
);
|
||||
let rejectButton = new NotificationButton(
|
||||
"No Thanks",
|
||||
"n",
|
||||
function() {return false;}
|
||||
);
|
||||
|
||||
let title = "Bookmark Share Offer From " + user;
|
||||
let description ="Weave user " + user +
|
||||
" is offering to share a bookmark folder called " + folderName +
|
||||
" with you. Do you want to accept it?";
|
||||
let notification = new Notification(title,
|
||||
description,
|
||||
null,
|
||||
Notifications.PRIORITY_INFO,
|
||||
[acceptButton, rejectButton]
|
||||
);
|
||||
Notifications.add(notification);
|
||||
},
|
||||
|
||||
_sendXmppNotification: function BmkSharing__sendXmpp(recipient, cmd, path, name) {
|
||||
// Send an xmpp message to the share-ee
|
||||
if ( this._xmppClient ) {
|
||||
if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) {
|
||||
let msgText = "share " + path + " " + name;
|
||||
this._log.debug( "Sending XMPP message: " + msgText );
|
||||
this._xmppClient.sendMessage( recipient, msgText );
|
||||
} else {
|
||||
this._log.warn( "No XMPP connection for share notification." );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_share: function BmkSharing__share( folderId, username ) {
|
||||
// Return true if success, false if failure.
|
||||
let ret = false;
|
||||
let self = yield;
|
||||
|
||||
/* TODO What should the behavior be if i'm already sharing it with user
|
||||
A and I ask to share it with user B? (This should be prevented by
|
||||
the UI. */
|
||||
let folderName = this._bms.getItemTitle(folderId);
|
||||
|
||||
// Create the outgoing share folder on the server
|
||||
this._createOutgoingShare.async( this, self.cb,
|
||||
folderId, folderName, username );
|
||||
let serverPath = yield;
|
||||
this._updateOutgoingShare.async( this, self.cb, folderId );
|
||||
yield;
|
||||
|
||||
/* Set the annotation on the folder so we know
|
||||
it's an outgoing share: */
|
||||
this._annoSvc.setItemAnnotation(folderId,
|
||||
OUTGOING_SHARED_ANNO,
|
||||
username,
|
||||
0,
|
||||
this._annoSvc.EXPIRE_NEVER);
|
||||
/* LONGTERM TODO: in the future when we allow sharing one folder
|
||||
with many people, the value of the annotation can be a whole list
|
||||
of usernames instead of just one. */
|
||||
|
||||
// The serverPath is relative; prepend it with /user/myusername/ to make
|
||||
// it absolute.
|
||||
let abspath = "/user/" + this._myUsername + "/" + serverPath;
|
||||
this._sendXmppNotification( username, "share", abspath, folderName);
|
||||
|
||||
|
||||
this._log.info("Shared " + folderName +" with " + username);
|
||||
ret = true;
|
||||
self.done( ret );
|
||||
},
|
||||
|
||||
_stopSharing: function BmkSharing__stopSharing( folderId, username ) {
|
||||
let self = yield;
|
||||
let folderName = this._bms.getItemTitle(folderId);
|
||||
let serverPath = "";
|
||||
|
||||
if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){
|
||||
serverPath = this._annoSvc.getItemAnnotation(folderId, SERVER_PATH_ANNO);
|
||||
} else {
|
||||
this._log.warn("The folder you are de-sharing has no path annotation.");
|
||||
}
|
||||
|
||||
/* LONGTERM TODO: when we move to being able to share one folder with
|
||||
* multiple people, this needs to be modified so we can stop sharing with
|
||||
* one person but keep sharing with others.
|
||||
*/
|
||||
|
||||
// Stop the outgoing share:
|
||||
this._stopOutgoingShare.async(this, self.cb, folderId);
|
||||
yield;
|
||||
|
||||
// Send message to the share-ee, so they can stop their incoming share
|
||||
let abspath = "/user/" + this._myUsername + "/" + serverPath;
|
||||
this._sendXmppNotification( username, "stop", abspath, folderName );
|
||||
|
||||
this._log.info("Stopped sharing " + folderName + "with " + username);
|
||||
self.done( true );
|
||||
},
|
||||
|
||||
/* FIXME! Gets all shares, not just the new ones. Doesn't impact
|
||||
functionality because _incomingShareOffer does not create
|
||||
duplicates, but annoys the user by showing notification of ALL
|
||||
shares on EVERY sync :(
|
||||
*/
|
||||
getNewShares: function BmkSharing_getNewShares(onComplete) {
|
||||
this._getNewShares.async(this, onComplete);
|
||||
},
|
||||
_getNewShares: function BmkSharing__getNewShares() {
|
||||
let self = yield;
|
||||
// FIXME
|
||||
// let sharingApi = new Sharing.Api( DAV );
|
||||
let result = yield sharingApi.getShares(self.cb);
|
||||
|
||||
this._log.info("Got Shares: " + result);
|
||||
let shares = result.split(',');
|
||||
if (shares.length > 1) {
|
||||
this._log.info('Found shares');
|
||||
for (var i = 0; i < shares.length - 1; i++) {
|
||||
let share = shares[i].split(':');
|
||||
let name = share[0];
|
||||
let user = share[1];
|
||||
let path = share[2];
|
||||
this._incomingShareOffer(user, '/user/' + user + '/' + path, name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateAllIncomingShares: function BmkSharing_updateAllIncoming(onComplete) {
|
||||
this._updateAllIncomingShares.async(this, onComplete);
|
||||
},
|
||||
_updateAllIncomingShares: function BmkSharing__updateAllIncoming() {
|
||||
/* For every bookmark folder in my tree that has the annotation
|
||||
marking it as an incoming shared folder, pull down its latest
|
||||
contents from its owner's account on the server. (This is
|
||||
a one-way data transfer because I can't modify bookmarks that
|
||||
are owned by someone else but shared to me; any changes I make
|
||||
to the folder contents are simply wiped out by the latest
|
||||
server contents.) */
|
||||
let self = yield;
|
||||
let mounts = this._engine._store.findIncomingShares();
|
||||
for (let i = 0; i < mounts.length; i++) {
|
||||
try {
|
||||
this._log.trace("Update incoming share from " + mounts[i].serverPath);
|
||||
this._updateIncomingShare.async(this, self.cb, mounts[i]);
|
||||
yield;
|
||||
} catch (e) {
|
||||
this._log.warn("Could not sync shared folder from " + mounts[i].userid);
|
||||
this._log.trace(Utils.stackTrace(e));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateAllOutgoingShares: function BmkSharing_updateAllOutgoing(onComplete) {
|
||||
this._updateAllOutgoingShares.async(this, onComplete);
|
||||
},
|
||||
_updateAllOutgoingShares: function BmkSharing__updateAllOutgoing() {
|
||||
let self = yield;
|
||||
let shares = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO,
|
||||
{});
|
||||
for ( let i=0; i < shares.length; i++ ) {
|
||||
/* TODO only update the shares that have changed. Perhaps we can
|
||||
do this by checking whether there's a corresponding entry in the
|
||||
diff produced by the latest sync. */
|
||||
this._updateOutgoingShare.async(this, self.cb, shares[i]);
|
||||
yield;
|
||||
}
|
||||
self.done();
|
||||
},
|
||||
|
||||
_createKeyChain: function BmkSharing__createKeychain(serverPath,
|
||||
myUserName,
|
||||
username){
|
||||
/* Creates a new keychain file on the server, inside the directory given
|
||||
* by serverPath. The keychain file contains keys for both me (the
|
||||
* current user) and for the friend specified by username (which must
|
||||
* be a valid weave user id.)
|
||||
*/
|
||||
let self = yield;
|
||||
// Create a new symmetric key, to be used only for encrypting this share.
|
||||
// XXX HACK. Seems like the engine shouldn't have to be doing any of this, or
|
||||
// should use its own identity here.
|
||||
let tmpIdentity = {
|
||||
realm : "temp ID",
|
||||
bulkKey : null,
|
||||
bulkIV : null
|
||||
};
|
||||
Crypto.randomKeyGen.async(Crypto, self.cb, tmpIdentity);
|
||||
yield;
|
||||
let bulkKey = tmpIdentity.bulkKey;
|
||||
let bulkIV = tmpIdentity.bulkIV;
|
||||
|
||||
/* Get public keys for me and the user I'm sharing with.
|
||||
Each user's public key is stored in /user/username/public/pubkey. */
|
||||
let idRSA = ID.get('WeaveCryptoID');
|
||||
let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey");
|
||||
userPubKeyFile.pushFilter( new JsonFilter() );
|
||||
// The above can get a 401 if .htaccess file isnot set to give public
|
||||
//access to the "public" directory. It can also get a 502?
|
||||
userPubKeyFile.get(self.cb);
|
||||
let userPubKey = yield;
|
||||
userPubKey = userPubKey.pubkey;
|
||||
|
||||
/* Create the keyring, containing the sym key encrypted with each
|
||||
of our public keys: */
|
||||
Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID",
|
||||
pubkey: idRSA.pubkey} );
|
||||
let encryptedForMe = yield;
|
||||
Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID",
|
||||
pubkey: userPubKey} );
|
||||
let encryptedForYou = yield;
|
||||
let keys = {
|
||||
ring : { },
|
||||
bulkIV : bulkIV
|
||||
};
|
||||
keys.ring[myUserName] = encryptedForMe;
|
||||
keys.ring[username] = encryptedForYou;
|
||||
|
||||
let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME );
|
||||
keyringFile.pushFilter(new JsonFilter());
|
||||
keyringFile.put( self.cb, keys);
|
||||
yield;
|
||||
|
||||
self.done();
|
||||
},
|
||||
|
||||
_createOutgoingShare: function BmkSharing__createOutgoing(folderId,
|
||||
folderName,
|
||||
username) {
|
||||
/* To be called asynchronously. FolderId is the integer id of the
|
||||
bookmark folder to be shared and folderName is a string of its
|
||||
title. username is a string indicating the user that it is to be
|
||||
shared with. This function creates the directory and keyring on
|
||||
the server in which the shared data will be put, but it doesn't
|
||||
actually put the bookmark data there (that's done in
|
||||
_updateOutgoingShare().)
|
||||
Returns a string which is the path on the server to the new share
|
||||
directory, or false if it failed.*/
|
||||
|
||||
let self = yield;
|
||||
this._log.debug("Turning folder " + folderName + " into outgoing share"
|
||||
+ " with " + username);
|
||||
|
||||
/* Generate a new GUID to use as the new directory name on the server
|
||||
in which we'll store the shared data. */
|
||||
let folderGuid = Utils.makeGUID();
|
||||
|
||||
/* Create the directory on the server if it does not exist already. */
|
||||
let serverPath = "share/" + folderGuid;
|
||||
// FIXME
|
||||
// let ret = yield DAV.MKCOL(serverPath, self.cb);
|
||||
|
||||
if (!ret) {
|
||||
this._log.error("Can't create remote folder for outgoing share.");
|
||||
self.done(false);
|
||||
}
|
||||
// TODO more error handling
|
||||
|
||||
/* Store the path to the server directory in an annotation on the shared
|
||||
bookmark folder, so we can easily get back to it later. */
|
||||
this._annoSvc.setItemAnnotation(folderId,
|
||||
SERVER_PATH_ANNO,
|
||||
serverPath,
|
||||
0,
|
||||
this._annoSvc.EXPIRE_NEVER);
|
||||
|
||||
let encryptionTurnedOn = true;
|
||||
if (encryptionTurnedOn) {
|
||||
yield this._createKeyChain.async(this, self.cb, serverPath,
|
||||
this._myUsername, username);
|
||||
}
|
||||
|
||||
// Call Atul's js api for setting htaccess:
|
||||
// FIXME
|
||||
// let sharingApi = new Sharing.Api( DAV );
|
||||
let result = yield sharingApi.shareWithUsers( serverPath,
|
||||
[username], folderName,
|
||||
self.cb );
|
||||
this._log.info(result.errorText);
|
||||
// return the server path:
|
||||
self.done( serverPath );
|
||||
},
|
||||
|
||||
_updateOutgoingShare: function BmkSharing__updateOutgoing(folderId) {
|
||||
/* Puts all the bookmark data from the specified bookmark folder,
|
||||
encrypted, onto the shared directory on the server (replacing
|
||||
anything that was already there).
|
||||
To be called asynchronously.
|
||||
TODO: error handling*/
|
||||
let self = yield;
|
||||
// The folder should have an annotation specifying the server path to
|
||||
// the directory:
|
||||
if (!this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)) {
|
||||
this._log.warn("Outgoing share is invalid and can't be synced.");
|
||||
return;
|
||||
}
|
||||
let serverPath = this._annoSvc.getItemAnnotation(folderId,
|
||||
SERVER_PATH_ANNO);
|
||||
// TODO the above can throw an exception if the expected anotation isn't
|
||||
// there.
|
||||
// From that directory, get the keyring file, and from it, the symmetric
|
||||
// key that we'll use to encrypt.
|
||||
let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME);
|
||||
keyringFile.pushFilter(new JsonFilter());
|
||||
keyringFile.get(self.cb);
|
||||
let keys = yield;
|
||||
|
||||
// Unwrap (decrypt) the key with the user's private key.
|
||||
let idRSA = ID.get('WeaveCryptoID');
|
||||
let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb,
|
||||
keys.ring[this._myUsername], idRSA);
|
||||
let bulkIV = keys.bulkIV;
|
||||
|
||||
// Get the json-wrapped contents of everything in the folder:
|
||||
let wrapMount = this._engine._store._wrapMountOutgoing(folderId);
|
||||
let jsonService = Components.classes["@mozilla.org/dom/json;1"]
|
||||
.createInstance(Components.interfaces.nsIJSON);
|
||||
let json = jsonService.encode( wrapMount );
|
||||
|
||||
// Encrypt it with the symkey and put it into the shared-bookmark file.
|
||||
let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME);
|
||||
let tmpIdentity = {
|
||||
realm : "temp ID",
|
||||
bulkKey : bulkKey,
|
||||
bulkIV : bulkIV
|
||||
};
|
||||
Crypto.encryptData.async( Crypto, self.cb, json, tmpIdentity );
|
||||
let cyphertext = yield;
|
||||
yield bmkFile.put( self.cb, cyphertext );
|
||||
self.done();
|
||||
},
|
||||
|
||||
_stopOutgoingShare: function BmkSharing__stopOutgoingShare(folderId) {
|
||||
/* Stops sharing the specified folder. Deletes its data from the
|
||||
server, deletes the annotations that mark it as shared, and sends
|
||||
a message to the shar-ee to let them know it's been withdrawn. */
|
||||
let self = yield;
|
||||
if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){
|
||||
let serverPath = this._annoSvc.getItemAnnotation( folderId,
|
||||
SERVER_PATH_ANNO );
|
||||
// Delete the share from the server:
|
||||
// TODO handle error that can happen if these resources do not exist.
|
||||
let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME);
|
||||
keyringFile.delete(self.cb);
|
||||
yield;
|
||||
let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME);
|
||||
keyringFile.delete(self.cb);
|
||||
yield;
|
||||
// TODO this leaves the folder itself in place... is there a way to
|
||||
// get rid of that, say through DAV?
|
||||
}
|
||||
// Remove the annotations from the local folder:
|
||||
this._annoSvc.removeItemAnnotation(folderId, SERVER_PATH_ANNO);
|
||||
this._annoSvc.removeItemAnnotation(folderId, OUTGOING_SHARED_ANNO);
|
||||
self.done();
|
||||
},
|
||||
|
||||
_createIncomingShare: function BmkSharing__createShare(user,
|
||||
serverPath,
|
||||
title) {
|
||||
/* Creates a new folder in the bookmark menu for the incoming share.
|
||||
user is the weave id of the user who is offering the folder to us;
|
||||
serverPath is the path on the server where the shared data is located,
|
||||
and title is the human-readable title to use for the new folder.
|
||||
|
||||
It is safe to call this again for a folder that already exist: this
|
||||
function will exit without doing anything. It won't create a duplicate.
|
||||
*/
|
||||
|
||||
/* Get the toolbar "Shared Folders" folder (identified by its annotation).
|
||||
If it doesn't already exist, create it: */
|
||||
dump( "I'm in _createIncomingShare. user= " + user + "path = " +
|
||||
serverPath + ", title= " + title + "\n" );
|
||||
let root;
|
||||
let a = this._annoSvc.getItemsWithAnnotation(INCOMING_SHARE_ROOT_ANNO,
|
||||
{});
|
||||
if (a.length == 1)
|
||||
root = a[0];
|
||||
if (!root) {
|
||||
root = this._bms.createFolder(this._bms.toolbarFolder,
|
||||
INCOMING_SHARE_ROOT_NAME,
|
||||
this._bms.DEFAULT_INDEX);
|
||||
this._annoSvc.setItemAnnotation(root,
|
||||
INCOMING_SHARE_ROOT_ANNO,
|
||||
true,
|
||||
0,
|
||||
this._annoSvc.EXPIRE_NEVER);
|
||||
}
|
||||
/* Inside "Shared Folders", create a new folder annotated with
|
||||
the originating user and the directory path specified by the incoming
|
||||
share offer. Unless a folder with these exact annotations already
|
||||
exists, in which case do nothing. */
|
||||
let itemExists = false;
|
||||
a = this._annoSvc.getItemsWithAnnotation(INCOMING_SHARED_ANNO, {});
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
let creator = this._annoSvc.getItemAnnotation(a[i], INCOMING_SHARED_ANNO);
|
||||
let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO);
|
||||
if ( creator == user && path == serverPath ) {
|
||||
itemExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!itemExists) {
|
||||
let newId = this._bms.createFolder(root, title, this._bms.DEFAULT_INDEX);
|
||||
// Keep track of who shared this folder with us...
|
||||
this._annoSvc.setItemAnnotation(newId,
|
||||
INCOMING_SHARED_ANNO,
|
||||
user,
|
||||
0,
|
||||
this._annoSvc.EXPIRE_NEVER);
|
||||
// and what the path to the shared data on the server is...
|
||||
this._annoSvc.setItemAnnotation(newId,
|
||||
SERVER_PATH_ANNO,
|
||||
serverPath,
|
||||
0,
|
||||
this._annoSvc.EXPIRE_NEVER);
|
||||
}
|
||||
},
|
||||
|
||||
_updateIncomingShare: function BmkSharing__updateIncomingShare(mountData) {
|
||||
/* Pull down bookmarks from the server for a single incoming
|
||||
shared folder, obliterating whatever was in that folder before.
|
||||
|
||||
mountData is an object that's expected to have member data:
|
||||
userid: weave id of the user sharing the folder with us,
|
||||
rootGUID: guid in our bookmark store of the share mount point,
|
||||
node: the bookmark menu node for the share mount point folder */
|
||||
|
||||
// TODO error handling (see what Resource can throw or return...)
|
||||
/* TODO tons of symmetry between this and _updateOutgoingShare, can
|
||||
probably factor the symkey decryption stuff into a common helper
|
||||
function. */
|
||||
let self = yield;
|
||||
let user = mountData.userid;
|
||||
// The folder has an annotation specifying the server path to the
|
||||
// directory:
|
||||
let serverPath = mountData.serverPath;
|
||||
// From that directory, get the keyring file, and from it, the symmetric
|
||||
// key that we'll use to decrypt.
|
||||
this._log.trace("UpdateIncomingShare: getting keyring file.");
|
||||
let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME);
|
||||
keyringFile.pushFilter(new JsonFilter());
|
||||
let keys = yield keyringFile.get(self.cb);
|
||||
|
||||
// Unwrap (decrypt) the key with the user's private key.
|
||||
this._log.trace("UpdateIncomingShare: decrypting sym key.");
|
||||
let idRSA = ID.get('WeaveCryptoID');
|
||||
let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb,
|
||||
keys.ring[this._myUsername], idRSA);
|
||||
let bulkIV = keys.bulkIV;
|
||||
|
||||
// Decrypt the contents of the bookmark file with the symmetric key:
|
||||
this._log.trace("UpdateIncomingShare: getting encrypted bookmark file.");
|
||||
let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME);
|
||||
let cyphertext = yield bmkFile.get(self.cb);
|
||||
let tmpIdentity = {
|
||||
realm : "temp ID",
|
||||
bulkKey : bulkKey,
|
||||
bulkIV : bulkIV
|
||||
};
|
||||
this._log.trace("UpdateIncomingShare: Decrypting.");
|
||||
Crypto.decryptData.async( Crypto, self.cb, cyphertext, tmpIdentity );
|
||||
let json = yield;
|
||||
// decrypting that gets us JSON, turn it into an object:
|
||||
this._log.trace("UpdateIncomingShare: De-JSON-izing.");
|
||||
let jsonService = Components.classes["@mozilla.org/dom/json;1"]
|
||||
.createInstance(Components.interfaces.nsIJSON);
|
||||
let serverContents = jsonService.decode( json );
|
||||
|
||||
// prune tree / get what we want
|
||||
this._log.trace("UpdateIncomingShare: Pruning.");
|
||||
for (let guid in serverContents) {
|
||||
if (serverContents[guid].type != "bookmark")
|
||||
delete serverContents[guid];
|
||||
else
|
||||
serverContents[guid].parentid = mountData.rootGUID;
|
||||
}
|
||||
|
||||
/* Wipe old local contents of the folder, starting from the node: */
|
||||
this._log.trace("Wiping local contents of incoming share...");
|
||||
this._bms.removeFolderChildren( mountData.node );
|
||||
|
||||
/* Create diff FROM current contents (i.e. nothing) TO the incoming
|
||||
* data from serverContents. Then apply the diff. */
|
||||
this._log.trace("Got bookmarks from " + user + ", comparing with local copy");
|
||||
this._engine._core.detectUpdates(self.cb, {}, serverContents);
|
||||
let diff = yield;
|
||||
|
||||
/* LONGTERM TODO: The createCommands that are executed in applyCommands
|
||||
* will fail badly if the GUID of the incoming item collides with a
|
||||
* GUID of a bookmark already in my store. (This happened to me a lot
|
||||
* during testing, obviously, since I was sharing bookmarks with myself).
|
||||
* Need to think about the right way to handle this. */
|
||||
|
||||
// FIXME: should make sure all GUIDs here live under the mountpoint
|
||||
this._log.trace("Applying changes to folder from " + user);
|
||||
this._engine._store.applyCommands.async(this._engine._store, self.cb, diff);
|
||||
yield;
|
||||
|
||||
this._log.trace("Shared folder from " + user + " successfully synced!");
|
||||
},
|
||||
|
||||
_stopIncomingShare: function BmkSharing__stopIncomingShare(user,
|
||||
serverPath,
|
||||
folderName)
|
||||
{
|
||||
/* Delete the incoming share folder. Since the update of incoming folders
|
||||
* is triggered when the engine spots a folder with a certain annotation on
|
||||
* it, just getting rid of this folder is all we need to do.
|
||||
*/
|
||||
let a = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {});
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
let creator = this._annoSvc.getItemAnnotation(a[i], OUTGOING_SHARED_ANNO);
|
||||
let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO);
|
||||
if ( creator == user && path == serverPath ) {
|
||||
this._bms.removeFolder( a[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function BookmarksEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
|
@ -729,33 +85,12 @@ BookmarksEngine.prototype = {
|
|||
return store;
|
||||
},
|
||||
|
||||
get _core() {
|
||||
let core = new BookmarksSyncCore();
|
||||
this.__defineGetter__("_core", function() core);
|
||||
return core;
|
||||
},
|
||||
|
||||
get _tracker() {
|
||||
let tracker = new BookmarksTracker();
|
||||
this.__defineGetter__("_tracker", function() tracker);
|
||||
return tracker;
|
||||
},
|
||||
|
||||
_getAllIDs: function BmkEngine__getAllIDs() {
|
||||
let self = yield;
|
||||
let all = this._store.wrap(); // FIXME: using store is an inefficient hack...
|
||||
delete all["unfiled"];
|
||||
delete all["toolbar"];
|
||||
delete all["menu"];
|
||||
self.done(all);
|
||||
},
|
||||
|
||||
_serializeItem: function BmkEngine__serializeItem(id) {
|
||||
let self = yield;
|
||||
let all = this._store.wrap(); // FIXME OMG SO INEFFICIENT
|
||||
self.done(all[id]);
|
||||
},
|
||||
|
||||
_recordLike: function SyncEngine__recordLike(a, b) {
|
||||
if (a.parentid != b.parentid)
|
||||
return false;
|
||||
|
@ -794,94 +129,6 @@ BookmarksEngine.prototype = {
|
|||
// and updating incoming/outgoing shared folders after syncing
|
||||
};
|
||||
|
||||
function BookmarksSyncCore(store) {
|
||||
this._store = store;
|
||||
this._init();
|
||||
}
|
||||
BookmarksSyncCore.prototype = {
|
||||
__proto__: SyncCore.prototype,
|
||||
_logName: "BMSync",
|
||||
_store: null,
|
||||
|
||||
_getEdits: function BSC__getEdits(a, b) {
|
||||
// NOTE: we do not increment ret.numProps, as that would cause
|
||||
// edit commands to always get generated
|
||||
let ret = SyncCore.prototype._getEdits.call(this, a, b);
|
||||
ret.props.type = a.type;
|
||||
return ret;
|
||||
},
|
||||
|
||||
// compares properties
|
||||
// returns true if the property is not set in either object
|
||||
// returns true if the property is set and equal in both objects
|
||||
// returns false otherwise
|
||||
_comp: function BSC__comp(a, b, prop) {
|
||||
return (!a.data[prop] && !b.data[prop]) ||
|
||||
(a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop]));
|
||||
},
|
||||
|
||||
_commandLike: function BSC__commandLike(a, b) {
|
||||
// Check that neither command is null, that their actions, types,
|
||||
// and parents are the same, and that they don't have the same
|
||||
// GUID.
|
||||
// * Items with the same GUID do not qualify for 'likeness' because
|
||||
// we already consider them to be the same object, and therefore
|
||||
// we need to process any edits.
|
||||
// * Remove or edit commands don't qualify for likeness either,
|
||||
// since remove or edit commands with different GUIDs are
|
||||
// guaranteed to refer to two different items
|
||||
// * The parent GUID check works because reconcile() fixes up the
|
||||
// parent GUIDs as it runs, and the command list is sorted by
|
||||
// depth
|
||||
if (!a || !b ||
|
||||
a.action != b.action ||
|
||||
a.action != "create" ||
|
||||
a.data.type != b.data.type ||
|
||||
a.data.parentid != b.data.parentid ||
|
||||
a.GUID == b.GUID)
|
||||
return false;
|
||||
|
||||
// Bookmarks and folders are allowed to be in a different index as long as
|
||||
// they are in the same folder. Separators must be at
|
||||
// the same index to qualify for 'likeness'.
|
||||
switch (a.data.type) {
|
||||
case "bookmark":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "query":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "microsummary":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'generatorURI'))
|
||||
return true;
|
||||
return false;
|
||||
case "folder":
|
||||
if (this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "livemark":
|
||||
if (this._comp(a, b, 'title') &&
|
||||
this._comp(a, b, 'siteURI') &&
|
||||
this._comp(a, b, 'feedURI'))
|
||||
return true;
|
||||
return false;
|
||||
case "separator":
|
||||
if (this._comp(a, b, 'index'))
|
||||
return true;
|
||||
return false;
|
||||
default:
|
||||
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||
this._log.error("commandLike: Unknown item type: " + json.encode(a));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function BookmarksStore() {
|
||||
this._init();
|
||||
}
|
||||
|
@ -1403,6 +650,25 @@ BookmarksStore.prototype = {
|
|||
return items;
|
||||
},
|
||||
|
||||
// FIXME: the fast path here is not so bad, specially since the engine always
|
||||
// gives the cache hint atm. but wrapping all items to return just one
|
||||
// (the slow path) is pretty bad
|
||||
wrapItem: function BStore_wrapItem(id) {
|
||||
if (this._itemCache)
|
||||
return this._itemCache[id];
|
||||
let all = this.wrap();
|
||||
return all[id];
|
||||
},
|
||||
|
||||
// XXX need a better way to query Places for all GUIDs
|
||||
getAllIDs: function BStore_getAllIDs() {
|
||||
let all = this.wrap();
|
||||
delete all["unfiled"];
|
||||
delete all["toolbar"];
|
||||
delete all["menu"];
|
||||
return all;
|
||||
},
|
||||
|
||||
wipe: function BStore_wipe() {
|
||||
this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder);
|
||||
this._bms.removeFolderChildren(this._bms.toolbarFolder);
|
||||
|
|
|
@ -59,8 +59,7 @@ function Store() {
|
|||
this._init();
|
||||
}
|
||||
Store.prototype = {
|
||||
_logName: "Store",
|
||||
_yieldDuringApply: true,
|
||||
_logName: "BaseClass",
|
||||
|
||||
// set this property in child object's wrap()!
|
||||
_lookup: null,
|
||||
|
@ -82,19 +81,21 @@ Store.prototype = {
|
|||
},
|
||||
|
||||
_init: function Store__init() {
|
||||
this._log = Log4Moz.repository.getLogger("Service." + this._logName);
|
||||
this._log = Log4Moz.repository.getLogger("Store." + this._logName);
|
||||
},
|
||||
|
||||
applyCommands: function Store_applyCommands(commandList) {
|
||||
_applyCommands: function Store__applyCommands(commandList) {
|
||||
let self = yield;
|
||||
|
||||
for (var i = 0; i < commandList.length; i++) {
|
||||
if (this._yieldDuringApply) {
|
||||
if (i % 5) {
|
||||
Utils.makeTimerForCall(self.cb);
|
||||
yield; // Yield to main loop
|
||||
}
|
||||
|
||||
var command = commandList[i];
|
||||
this._log.trace("Processing command: " + this._json.encode(command));
|
||||
if (this._log.level <= Log4Moz.Level.Trace)
|
||||
this._log.trace("Processing command: " + this._json.encode(command));
|
||||
switch (command["action"]) {
|
||||
case "create":
|
||||
this._createCommand(command);
|
||||
|
@ -110,7 +111,9 @@ Store.prototype = {
|
|||
break;
|
||||
}
|
||||
}
|
||||
self.done();
|
||||
},
|
||||
applyCommands: function Store_applyCommands(onComplete, commandList) {
|
||||
this._applyCommands.async(this, onComplete, commandList);
|
||||
},
|
||||
|
||||
// override only if neccessary
|
||||
|
@ -121,23 +124,30 @@ Store.prototype = {
|
|||
return false;
|
||||
},
|
||||
|
||||
cacheItemsHint: function Store_cacheItemsHint() {
|
||||
this._itemCache = this.wrap();
|
||||
},
|
||||
|
||||
clearItemCacheHint: function Store_clearItemCacheHint() {
|
||||
this._itemCache = null;
|
||||
},
|
||||
|
||||
// override these in derived objects
|
||||
|
||||
// wrap MUST save the wrapped store in the _lookup property!
|
||||
wrap: function Store_wrap() {
|
||||
throw "wrap needs to be subclassed";
|
||||
throw "override wrap in a subclass";
|
||||
},
|
||||
|
||||
wrapItem: function Store_wrapItem() {
|
||||
throw "override wrapItem in a subclass";
|
||||
},
|
||||
|
||||
getAllIDs: function Store_getAllIDs() {
|
||||
throw "override getAllIDs in a subclass";
|
||||
},
|
||||
|
||||
wipe: function Store_wipe() {
|
||||
throw "wipe needs to be subclassed";
|
||||
},
|
||||
|
||||
_resetGUIDs: function Store__resetGUIDs() {
|
||||
let self = yield;
|
||||
// default to do nothing
|
||||
},
|
||||
resetGUIDs: function Store_resetGUIDs(onComplete) {
|
||||
this._resetGUIDs.async(this, onComplete);
|
||||
throw "override wipe in a subclass";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче