зеркало из https://github.com/mozilla/gecko-dev.git
more and more refactoring: split weave module into multiple files (finally!)
This commit is contained in:
Родитель
391859079a
Коммит
085f383f57
|
@ -0,0 +1,63 @@
|
|||
/* ***** 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 = ['Cc', 'Ci', 'Cr', 'Cu',
|
||||
'MODE_RDONLY', 'MODE_WRONLY',
|
||||
'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE',
|
||||
'PERMS_FILE', 'PERMS_DIRECTORY',
|
||||
'STORAGE_FORMAT_VERSION',
|
||||
'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
const MODE_RDONLY = 0x01;
|
||||
const MODE_WRONLY = 0x02;
|
||||
const MODE_CREATE = 0x08;
|
||||
const MODE_APPEND = 0x10;
|
||||
const MODE_TRUNCATE = 0x20;
|
||||
|
||||
const PERMS_FILE = 0644;
|
||||
const PERMS_DIRECTORY = 0755;
|
||||
|
||||
const STORAGE_FORMAT_VERSION = 2;
|
||||
|
||||
const ONE_BYTE = 1;
|
||||
const ONE_KILOBYTE = 1024 * ONE_BYTE;
|
||||
const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/* ***** 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 = ['WeaveCrypto'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/constants.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
function WeaveCrypto() {
|
||||
this._init();
|
||||
}
|
||||
WeaveCrypto.prototype = {
|
||||
_logName: "Crypto",
|
||||
|
||||
__os: null,
|
||||
get _os() {
|
||||
if (!this.__os)
|
||||
this.__os = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
return this.__os;
|
||||
},
|
||||
|
||||
__xxxtea: {},
|
||||
__xxxteaLoaded: false,
|
||||
get _xxxtea() {
|
||||
if (!this.__xxxteaLoaded) {
|
||||
let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
|
||||
getService(Ci.mozIJSSubScriptLoader);
|
||||
jsLoader.loadSubScript("chrome://weave/content/encrypt.js", this.__xxxtea);
|
||||
this.__xxxteaLoaded = true;
|
||||
}
|
||||
return this.__xxxtea;
|
||||
},
|
||||
|
||||
get defaultAlgorithm() {
|
||||
let branch = Cc["@mozilla.org/preferences-service;1"]
|
||||
.getService(Ci.nsIPrefBranch);
|
||||
return branch.getCharPref("browser.places.sync.encryption");
|
||||
},
|
||||
set defaultAlgorithm(value) {
|
||||
let branch = Cc["@mozilla.org/preferences-service;1"]
|
||||
.getService(Ci.nsIPrefBranch);
|
||||
let cur = branch.getCharPref("browser.places.sync.encryption");
|
||||
if (value != cur)
|
||||
branch.setCharPref("browser.places.sync.encryption", value);
|
||||
},
|
||||
|
||||
_init: function Crypto__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
let branch = Cc["@mozilla.org/preferences-service;1"]
|
||||
.getService(Ci.nsIPrefBranch2);
|
||||
branch.addObserver("browser.places.sync.encryption", this, false);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]),
|
||||
|
||||
// nsIObserver
|
||||
|
||||
observe: function Sync_observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "browser.places.sync.encryption":
|
||||
let branch = Cc["@mozilla.org/preferences-service;1"]
|
||||
.getService(Ci.nsIPrefBranch);
|
||||
|
||||
let cur = branch.getCharPref("browser.places.sync.encryption");
|
||||
if (cur == data)
|
||||
return;
|
||||
|
||||
switch (data) {
|
||||
case "none":
|
||||
this._log.info("Encryption disabled");
|
||||
break;
|
||||
case "XXXTEA":
|
||||
this._log.info("Using encryption algorithm: " + data);
|
||||
break;
|
||||
default:
|
||||
this._log.warn("Unknown encryption algorithm, resetting");
|
||||
branch.setCharPref("browser.places.sync.encryption", "XXXTEA");
|
||||
return; // otherwise we'll send the alg changed event twice
|
||||
}
|
||||
// FIXME: listen to this bad boy somewhere
|
||||
this._os.notifyObservers(null, "weave:encryption:algorithm-changed", "");
|
||||
break;
|
||||
default:
|
||||
this._log.warn("Unknown encryption preference changed - ignoring");
|
||||
}
|
||||
},
|
||||
|
||||
// Crypto
|
||||
|
||||
PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) {
|
||||
let out;
|
||||
if (!algorithm)
|
||||
algorithm = this.defaultAlgorithm;
|
||||
switch (algorithm) {
|
||||
case "none":
|
||||
out = data;
|
||||
break;
|
||||
case "XXXTEA":
|
||||
try {
|
||||
this._log.debug("Encrypting data");
|
||||
out = this._xxxtea.encrypt(data, identity.password);
|
||||
this._log.debug("Done encrypting data");
|
||||
} catch (e) {
|
||||
this._log.error("Data encryption failed: " + e);
|
||||
throw 'encrypt failed';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this._log.error("Unknown encryption algorithm: " + algorithm);
|
||||
throw 'encrypt failed';
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) {
|
||||
let out;
|
||||
switch (algorithm) {
|
||||
case "none":
|
||||
out = eval(data);
|
||||
break;
|
||||
case "XXXTEA":
|
||||
try {
|
||||
this._log.debug("Decrypting data");
|
||||
out = eval(this._xxxtea.decrypt(data, identity.password));
|
||||
this._log.debug("Done decrypting data");
|
||||
} catch (e) {
|
||||
this._log.error("Data decryption failed: " + e);
|
||||
throw 'decrypt failed';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this._log.error("Unknown encryption algorithm: " + algorithm);
|
||||
throw 'decrypt failed';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,559 @@
|
|||
/* ***** 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 = ['DAVCollection'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
Function.prototype.async = generatorAsync;
|
||||
|
||||
/*
|
||||
* DAV object
|
||||
* Abstracts the raw DAV commands
|
||||
*/
|
||||
|
||||
function DAVCollection(baseURL) {
|
||||
this._baseURL = baseURL;
|
||||
this._authProvider = new DummyAuthProvider();
|
||||
this._log = Log4Moz.Service.getLogger("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;
|
||||
},
|
||||
|
||||
_auth: null,
|
||||
|
||||
get baseURL() {
|
||||
return this._baseURL;
|
||||
},
|
||||
set baseURL(value) {
|
||||
this._baseURL = value;
|
||||
},
|
||||
|
||||
_loggedIn: false,
|
||||
get loggedIn() {
|
||||
return this._loggedIn;
|
||||
},
|
||||
|
||||
_makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) {
|
||||
let [self, cont] = yield;
|
||||
let ret;
|
||||
|
||||
try {
|
||||
this._log.debug("Creating " + op + " request for " + this._baseURL + path);
|
||||
|
||||
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
|
||||
request = request.QueryInterface(Ci.nsIDOMEventTarget);
|
||||
|
||||
request.addEventListener("load", new EventListener(cont, "load"), false);
|
||||
request.addEventListener("error", new EventListener(cont, "error"), false);
|
||||
request = request.QueryInterface(Ci.nsIXMLHttpRequest);
|
||||
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) {
|
||||
this._log.debug("HTTP Header " + key + ": " + headers[key]);
|
||||
request.setRequestHeader(key, headers[key]);
|
||||
}
|
||||
|
||||
this._authProvider._authFailed = false;
|
||||
request.channel.notificationCallbacks = this._authProvider;
|
||||
|
||||
request.send(data);
|
||||
let event = yield;
|
||||
ret = event.target;
|
||||
|
||||
if (this._authProvider._authFailed)
|
||||
this._log.warn("_makeRequest: authentication failed");
|
||||
if (ret.status < 200 || ret.status >= 300)
|
||||
this._log.warn("_makeRequest: got status " + ret.status);
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
generatorDone(this, self, onComplete, ret);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
get _defaultHeaders() {
|
||||
return {'Authorization': this._auth? this._auth : '',
|
||||
'Content-type': 'text/plain',
|
||||
'If': this._token?
|
||||
"<" + this._baseURL + "> (<" + this._token + ">)" : ''};
|
||||
},
|
||||
|
||||
GET: function DC_GET(path, onComplete) {
|
||||
return this._makeRequest.async(this, onComplete, "GET", path,
|
||||
this._defaultHeaders);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
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': '<' + this._token + '>'};
|
||||
headers.__proto__ = this._defaultHeaders;
|
||||
return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers);
|
||||
},
|
||||
|
||||
// Login / Logout
|
||||
|
||||
login: function DC_login(onComplete, username, password) {
|
||||
let [self, cont] = yield;
|
||||
|
||||
try {
|
||||
if (this._loggedIn) {
|
||||
this._log.debug("Login requested, but already logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.info("Logging in");
|
||||
|
||||
let URI = makeURI(this._baseURL);
|
||||
this._auth = "Basic " + btoa(username + ":" + password);
|
||||
|
||||
// Make a call to make sure it's working
|
||||
this.GET("", cont);
|
||||
let resp = yield;
|
||||
|
||||
if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300)
|
||||
return;
|
||||
|
||||
this._loggedIn = true;
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (this._loggedIn)
|
||||
this._log.info("Logged in");
|
||||
else
|
||||
this._log.warn("Could not log in");
|
||||
generatorDone(this, self, onComplete, this._loggedIn);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
logout: function DC_logout() {
|
||||
this._log.debug("Logging out (forgetting auth header)");
|
||||
this._loggedIn = false;
|
||||
this.__auth = null;
|
||||
},
|
||||
|
||||
// Locking
|
||||
|
||||
_getActiveLock: function DC__getActiveLock(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let ret = null;
|
||||
|
||||
try {
|
||||
this._log.info("Getting active lock token");
|
||||
this.PROPFIND("",
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
|
||||
"<D:propfind xmlns:D='DAV:'>" +
|
||||
" <D:prop><D:lockdiscovery/></D:prop>" +
|
||||
"</D:propfind>", cont);
|
||||
let resp = yield;
|
||||
|
||||
if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300)
|
||||
return;
|
||||
|
||||
let tokens = xpath(resp.responseXML, '//D:locktoken/D:href');
|
||||
let token = tokens.iterateNext();
|
||||
ret = token.textContent;
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (ret)
|
||||
this._log.debug("Found an active lock token");
|
||||
else
|
||||
this._log.debug("No active lock token found");
|
||||
generatorDone(this, self, onComplete, ret);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
lock: function DC_lock(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
this._token = null;
|
||||
|
||||
try {
|
||||
this._log.info("Acquiring lock");
|
||||
|
||||
if (this._token) {
|
||||
this._log.debug("Lock called, but we already hold a token");
|
||||
return;
|
||||
}
|
||||
|
||||
this.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>", cont);
|
||||
let resp = yield;
|
||||
|
||||
if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300)
|
||||
return;
|
||||
|
||||
let tokens = xpath(resp.responseXML, '//D:locktoken/D:href');
|
||||
let token = tokens.iterateNext();
|
||||
if (token)
|
||||
this._token = token.textContent;
|
||||
|
||||
} catch (e){
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (this._token)
|
||||
this._log.info("Lock acquired");
|
||||
else
|
||||
this._log.warn("Could not acquire lock");
|
||||
generatorDone(this, self, onComplete, this._token);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
unlock: function DC_unlock(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
try {
|
||||
this._log.info("Releasing lock");
|
||||
|
||||
if (this._token === null) {
|
||||
this._log.debug("Unlock called, but we don't hold a token right now");
|
||||
return;
|
||||
}
|
||||
|
||||
this.UNLOCK("", cont);
|
||||
let resp = yield;
|
||||
|
||||
if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300)
|
||||
return;
|
||||
|
||||
this._token = null;
|
||||
|
||||
} catch (e){
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (this._token) {
|
||||
this._log.info("Could not release lock");
|
||||
generatorDone(this, self, onComplete, false);
|
||||
} else {
|
||||
this._log.info("Lock released (or we didn't have one)");
|
||||
generatorDone(this, self, onComplete, true);
|
||||
}
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
forceUnlock: function DC_forceUnlock(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let unlocked = true;
|
||||
|
||||
try {
|
||||
this._log.info("Forcibly releasing any server locks");
|
||||
|
||||
this._getActiveLock.async(this, cont);
|
||||
this._token = yield;
|
||||
|
||||
if (!this._token) {
|
||||
this._log.info("No server lock found");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.info("Server lock found, unlocking");
|
||||
this.unlock.async(this, cont);
|
||||
unlocked = yield;
|
||||
|
||||
} catch (e){
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (unlocked)
|
||||
this._log.debug("Lock released");
|
||||
else
|
||||
this._log.debug("No lock released");
|
||||
generatorDone(this, self, onComplete, unlocked);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
stealLock: function DC_stealLock(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let stolen = null;
|
||||
|
||||
try {
|
||||
this.forceUnlock.async(this, cont);
|
||||
let unlocked = yield;
|
||||
|
||||
if (unlocked) {
|
||||
this.lock.async(this, cont);
|
||||
stolen = yield;
|
||||
}
|
||||
|
||||
} catch (e){
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
generatorDone(this, self, onComplete, stolen);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Auth provider object
|
||||
* Taken from nsMicrosummaryService.js and massaged slightly
|
||||
*/
|
||||
|
||||
function DummyAuthProvider() {}
|
||||
DummyAuthProvider.prototype = {
|
||||
// Implement notification callback interfaces so we can suppress UI
|
||||
// and abort loads for bad SSL certs and HTTP authorization requests.
|
||||
|
||||
// Interfaces this component implements.
|
||||
interfaces: [Ci.nsIBadCertListener,
|
||||
Ci.nsIAuthPromptProvider,
|
||||
Ci.nsIAuthPrompt,
|
||||
Ci.nsIPrompt,
|
||||
Ci.nsIProgressEventSink,
|
||||
Ci.nsIInterfaceRequestor,
|
||||
Ci.nsISupports],
|
||||
|
||||
// Auth requests appear to succeed when we cancel them (since the server
|
||||
// redirects us to a "you're not authorized" page), so we have to set a flag
|
||||
// to let the load handler know to treat the load as a failure.
|
||||
get _authFailed() { return this.__authFailed; },
|
||||
set _authFailed(newValue) { return this.__authFailed = newValue },
|
||||
|
||||
// nsISupports
|
||||
|
||||
QueryInterface: function DAP_QueryInterface(iid) {
|
||||
if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
|
||||
// nsIAuthPrompt and nsIPrompt need separate implementations because
|
||||
// their method signatures conflict. The other interfaces we implement
|
||||
// within DummyAuthProvider itself.
|
||||
switch(iid) {
|
||||
case Ci.nsIAuthPrompt:
|
||||
return this.authPrompt;
|
||||
case Ci.nsIPrompt:
|
||||
return this.prompt;
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
},
|
||||
|
||||
// nsIInterfaceRequestor
|
||||
|
||||
getInterface: function DAP_getInterface(iid) {
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
|
||||
// nsIBadCertListener
|
||||
|
||||
// Suppress UI and abort secure loads from servers with bad SSL certificates.
|
||||
|
||||
confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) {
|
||||
return false;
|
||||
},
|
||||
|
||||
confirmMismatchDomain: function DAP_confirmMismatchDomain(socketInfo, targetURL, cert) {
|
||||
return false;
|
||||
},
|
||||
|
||||
confirmCertExpired: function DAP_confirmCertExpired(socketInfo, cert) {
|
||||
return false;
|
||||
},
|
||||
|
||||
notifyCrlNextupdate: function DAP_notifyCrlNextupdate(socketInfo, targetURL, cert) {
|
||||
},
|
||||
|
||||
// nsIAuthPromptProvider
|
||||
|
||||
getAuthPrompt: function(aPromptReason, aIID) {
|
||||
this._authFailed = true;
|
||||
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||||
},
|
||||
|
||||
// HTTP always requests nsIAuthPromptProvider first, so it never needs
|
||||
// nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we
|
||||
// implement nsIAuthPrompt too.
|
||||
|
||||
// nsIAuthPrompt
|
||||
|
||||
get authPrompt() {
|
||||
var resource = this;
|
||||
return {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
|
||||
prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) {
|
||||
resource._authFailed = true;
|
||||
return false;
|
||||
},
|
||||
promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) {
|
||||
resource._authFailed = true;
|
||||
return false;
|
||||
},
|
||||
promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) {
|
||||
resource._authFailed = true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// nsIPrompt
|
||||
|
||||
get prompt() {
|
||||
var resource = this;
|
||||
return {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
|
||||
alert: function(dialogTitle, text) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
alertCheck: function(dialogTitle, text, checkMessage, checkValue) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
confirm: function(dialogTitle, text) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
confirmCheck: function(dialogTitle, text, checkMessage, checkValue) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
prompt: function(dialogTitle, text, value, checkMsg, checkValue) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) {
|
||||
resource._authFailed = true;
|
||||
return false;
|
||||
},
|
||||
promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) {
|
||||
resource._authFailed = true;
|
||||
return false;
|
||||
},
|
||||
select: function(dialogTitle, text, count, selectList, outSelection) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// nsIProgressEventSink
|
||||
|
||||
onProgress: function DAP_onProgress(aRequest, aContext,
|
||||
aProgress, aProgressMax) {
|
||||
},
|
||||
|
||||
onStatus: function DAP_onStatus(aRequest, aContext,
|
||||
aStatus, aStatusArg) {
|
||||
}
|
||||
};
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,697 @@
|
|||
/* ***** 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 = ['BookmarksEngine'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
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/stores.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
|
||||
Function.prototype.async = generatorAsync;
|
||||
let Crypto = new WeaveCrypto();
|
||||
|
||||
function BookmarksEngine(davCollection, cryptoId) {
|
||||
this._init(davCollection, cryptoId);
|
||||
}
|
||||
BookmarksEngine.prototype = {
|
||||
_logName: "BmkEngine",
|
||||
|
||||
__os: null,
|
||||
get _os() {
|
||||
if (!this.__os)
|
||||
this.__os = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
return this.__os;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new BookmarksStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new BookmarksSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__snapshot: null,
|
||||
get _snapshot() {
|
||||
if (!this.__snapshot)
|
||||
this.__snapshot = new SnapshotStore();
|
||||
return this.__snapshot;
|
||||
},
|
||||
set _snapshot(value) {
|
||||
this.__snapshot = value;
|
||||
},
|
||||
|
||||
_init: function BmkEngine__init(davCollection, cryptoId) {
|
||||
this._dav = davCollection;
|
||||
this._cryptoId = cryptoId;
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._snapshot.load();
|
||||
},
|
||||
|
||||
_checkStatus: function BmkEngine__checkStatus(code, msg) {
|
||||
if (code >= 200 && code < 300)
|
||||
return;
|
||||
this._log.error(msg + " Error code: " + code);
|
||||
throw 'checkStatus failed';
|
||||
},
|
||||
|
||||
/* Get the deltas/combined updates from the server
|
||||
* Returns:
|
||||
* status:
|
||||
* -1: error
|
||||
* 0: ok
|
||||
* These fields may be null when status is -1:
|
||||
* formatVersion:
|
||||
* version of the data format itself. For compatibility checks.
|
||||
* maxVersion:
|
||||
* the latest version on the server
|
||||
* snapVersion:
|
||||
* the version of the current snapshot on the server (deltas not applied)
|
||||
* snapEncryption:
|
||||
* encryption algorithm currently used on the server-stored snapshot
|
||||
* deltasEncryption:
|
||||
* encryption algorithm currently used on the server-stored deltas
|
||||
* snapshot:
|
||||
* full snapshot of the latest server version (deltas applied)
|
||||
* deltas:
|
||||
* all of the individual deltas on the server
|
||||
* updates:
|
||||
* the relevant deltas (from our snapshot version to current),
|
||||
* combined into a single set.
|
||||
*/
|
||||
_getServerData: function BmkEngine__getServerData(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let ret = {status: -1,
|
||||
formatVersion: null, maxVersion: null, snapVersion: null,
|
||||
snapEncryption: null, deltasEncryption: null,
|
||||
snapshot: null, deltas: null, updates: null};
|
||||
|
||||
try {
|
||||
this._log.info("Getting bookmarks status from server");
|
||||
this._dav.GET("bookmarks-status.json", cont);
|
||||
let resp = yield;
|
||||
let status = resp.status;
|
||||
|
||||
switch (status) {
|
||||
case 200:
|
||||
this._log.info("Got bookmarks status from server");
|
||||
|
||||
let status = eval(resp.responseText);
|
||||
let deltas, allDeltas;
|
||||
let snap = new SnapshotStore();
|
||||
|
||||
// Bail out if the server has a newer format version than we can parse
|
||||
if (status.formatVersion > STORAGE_FORMAT_VERSION) {
|
||||
this._log.error("Server uses storage format v" + status.formatVersion +
|
||||
", this client understands up to v" + STORAGE_FORMAT_VERSION);
|
||||
generatorDone(this, self, onComplete, ret)
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.formatVersion == 0) {
|
||||
ret.snapEncryption = status.snapEncryption = "none";
|
||||
ret.deltasEncryption = status.deltasEncryption = "none";
|
||||
}
|
||||
|
||||
if (status.GUID != this._snapshot.GUID) {
|
||||
this._log.info("Remote/local sync GUIDs do not match. " +
|
||||
"Forcing initial sync.");
|
||||
this._store.resetGUIDs();
|
||||
this._snapshot.data = {};
|
||||
this._snapshot.version = -1;
|
||||
this._snapshot.GUID = status.GUID;
|
||||
}
|
||||
|
||||
if (this._snapshot.version < status.snapVersion) {
|
||||
if (this._snapshot.version >= 0)
|
||||
this._log.info("Local snapshot is out of date");
|
||||
|
||||
this._log.info("Downloading server snapshot");
|
||||
this._dav.GET("bookmarks-snapshot.json", cont);
|
||||
resp = yield;
|
||||
this._checkStatus(resp.status, "Could not download snapshot.");
|
||||
snap.data = Crypto.PBEdecrypt(resp.responseText,
|
||||
this._cryptoId,
|
||||
status.snapEncryption);
|
||||
|
||||
this._log.info("Downloading server deltas");
|
||||
this._dav.GET("bookmarks-deltas.json", cont);
|
||||
resp = yield;
|
||||
this._checkStatus(resp.status, "Could not download deltas.");
|
||||
allDeltas = Crypto.PBEdecrypt(resp.responseText,
|
||||
this._cryptoId,
|
||||
status.deltasEncryption);
|
||||
deltas = eval(uneval(allDeltas));
|
||||
|
||||
} else if (this._snapshot.version >= status.snapVersion &&
|
||||
this._snapshot.version < status.maxVersion) {
|
||||
snap.data = eval(uneval(this._snapshot.data));
|
||||
|
||||
this._log.info("Downloading server deltas");
|
||||
this._dav.GET("bookmarks-deltas.json", cont);
|
||||
resp = yield;
|
||||
this._checkStatus(resp.status, "Could not download deltas.");
|
||||
allDeltas = Crypto.PBEdecrypt(resp.responseText,
|
||||
this._cryptoId,
|
||||
status.deltasEncryption);
|
||||
deltas = allDeltas.slice(this._snapshot.version - status.snapVersion);
|
||||
|
||||
} else if (this._snapshot.version == status.maxVersion) {
|
||||
snap.data = eval(uneval(this._snapshot.data));
|
||||
|
||||
// FIXME: could optimize this case by caching deltas file
|
||||
this._log.info("Downloading server deltas");
|
||||
this._dav.GET("bookmarks-deltas.json", cont);
|
||||
resp = yield;
|
||||
this._checkStatus(resp.status, "Could not download deltas.");
|
||||
allDeltas = Crypto.PBEdecrypt(resp.responseText,
|
||||
this._cryptoId,
|
||||
status.deltasEncryption);
|
||||
deltas = [];
|
||||
|
||||
} else { // this._snapshot.version > status.maxVersion
|
||||
this._log.error("Server snapshot is older than local snapshot");
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < deltas.length; i++) {
|
||||
snap.applyCommands(deltas[i]);
|
||||
}
|
||||
|
||||
ret.status = 0;
|
||||
ret.formatVersion = status.formatVersion;
|
||||
ret.maxVersion = status.maxVersion;
|
||||
ret.snapVersion = status.snapVersion;
|
||||
ret.snapEncryption = status.snapEncryption;
|
||||
ret.deltasEncryption = status.deltasEncryption;
|
||||
ret.snapshot = snap.data;
|
||||
ret.deltas = allDeltas;
|
||||
this._core.detectUpdates(cont, this._snapshot.data, snap.data);
|
||||
ret.updates = yield;
|
||||
break;
|
||||
|
||||
case 404:
|
||||
this._log.info("Server has no status file, Initial upload to server");
|
||||
|
||||
this._snapshot.data = this._store.wrap();
|
||||
this._snapshot.version = 0;
|
||||
this._snapshot.GUID = null; // in case there are other snapshots out there
|
||||
|
||||
this._fullUpload.async(this, cont);
|
||||
let uploadStatus = yield;
|
||||
if (!uploadStatus)
|
||||
return;
|
||||
|
||||
this._log.info("Initial upload to server successful");
|
||||
this.snapshot.save();
|
||||
|
||||
ret.status = 0;
|
||||
ret.formatVersion = STORAGE_FORMAT_VERSION;
|
||||
ret.maxVersion = this._snapshot.version;
|
||||
ret.snapVersion = this._snapshot.version;
|
||||
ret.snapEncryption = Crypto.defaultAlgorithm;
|
||||
ret.deltasEncryption = Crypto.defaultAlgorithm;
|
||||
ret.snapshot = eval(uneval(this._snapshot.data));
|
||||
ret.deltas = [];
|
||||
ret.updates = [];
|
||||
break;
|
||||
|
||||
default:
|
||||
this._log.error("Could not get bookmarks.status: unknown HTTP status code " +
|
||||
status);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (e != 'checkStatus failed' &&
|
||||
e != 'decrypt failed')
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
generatorDone(this, self, onComplete, ret)
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
_fullUpload: function BmkEngine__fullUpload(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let ret = false;
|
||||
|
||||
try {
|
||||
let data = Crypto.PBEencrypt(this._snapshot.serialize(),
|
||||
this._cryptoId);
|
||||
this._dav.PUT("bookmarks-snapshot.json", data, cont);
|
||||
resp = yield;
|
||||
this._checkStatus(resp.status, "Could not upload snapshot.");
|
||||
|
||||
this._dav.PUT("bookmarks-deltas.json", uneval([]), cont);
|
||||
resp = yield;
|
||||
this._checkStatus(resp.status, "Could not upload deltas.");
|
||||
|
||||
let c = 0;
|
||||
for (GUID in this._snapshot.data)
|
||||
c++;
|
||||
|
||||
this._dav.PUT("bookmarks-status.json",
|
||||
uneval({GUID: this._snapshot.GUID,
|
||||
formatVersion: STORAGE_FORMAT_VERSION,
|
||||
snapVersion: this._snapshot.version,
|
||||
maxVersion: this._snapshot.version,
|
||||
snapEncryption: Crypto.defaultAlgorithm,
|
||||
deltasEncryption: "none",
|
||||
bookmarksCount: c}), cont);
|
||||
resp = yield;
|
||||
this._checkStatus(resp.status, "Could not upload status file.");
|
||||
|
||||
this._log.info("Full upload to server successful");
|
||||
ret = true;
|
||||
|
||||
} catch (e) {
|
||||
if (e != 'checkStatus failed')
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
generatorDone(this, self, onComplete, ret)
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
// original
|
||||
// / \
|
||||
// A / \ B
|
||||
// / \
|
||||
// client --C-> server
|
||||
// \ /
|
||||
// D \ / C
|
||||
// \ /
|
||||
// final
|
||||
|
||||
// If we have a saved snapshot, original == snapshot. Otherwise,
|
||||
// it's the empty set {}.
|
||||
|
||||
// C is really the diff between server -> final, so if we determine
|
||||
// D we can calculate C from that. In the case where A and B have
|
||||
// no conflicts, C == A and D == B.
|
||||
|
||||
// Sync flow:
|
||||
// 1) Fetch server deltas
|
||||
// 1.1) Construct current server status from snapshot + server deltas
|
||||
// 1.2) Generate single delta from snapshot -> current server status ("B")
|
||||
// 2) Generate local deltas from snapshot -> current client status ("A")
|
||||
// 3) Reconcile client/server deltas and generate new deltas for them.
|
||||
// Reconciliation won't generate C directly, we will simply diff
|
||||
// server->final after step 3.1.
|
||||
// 3.1) Apply local delta with server changes ("D")
|
||||
// 3.2) Append server delta to the delta file and upload ("C")
|
||||
|
||||
_sync: function BmkEngine__sync(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let synced = false, locked = null;
|
||||
|
||||
try {
|
||||
this._log.info("Beginning sync");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:sync-start", "");
|
||||
|
||||
this._dav.lock.async(this._dav, cont);
|
||||
locked = yield;
|
||||
|
||||
if (locked)
|
||||
this._log.info("Lock acquired");
|
||||
else {
|
||||
this._log.warn("Could not acquire lock, aborting sync");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Fetch server deltas
|
||||
this._getServerData.async(this, cont);
|
||||
let server = yield;
|
||||
|
||||
this._log.info("Local snapshot version: " + this._snapshot.version);
|
||||
this._log.info("Server status: " + server.status);
|
||||
this._log.info("Server maxVersion: " + server.maxVersion);
|
||||
this._log.info("Server snapVersion: " + server.snapVersion);
|
||||
|
||||
if (server.status != 0) {
|
||||
this._log.fatal("Sync error: could not get server status, " +
|
||||
"or initial upload failed. Aborting sync.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Generate local deltas from snapshot -> current client status
|
||||
|
||||
let localJson = new SnapshotStore();
|
||||
localJson.data = this._store.wrap();
|
||||
this._core.detectUpdates(cont, this._snapshot.data, localJson.data);
|
||||
let localUpdates = yield;
|
||||
|
||||
this._log.debug("local json:\n" + localJson.serialize());
|
||||
this._log.debug("Local updates: " + serializeCommands(localUpdates));
|
||||
this._log.debug("Server updates: " + serializeCommands(server.updates));
|
||||
|
||||
if (server.updates.length == 0 && localUpdates.length == 0) {
|
||||
this._snapshot.version = server.maxVersion;
|
||||
this._log.info("Sync complete (1): no changes needed on client or server");
|
||||
synced = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Reconcile client/server deltas and generate new deltas for them.
|
||||
|
||||
this._log.info("Reconciling client/server updates");
|
||||
this._core.reconcile(cont, localUpdates, server.updates);
|
||||
let ret = yield;
|
||||
|
||||
let clientChanges = ret.propagations[0];
|
||||
let serverChanges = ret.propagations[1];
|
||||
let clientConflicts = ret.conflicts[0];
|
||||
let serverConflicts = ret.conflicts[1];
|
||||
|
||||
this._log.info("Changes for client: " + clientChanges.length);
|
||||
this._log.info("Predicted changes for server: " + serverChanges.length);
|
||||
this._log.info("Client conflicts: " + clientConflicts.length);
|
||||
this._log.info("Server conflicts: " + serverConflicts.length);
|
||||
this._log.debug("Changes for client: " + serializeCommands(clientChanges));
|
||||
this._log.debug("Predicted changes for server: " + serializeCommands(serverChanges));
|
||||
this._log.debug("Client conflicts: " + serializeConflicts(clientConflicts));
|
||||
this._log.debug("Server conflicts: " + serializeConflicts(serverConflicts));
|
||||
|
||||
if (!(clientChanges.length || serverChanges.length ||
|
||||
clientConflicts.length || serverConflicts.length)) {
|
||||
this._log.info("Sync complete (2): no changes needed on client or server");
|
||||
this._snapshot.data = localJson.data;
|
||||
this._snapshot.version = server.maxVersion;
|
||||
this._snapshot.save();
|
||||
synced = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientConflicts.length || serverConflicts.length) {
|
||||
this._log.warn("Conflicts found! Discarding server changes");
|
||||
}
|
||||
|
||||
let savedSnap = eval(uneval(this._snapshot.data));
|
||||
let savedVersion = this._snapshot.version;
|
||||
let newSnapshot;
|
||||
|
||||
// 3.1) Apply server changes to local store
|
||||
if (clientChanges.length) {
|
||||
this._log.info("Applying changes locally");
|
||||
// Note that we need to need to apply client changes to the
|
||||
// current tree, not the saved snapshot
|
||||
|
||||
localJson.applyCommands(clientChanges);
|
||||
this._snapshot.data = localJson.data;
|
||||
this._snapshot.version = server.maxVersion;
|
||||
this._store.applyCommands(clientChanges);
|
||||
newSnapshot = this._store.wrap();
|
||||
|
||||
this._core.detectUpdates(cont, this._snapshot.data, newSnapshot);
|
||||
let diff = yield;
|
||||
if (diff.length != 0) {
|
||||
this._log.warn("Commands did not apply correctly");
|
||||
this._log.debug("Diff from snapshot+commands -> " +
|
||||
"new snapshot after commands:\n" +
|
||||
serializeCommands(diff));
|
||||
// FIXME: do we really want to revert the snapshot here?
|
||||
this._snapshot.data = eval(uneval(savedSnap));
|
||||
this._snapshot.version = savedVersion;
|
||||
}
|
||||
|
||||
this._snapshot.save();
|
||||
}
|
||||
|
||||
// 3.2) Append server delta to the delta file and upload
|
||||
|
||||
// Generate a new diff, from the current server snapshot to the
|
||||
// current client snapshot. In the case where there are no
|
||||
// conflicts, it should be the same as what the resolver returned
|
||||
|
||||
newSnapshot = this._store.wrap();
|
||||
this._core.detectUpdates(cont, server.snapshot, newSnapshot);
|
||||
let serverDelta = yield;
|
||||
|
||||
// Log an error if not the same
|
||||
if (!(serverConflicts.length ||
|
||||
deepEquals(serverChanges, serverDelta)))
|
||||
this._log.warn("Predicted server changes differ from " +
|
||||
"actual server->client diff (can be ignored in many cases)");
|
||||
|
||||
this._log.info("Actual changes for server: " + serverDelta.length);
|
||||
this._log.debug("Actual changes for server: " +
|
||||
serializeCommands(serverDelta));
|
||||
|
||||
if (serverDelta.length) {
|
||||
this._log.info("Uploading changes to server");
|
||||
|
||||
this._snapshot.data = newSnapshot;
|
||||
this._snapshot.version = ++server.maxVersion;
|
||||
|
||||
server.deltas.push(serverDelta);
|
||||
|
||||
if (server.formatVersion != STORAGE_FORMAT_VERSION ||
|
||||
this._encryptionChanged) {
|
||||
this._fullUpload.async(this, cont);
|
||||
let status = yield;
|
||||
if (!status)
|
||||
this._log.error("Could not upload files to server"); // eep?
|
||||
|
||||
} else {
|
||||
let data = Crypto.PBEencrypt(serializeCommands(server.deltas),
|
||||
this._cryptoId);
|
||||
this._dav.PUT("bookmarks-deltas.json", data, cont);
|
||||
let deltasPut = yield;
|
||||
|
||||
let c = 0;
|
||||
for (GUID in this._snapshot.data)
|
||||
c++;
|
||||
|
||||
this._dav.PUT("bookmarks-status.json",
|
||||
uneval({GUID: this._snapshot.GUID,
|
||||
formatVersion: STORAGE_FORMAT_VERSION,
|
||||
snapVersion: server.snapVersion,
|
||||
maxVersion: this._snapshot.version,
|
||||
snapEncryption: server.snapEncryption,
|
||||
deltasEncryption: Crypto.defaultAlgorithm,
|
||||
Bookmarkscount: c}), cont);
|
||||
let statusPut = yield;
|
||||
|
||||
if (deltasPut.status >= 200 && deltasPut.status < 300 &&
|
||||
statusPut.status >= 200 && statusPut.status < 300) {
|
||||
this._log.info("Successfully updated deltas and status on server");
|
||||
this._snapshot.save();
|
||||
} else {
|
||||
// FIXME: revert snapshot here? - can't, we already applied
|
||||
// updates locally! - need to save and retry
|
||||
this._log.error("Could not update deltas on server");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._log.info("Sync complete");
|
||||
synced = true;
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
let ok = false;
|
||||
if (locked) {
|
||||
this._dav.unlock.async(this._dav, cont);
|
||||
ok = yield;
|
||||
}
|
||||
if (ok && synced) {
|
||||
this._os.notifyObservers(null, "bookmarks-sync:sync-end", "");
|
||||
generatorDone(this, self, onComplete, true);
|
||||
} else {
|
||||
this._os.notifyObservers(null, "bookmarks-sync:sync-error", "");
|
||||
generatorDone(this, self, onComplete, false);
|
||||
}
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
_resetServer: function BmkEngine__resetServer(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let done = false;
|
||||
|
||||
try {
|
||||
this._log.debug("Resetting server data");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", "");
|
||||
|
||||
this._dav.lock.async(this._dav, cont);
|
||||
let locked = yield;
|
||||
if (locked)
|
||||
this._log.debug("Lock acquired");
|
||||
else {
|
||||
this._log.warn("Could not acquire lock, aborting server reset");
|
||||
return;
|
||||
}
|
||||
|
||||
this._dav.DELETE("bookmarks-status.json", cont);
|
||||
let statusResp = yield;
|
||||
this._dav.DELETE("bookmarks-snapshot.json", cont);
|
||||
let snapshotResp = yield;
|
||||
this._dav.DELETE("bookmarks-deltas.json", cont);
|
||||
let deltasResp = yield;
|
||||
|
||||
this._dav.unlock.async(this._dav, cont);
|
||||
let unlocked = yield;
|
||||
|
||||
function ok(code) {
|
||||
if (code >= 200 && code < 300)
|
||||
return true;
|
||||
if (code == 404)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(ok(statusResp.status) && ok(snapshotResp.status) &&
|
||||
ok(deltasResp.status))) {
|
||||
this._log.error("Could delete server data, response codes " +
|
||||
statusResp.status + ", " + snapshotResp.status + ", " +
|
||||
deltasResp.status);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.debug("Server files deleted");
|
||||
done = true;
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (done) {
|
||||
this._log.debug("Server reset completed successfully");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", "");
|
||||
} else {
|
||||
this._log.debug("Server reset failed");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", "");
|
||||
}
|
||||
generatorDone(this, self, onComplete, done)
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
_resetClient: function BmkEngine__resetClient(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let done = false;
|
||||
|
||||
try {
|
||||
this._log.debug("Resetting client state");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", "");
|
||||
|
||||
this._snapshot.data = {};
|
||||
this._snapshot.version = -1;
|
||||
this.snapshot.save();
|
||||
done = true;
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (done) {
|
||||
this._log.debug("Client reset completed successfully");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:reset-client-end", "");
|
||||
} else {
|
||||
this._log.debug("Client reset failed");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:reset-client-error", "");
|
||||
}
|
||||
generatorDone(this, self, onComplete, done);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
sync: function BmkEngine_sync(onComplete) {
|
||||
return this._sync.async(this, onComplete);
|
||||
},
|
||||
|
||||
resetServer: function BmkEngine_resetServer(onComplete) {
|
||||
return this._resetServer.async(this, onComplete);
|
||||
},
|
||||
|
||||
resetClient: function BmkEngine_resetClient(onComplete) {
|
||||
return this._resetClient.async(this, onComplete);
|
||||
}
|
||||
};
|
||||
|
||||
serializeCommands: function serializeCommands(commands) {
|
||||
let json = uneval(commands);
|
||||
json = json.replace(/ {action/g, "\n {action");
|
||||
return json;
|
||||
}
|
||||
|
||||
serializeConflicts: function serializeConflicts(conflicts) {
|
||||
let json = uneval(conflicts);
|
||||
json = json.replace(/ {action/g, "\n {action");
|
||||
return json;
|
||||
}
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/* ***** 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 = ['Identity'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/constants.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
|
||||
/*
|
||||
* Identity
|
||||
* These objects hold a realm, username, and password
|
||||
* They can hold a password in memory, but will try to fetch it from
|
||||
* the password manager if it's not set.
|
||||
* FIXME: need to rethink this stuff as part of a bigger identity mgmt framework
|
||||
*/
|
||||
|
||||
function Identity(realm, username, password) {
|
||||
this._realm = realm;
|
||||
this._username = username;
|
||||
this._password = password;
|
||||
}
|
||||
Identity.prototype = {
|
||||
get realm() { return this._realm; },
|
||||
set realm(value) { this._realm = value; },
|
||||
|
||||
get username() { return this._username; },
|
||||
set username(value) { this._username = value; },
|
||||
|
||||
_password: null,
|
||||
get password() {
|
||||
if (this._password === null)
|
||||
return findPassword(this.realm, this.username);
|
||||
return this._password;
|
||||
},
|
||||
set password(value) {
|
||||
setPassword(this.realm, this.username, value);
|
||||
},
|
||||
|
||||
setTempPassword: function Id_setTempPassword(value) {
|
||||
this._password = value;
|
||||
}
|
||||
};
|
||||
|
||||
// fixme: move these to util.js?
|
||||
function findPassword(realm, username) {
|
||||
// fixme: make a request and get the realm ?
|
||||
let password;
|
||||
let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
|
||||
let logins = lm.findLogins({}, 'chrome://sync', null, realm);
|
||||
|
||||
for (let i = 0; i < logins.length; i++) {
|
||||
if (logins[i].username == username) {
|
||||
password = logins[i].password;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
function setPassword(realm, username, password) {
|
||||
// cleanup any existing passwords
|
||||
let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
|
||||
let logins = lm.findLogins({}, 'chrome://sync', null, realm);
|
||||
for(let i = 0; i < logins.length; i++) {
|
||||
lm.removeLogin(logins[i]);
|
||||
}
|
||||
|
||||
if (!password)
|
||||
return;
|
||||
|
||||
// save the new one
|
||||
let nsLoginInfo = new Components.Constructor(
|
||||
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
|
||||
let login = new nsLoginInfo('chrome://sync', null, realm,
|
||||
username, password, null, null);
|
||||
lm.addLogin(login);
|
||||
}
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,448 @@
|
|||
/* ***** 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 = ['WeaveSyncService'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
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/engines.js");
|
||||
Cu.import("resource://weave/dav.js");
|
||||
Cu.import("resource://weave/identity.js");
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
Function.prototype.async = generatorAsync;
|
||||
|
||||
/*
|
||||
* Service singleton
|
||||
* Main entry point into Weave's sync framework
|
||||
*/
|
||||
|
||||
function WeaveSyncService() { this._init(); }
|
||||
WeaveSyncService.prototype = {
|
||||
|
||||
__os: null,
|
||||
get _os() {
|
||||
if (!this.__os)
|
||||
this.__os = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
return this.__os;
|
||||
},
|
||||
|
||||
__dirSvc: null,
|
||||
get _dirSvc() {
|
||||
if (!this.__dirSvc)
|
||||
this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties);
|
||||
return this.__dirSvc;
|
||||
},
|
||||
|
||||
__dav: null,
|
||||
get _dav() {
|
||||
if (!this.__dav)
|
||||
this.__dav = new DAVCollection();
|
||||
return this.__dav;
|
||||
},
|
||||
|
||||
__bmkEngine: null,
|
||||
get _bmkEngine() {
|
||||
if (!this.__bmkEngine)
|
||||
this.__bmkEngine = new BookmarksEngine(this._dav, this._cryptoId);
|
||||
return this.__bmkEngine;
|
||||
},
|
||||
|
||||
// Logger object
|
||||
_log: null,
|
||||
|
||||
// Timer object for automagically syncing
|
||||
_scheduleTimer: null,
|
||||
|
||||
__mozId: null,
|
||||
get _mozId() {
|
||||
if (this.__mozId === null)
|
||||
this.__mozId = new Identity('Mozilla Services Password', this.username);
|
||||
return this.__mozId;
|
||||
},
|
||||
|
||||
__cryptoId: null,
|
||||
get _cryptoId() {
|
||||
if (this.__cryptoId === null)
|
||||
this.__cryptoId = new Identity('Mozilla Services Encryption Passphrase',
|
||||
this.username);
|
||||
return this.__cryptoId;
|
||||
},
|
||||
|
||||
get username() {
|
||||
let branch = Cc["@mozilla.org/preferences-service;1"]
|
||||
.getService(Ci.nsIPrefBranch);
|
||||
return branch.getCharPref("browser.places.sync.username");
|
||||
},
|
||||
set username(value) {
|
||||
let branch = Cc["@mozilla.org/preferences-service;1"]
|
||||
.getService(Ci.nsIPrefBranch);
|
||||
branch.setCharPref("browser.places.sync.username", value);
|
||||
// fixme - need to loop over all Identity objects - needs some rethinking...
|
||||
this._mozId.username = value;
|
||||
this._cryptoId.username = value;
|
||||
},
|
||||
|
||||
get password() { return this._mozId.password; },
|
||||
set password(value) { this._mozId.password = value; },
|
||||
|
||||
get passphrase() { return this._cryptoId.password; },
|
||||
set passphrase(value) { this._cryptoId.password = value; },
|
||||
|
||||
get userPath() {
|
||||
this._log.info("Hashing username " + this.username);
|
||||
|
||||
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
|
||||
createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hasher.SHA1);
|
||||
|
||||
let data = converter.convertToByteArray(this.username, {});
|
||||
hasher.update(data, data.length);
|
||||
let rawHash = hasher.finish(false);
|
||||
|
||||
// return the two-digit hexadecimal code for a byte
|
||||
function toHexString(charCode) {
|
||||
return ("0" + charCode.toString(16)).slice(-2);
|
||||
}
|
||||
|
||||
let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join("");
|
||||
this._log.debug("Username hashes to " + hash);
|
||||
return hash;
|
||||
},
|
||||
|
||||
get currentUser() {
|
||||
if (this._dav.loggedIn)
|
||||
return this.username;
|
||||
return null;
|
||||
},
|
||||
|
||||
_init: function BSS__init() {
|
||||
this._initLogs();
|
||||
this._log.info("Weave Sync Service Initializing");
|
||||
|
||||
this._serverURL = 'https://services.mozilla.com/';
|
||||
this._user = '';
|
||||
let enabled = false;
|
||||
let schedule = 0;
|
||||
try {
|
||||
let branch = Cc["@mozilla.org/preferences-service;1"].
|
||||
getService(Ci.nsIPrefBranch2);
|
||||
this._serverURL = branch.getCharPref("browser.places.sync.serverURL");
|
||||
enabled = branch.getBoolPref("browser.places.sync.enabled");
|
||||
schedule = branch.getIntPref("browser.places.sync.schedule");
|
||||
|
||||
branch.addObserver("browser.places.sync", this, false);
|
||||
}
|
||||
catch (ex) { /* use defaults */ }
|
||||
|
||||
if (!enabled) {
|
||||
this._log.info("Bookmarks sync disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (schedule) {
|
||||
case 0:
|
||||
this._log.info("Bookmarks sync enabled, manual mode");
|
||||
break;
|
||||
case 1:
|
||||
this._log.info("Bookmarks sync enabled, automagic mode");
|
||||
this._enableSchedule();
|
||||
break;
|
||||
default:
|
||||
this._log.info("Bookmarks sync enabled");
|
||||
this._log.info("Invalid schedule setting: " + schedule);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_enableSchedule: function BSS__enableSchedule() {
|
||||
this._scheduleTimer = Cc["@mozilla.org/timer;1"].
|
||||
createInstance(Ci.nsITimer);
|
||||
let listener = new EventListener(bind2(this, this._onSchedule));
|
||||
this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min
|
||||
this._scheduleTimer.TYPE_REPEATING_SLACK);
|
||||
},
|
||||
|
||||
_disableSchedule: function BSS__disableSchedule() {
|
||||
this._scheduleTimer = null;
|
||||
},
|
||||
|
||||
_onSchedule: function BSS__onSchedule() {
|
||||
this._log.info("Running scheduled sync");
|
||||
this.sync();
|
||||
},
|
||||
|
||||
_initLogs: function BSS__initLogs() {
|
||||
this._log = Log4Moz.Service.getLogger("Service.Main");
|
||||
|
||||
let formatter = Log4Moz.Service.newFormatter("basic");
|
||||
let root = Log4Moz.Service.rootLogger;
|
||||
root.level = Log4Moz.Level.Debug;
|
||||
|
||||
let capp = Log4Moz.Service.newAppender("console", formatter);
|
||||
capp.level = Log4Moz.Level.Warn;
|
||||
root.addAppender(capp);
|
||||
|
||||
let dapp = Log4Moz.Service.newAppender("dump", formatter);
|
||||
dapp.level = Log4Moz.Level.All;
|
||||
root.addAppender(dapp);
|
||||
|
||||
let logFile = this._dirSvc.get("ProfD", Ci.nsIFile);
|
||||
let verboseFile = logFile.clone();
|
||||
logFile.append("bm-sync.log");
|
||||
logFile.QueryInterface(Ci.nsILocalFile);
|
||||
verboseFile.append("bm-sync-verbose.log");
|
||||
verboseFile.QueryInterface(Ci.nsILocalFile);
|
||||
|
||||
let fapp = Log4Moz.Service.newFileAppender("rotating", logFile, formatter);
|
||||
fapp.level = Log4Moz.Level.Info;
|
||||
root.addAppender(fapp);
|
||||
let vapp = Log4Moz.Service.newFileAppender("rotating", verboseFile, formatter);
|
||||
vapp.level = Log4Moz.Level.Debug;
|
||||
root.addAppender(vapp);
|
||||
},
|
||||
|
||||
_lock: function BSS__lock() {
|
||||
if (this._locked) {
|
||||
this._log.warn("Service lock failed: already locked");
|
||||
return false;
|
||||
}
|
||||
this._locked = true;
|
||||
this._log.debug("Service lock acquired");
|
||||
return true;
|
||||
},
|
||||
|
||||
_unlock: function BSS__unlock() {
|
||||
this._locked = false;
|
||||
this._log.debug("Service lock released");
|
||||
},
|
||||
|
||||
// IBookmarksSyncService internal implementation
|
||||
|
||||
_login: function BSS__login(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
this._log.debug("Logging in");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:login-start", "");
|
||||
|
||||
if (!this.username) {
|
||||
this._log.warn("No username set, login failed");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:login-error", "");
|
||||
return;
|
||||
}
|
||||
if (!this.password) {
|
||||
this._log.warn("No password given or found in password manager");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:login-error", "");
|
||||
return;
|
||||
}
|
||||
|
||||
this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/";
|
||||
this._log.info("Using server URL: " + this._dav.baseURL);
|
||||
|
||||
this._dav.login.async(this._dav, cont, this.username, this.password);
|
||||
success = yield;
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
this._passphrase = null;
|
||||
if (success) {
|
||||
this._log.debug("Login successful");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:login-end", "");
|
||||
} else {
|
||||
this._log.debug("Login error");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:login-error", "");
|
||||
}
|
||||
generatorDone(this, self, onComplete, success);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
_resetLock: function BSS__resetLock(onComplete) {
|
||||
let [self, cont] = yield;
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
this._log.debug("Resetting server lock");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:lock-reset-start", "");
|
||||
|
||||
this._dav.forceUnlock.async(this._dav, cont);
|
||||
success = yield;
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
if (success) {
|
||||
this._log.debug("Server lock reset successful");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:lock-reset-end", "");
|
||||
} else {
|
||||
this._log.debug("Server lock reset failed");
|
||||
this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", "");
|
||||
}
|
||||
generatorDone(this, self, onComplete, success);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]),
|
||||
|
||||
// nsIObserver
|
||||
|
||||
observe: function BSS__observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "browser.places.sync.enabled":
|
||||
switch (data) {
|
||||
case false:
|
||||
this._log.info("Disabling automagic bookmarks sync");
|
||||
this._disableSchedule();
|
||||
break;
|
||||
case true:
|
||||
this._log.info("Enabling automagic bookmarks sync");
|
||||
this._enableSchedule();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "browser.places.sync.schedule":
|
||||
switch (data) {
|
||||
case 0:
|
||||
this._log.info("Disabling automagic bookmarks sync");
|
||||
this._disableSchedule();
|
||||
break;
|
||||
case 1:
|
||||
this._log.info("Enabling automagic bookmarks sync");
|
||||
this._enableSchedule();
|
||||
break;
|
||||
default:
|
||||
this._log.warn("Unknown schedule value set");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// ignore, there are prefs we observe but don't care about
|
||||
}
|
||||
},
|
||||
|
||||
// IBookmarksSyncService public methods
|
||||
|
||||
// These are global (for all engines)
|
||||
|
||||
login: function BSS_login(password, passphrase) {
|
||||
if (!this._lock())
|
||||
return;
|
||||
// cache password & passphrase
|
||||
// if null, _login() will try to get them from the pw manager
|
||||
this._mozId.setTempPassword(password);
|
||||
this._cryptoId.setTempPassword(passphrase);
|
||||
let self = this;
|
||||
this._login.async(this, function() {self._unlock()});
|
||||
},
|
||||
|
||||
logout: function BSS_logout() {
|
||||
this._log.info("Logging out");
|
||||
this._dav.logout();
|
||||
this._mozId.setTempPassword(null); // clear cached password
|
||||
this._cryptoId.setTempPassword(null); // and passphrase
|
||||
this._os.notifyObservers(null, "bookmarks-sync:logout", "");
|
||||
},
|
||||
|
||||
resetLock: function BSS_resetLock() {
|
||||
if (!this._lock())
|
||||
return;
|
||||
let self = this;
|
||||
this._resetLock.async(this, function() {self._unlock()});
|
||||
},
|
||||
|
||||
// These are per-engine
|
||||
|
||||
sync: function BSS_sync() {
|
||||
if (!this._lock())
|
||||
return;
|
||||
let self = this;
|
||||
this._bmkEngine.sync(function() {self._unlock()});
|
||||
},
|
||||
|
||||
resetServer: function BSS_resetServer() {
|
||||
if (!this._lock())
|
||||
return;
|
||||
let self = this;
|
||||
this._bmkEngine.resetServer(function() {self._unlock()});
|
||||
},
|
||||
|
||||
resetClient: function BSS_resetClient() {
|
||||
if (!this._lock())
|
||||
return;
|
||||
let self = this;
|
||||
this._bmkEngine.resetClient(function() {self._unlock()});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,556 @@
|
|||
/* ***** 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 = ['Store', 'SnapshotStore', 'BookmarksStore'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/constants.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
/*
|
||||
* Data Stores
|
||||
* These can wrap, serialize items and apply commands
|
||||
*/
|
||||
|
||||
function Store() {
|
||||
this._init();
|
||||
}
|
||||
Store.prototype = {
|
||||
_logName: "Store",
|
||||
|
||||
_init: function Store__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
},
|
||||
|
||||
wrap: function Store_wrap() {
|
||||
},
|
||||
|
||||
applyCommands: function Store_applyCommands(commandList) {
|
||||
}
|
||||
};
|
||||
|
||||
function SnapshotStore() {
|
||||
this._init();
|
||||
}
|
||||
SnapshotStore.prototype = {
|
||||
_logName: "SStore",
|
||||
|
||||
__dirSvc: null,
|
||||
get _dirSvc() {
|
||||
if (!this.__dirSvc)
|
||||
this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties);
|
||||
return this.__dirSvc;
|
||||
},
|
||||
|
||||
// Last synced tree, version, and GUID (to detect if the store has
|
||||
// been completely replaced and invalidate the snapshot)
|
||||
|
||||
_data: {},
|
||||
get data() { return this._data; },
|
||||
set data(value) { this._data = value; },
|
||||
|
||||
_version: 0,
|
||||
get version() { return this._version; },
|
||||
set version(value) { this._version = value; },
|
||||
|
||||
_GUID: null,
|
||||
get GUID() {
|
||||
if (!this._GUID) {
|
||||
let uuidgen = Cc["@mozilla.org/uuid-generator;1"].
|
||||
getService(Ci.nsIUUIDGenerator);
|
||||
this._GUID = uuidgen.generateUUID().toString().replace(/[{}]/g, '');
|
||||
}
|
||||
return this._GUID;
|
||||
},
|
||||
set GUID(GUID) {
|
||||
this._GUID = GUID;
|
||||
},
|
||||
|
||||
save: function SStore_save() {
|
||||
this._log.info("Saving snapshot to disk");
|
||||
|
||||
let file = this._dirSvc.get("ProfD", Ci.nsIFile);
|
||||
file.append("bm-sync-snapshot.json");
|
||||
file.QueryInterface(Ci.nsILocalFile);
|
||||
|
||||
if (!file.exists())
|
||||
file.create(file.NORMAL_FILE_TYPE, PERMS_FILE);
|
||||
|
||||
let fos = Cc["@mozilla.org/network/file-output-stream;1"].
|
||||
createInstance(Ci.nsIFileOutputStream);
|
||||
let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE;
|
||||
fos.init(file, flags, PERMS_FILE, 0);
|
||||
|
||||
let out = {version: this.version,
|
||||
GUID: this.GUID,
|
||||
snapshot: this.data};
|
||||
out = uneval(out);
|
||||
fos.write(out, out.length);
|
||||
fos.close();
|
||||
},
|
||||
|
||||
load: function SStore_load() {
|
||||
let file = this._dirSvc.get("ProfD", Ci.nsIFile);
|
||||
file.append("bm-sync-snapshot.json");
|
||||
|
||||
if (!file.exists())
|
||||
return;
|
||||
|
||||
let fis = Cc["@mozilla.org/network/file-input-stream;1"].
|
||||
createInstance(Ci.nsIFileInputStream);
|
||||
fis.init(file, MODE_RDONLY, PERMS_FILE, 0);
|
||||
fis.QueryInterface(Ci.nsILineInputStream);
|
||||
|
||||
let json = "";
|
||||
while (fis.available()) {
|
||||
let ret = {};
|
||||
fis.readLine(ret);
|
||||
json += ret.value;
|
||||
}
|
||||
fis.close();
|
||||
json = eval(json);
|
||||
|
||||
if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) {
|
||||
this._log.info("Read saved snapshot from disk");
|
||||
this.data = json.snapshot;
|
||||
this.version = json.version;
|
||||
this.GUID = json.GUID;
|
||||
}
|
||||
},
|
||||
|
||||
serialize: function SStore_serialize() {
|
||||
let json = uneval(this.data);
|
||||
json = json.replace(/:{type/g, ":\n\t{type");
|
||||
json = json.replace(/}, /g, "},\n ");
|
||||
json = json.replace(/, parentGUID/g, ",\n\t parentGUID");
|
||||
json = json.replace(/, index/g, ",\n\t index");
|
||||
json = json.replace(/, title/g, ",\n\t title");
|
||||
json = json.replace(/, URI/g, ",\n\t URI");
|
||||
json = json.replace(/, tags/g, ",\n\t tags");
|
||||
json = json.replace(/, keyword/g, ",\n\t keyword");
|
||||
return json;
|
||||
},
|
||||
|
||||
wrap: function SStore_wrap() {
|
||||
},
|
||||
|
||||
applyCommands: function SStore_applyCommands(commands) {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
// this._log.debug("Applying cmd to obj: " + uneval(commands[i]));
|
||||
switch (commands[i].action) {
|
||||
case "create":
|
||||
this._data[commands[i].GUID] = eval(uneval(commands[i].data));
|
||||
break;
|
||||
case "edit":
|
||||
if ("GUID" in commands[i].data) {
|
||||
// special-case guid changes
|
||||
let newGUID = commands[i].data.GUID,
|
||||
oldGUID = commands[i].GUID;
|
||||
|
||||
this._data[newGUID] = this._data[oldGUID];
|
||||
delete this._data[oldGUID]
|
||||
|
||||
for (let GUID in this._data) {
|
||||
if (this._data[GUID].parentGUID == oldGUID)
|
||||
this._data[GUID].parentGUID = newGUID;
|
||||
}
|
||||
}
|
||||
for (let prop in commands[i].data) {
|
||||
if (prop == "GUID")
|
||||
continue;
|
||||
this._data[commands[i].GUID][prop] = commands[i].data[prop];
|
||||
}
|
||||
break;
|
||||
case "remove":
|
||||
delete this._data[commands[i].GUID];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this._data;
|
||||
}
|
||||
};
|
||||
SnapshotStore.prototype.__proto__ = new Store();
|
||||
|
||||
function BookmarksStore() {
|
||||
this._init();
|
||||
}
|
||||
BookmarksStore.prototype = {
|
||||
_logName: "BStore",
|
||||
|
||||
__bms: null,
|
||||
get _bms() {
|
||||
if (!this.__bms)
|
||||
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
return this.__bms;
|
||||
},
|
||||
|
||||
__hsvc: null,
|
||||
get _hsvc() {
|
||||
if (!this.__hsvc)
|
||||
this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
return this.__hsvc;
|
||||
},
|
||||
|
||||
__ls: null,
|
||||
get _ls() {
|
||||
if (!this.__ls)
|
||||
this.__ls = Cc["@mozilla.org/browser/livemark-service;2"].
|
||||
getService(Ci.nsILivemarkService);
|
||||
return this.__ls;
|
||||
},
|
||||
|
||||
__ms: null,
|
||||
get _ms() {
|
||||
if (!this.__ms)
|
||||
this.__ms = Cc["@mozilla.org/microsummary/service;1"].
|
||||
getService(Ci.nsIMicrosummaryService);
|
||||
return this.__ms;
|
||||
},
|
||||
|
||||
__ts: null,
|
||||
get _ts() {
|
||||
if (!this.__ts)
|
||||
this.__ts = Cc["@mozilla.org/browser/tagging-service;1"].
|
||||
getService(Ci.nsITaggingService);
|
||||
return this.__ts;
|
||||
},
|
||||
|
||||
__ans: null,
|
||||
get _ans() {
|
||||
if (!this.__ans)
|
||||
this.__ans = Cc["@mozilla.org/browser/annotation-service;1"].
|
||||
getService(Ci.nsIAnnotationService);
|
||||
return this.__ans;
|
||||
},
|
||||
|
||||
_getFolderNodes: function BSS__getFolderNodes(folder) {
|
||||
let query = this._hsvc.getNewQuery();
|
||||
query.setFolders([folder], 1);
|
||||
return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root;
|
||||
},
|
||||
|
||||
_wrapNode: function BSS__wrapNode(node) {
|
||||
var items = {};
|
||||
this._wrapNodeInternal(node, items, null, null);
|
||||
return items;
|
||||
},
|
||||
|
||||
_wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) {
|
||||
let GUID = this._bms.getItemGUID(node.itemId);
|
||||
let item = {parentGUID: parentGUID,
|
||||
index: index};
|
||||
|
||||
if (node.type == node.RESULT_TYPE_FOLDER) {
|
||||
if (this._ls.isLivemark(node.itemId)) {
|
||||
item.type = "livemark";
|
||||
let siteURI = this._ls.getSiteURI(node.itemId);
|
||||
let feedURI = this._ls.getFeedURI(node.itemId);
|
||||
item.siteURI = siteURI? siteURI.spec : "";
|
||||
item.feedURI = feedURI? feedURI.spec : "";
|
||||
} else {
|
||||
item.type = "folder";
|
||||
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
node.containerOpen = true;
|
||||
for (var i = 0; i < node.childCount; i++) {
|
||||
this._wrapNodeInternal(node.getChild(i), items, GUID, i);
|
||||
}
|
||||
}
|
||||
item.title = node.title;
|
||||
} else if (node.type == node.RESULT_TYPE_URI ||
|
||||
node.type == node.RESULT_TYPE_QUERY) {
|
||||
if (this._ms.hasMicrosummary(node.itemId)) {
|
||||
item.type = "microsummary";
|
||||
let micsum = this._ms.getMicrosummary(node.itemId);
|
||||
item.generatorURI = micsum.generator.uri.spec; // breaks local generators
|
||||
} else if (node.type == node.RESULT_TYPE_QUERY) {
|
||||
item.type = "query";
|
||||
item.title = node.title;
|
||||
} else {
|
||||
item.type = "bookmark";
|
||||
item.title = node.title;
|
||||
}
|
||||
item.URI = node.uri;
|
||||
item.tags = this._ts.getTagsForURI(makeURI(node.uri));
|
||||
item.keyword = this._bms.getKeywordForBookmark(node.itemId);
|
||||
} else if (node.type == node.RESULT_TYPE_SEPARATOR) {
|
||||
item.type = "separator";
|
||||
} else {
|
||||
this._log.warn("Warning: unknown item type, cannot serialize: " + node.type);
|
||||
return;
|
||||
}
|
||||
|
||||
items[GUID] = item;
|
||||
},
|
||||
|
||||
_getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) {
|
||||
return this._wrapNode(this._getFolderNodes(folder));
|
||||
},
|
||||
|
||||
_resetGUIDsInt: function BSS__resetGUIDsInt(node) {
|
||||
if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID"))
|
||||
this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID");
|
||||
|
||||
if (node.type == node.RESULT_TYPE_FOLDER &&
|
||||
!this._ls.isLivemark(node.itemId)) {
|
||||
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
node.containerOpen = true;
|
||||
for (var i = 0; i < node.childCount; i++) {
|
||||
this._resetGUIDsInt(node.getChild(i));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_createCommand: function BStore__createCommand(command) {
|
||||
let newId;
|
||||
let parentId = this._bms.getItemIdForGUID(command.data.parentGUID);
|
||||
|
||||
if (parentId < 0) {
|
||||
this._log.warn("Creating node with unknown parent -> reparenting to root");
|
||||
parentId = this._bms.bookmarksMenuFolder;
|
||||
}
|
||||
|
||||
switch (command.data.type) {
|
||||
case "query":
|
||||
case "bookmark":
|
||||
case "microsummary":
|
||||
this._log.info(" -> creating bookmark \"" + command.data.title + "\"");
|
||||
let URI = makeURI(command.data.URI);
|
||||
newId = this._bms.insertBookmark(parentId,
|
||||
URI,
|
||||
command.data.index,
|
||||
command.data.title);
|
||||
this._ts.untagURI(URI, null);
|
||||
this._ts.tagURI(URI, command.data.tags);
|
||||
this._bms.setKeywordForBookmark(newId, command.data.keyword);
|
||||
|
||||
if (command.data.type == "microsummary") {
|
||||
this._log.info(" \-> is a microsummary");
|
||||
let genURI = makeURI(command.data.generatorURI);
|
||||
try {
|
||||
let micsum = this._ms.createMicrosummary(URI, genURI);
|
||||
this._ms.setMicrosummary(newId, micsum);
|
||||
}
|
||||
catch(ex) { /* ignore "missing local generator" exceptions */ }
|
||||
}
|
||||
break;
|
||||
case "folder":
|
||||
this._log.info(" -> creating folder \"" + command.data.title + "\"");
|
||||
newId = this._bms.createFolder(parentId,
|
||||
command.data.title,
|
||||
command.data.index);
|
||||
break;
|
||||
case "livemark":
|
||||
this._log.info(" -> creating livemark \"" + command.data.title + "\"");
|
||||
newId = this._ls.createLivemark(parentId,
|
||||
command.data.title,
|
||||
makeURI(command.data.siteURI),
|
||||
makeURI(command.data.feedURI),
|
||||
command.data.index);
|
||||
break;
|
||||
case "separator":
|
||||
this._log.info(" -> creating separator");
|
||||
newId = this._bms.insertSeparator(parentId, command.data.index);
|
||||
break;
|
||||
default:
|
||||
this._log.error("_createCommand: Unknown item type: " + command.data.type);
|
||||
break;
|
||||
}
|
||||
if (newId)
|
||||
this._bms.setItemGUID(newId, command.GUID);
|
||||
},
|
||||
|
||||
_removeCommand: function BStore__removeCommand(command) {
|
||||
var itemId = this._bms.getItemIdForGUID(command.GUID);
|
||||
if (itemId < 0) {
|
||||
this._log.warn("Attempted to remove item " + command.GUID +
|
||||
", but it does not exist. Skipping.");
|
||||
return;
|
||||
}
|
||||
var type = this._bms.getItemType(itemId);
|
||||
|
||||
switch (type) {
|
||||
case this._bms.TYPE_BOOKMARK:
|
||||
this._log.info(" -> removing bookmark " + command.GUID);
|
||||
this._bms.removeItem(itemId);
|
||||
break;
|
||||
case this._bms.TYPE_FOLDER:
|
||||
this._log.info(" -> removing folder " + command.GUID);
|
||||
this._bms.removeFolder(itemId);
|
||||
break;
|
||||
case this._bms.TYPE_SEPARATOR:
|
||||
this._log.info(" -> removing separator " + command.GUID);
|
||||
this._bms.removeItem(itemId);
|
||||
break;
|
||||
default:
|
||||
this._log.error("removeCommand: Unknown item type: " + type);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_editCommand: function BStore__editCommand(command) {
|
||||
var itemId = this._bms.getItemIdForGUID(command.GUID);
|
||||
if (itemId < 0) {
|
||||
this._log.warn("Item for GUID " + command.GUID + " not found. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (let key in command.data) {
|
||||
switch (key) {
|
||||
case "GUID":
|
||||
var existing = this._bms.getItemIdForGUID(command.data.GUID);
|
||||
if (existing < 0)
|
||||
this._bms.setItemGUID(itemId, command.data.GUID);
|
||||
else
|
||||
this._log.warn("Can't change GUID " + command.GUID +
|
||||
" to " + command.data.GUID + ": GUID already exists.");
|
||||
break;
|
||||
case "title":
|
||||
this._bms.setItemTitle(itemId, command.data.title);
|
||||
break;
|
||||
case "URI":
|
||||
this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI));
|
||||
break;
|
||||
case "index":
|
||||
this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId),
|
||||
command.data.index);
|
||||
break;
|
||||
case "parentGUID":
|
||||
let index = -1;
|
||||
if (command.data.index && command.data.index >= 0)
|
||||
index = command.data.index;
|
||||
this._bms.moveItem(
|
||||
itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index);
|
||||
break;
|
||||
case "tags":
|
||||
let tagsURI = this._bms.getBookmarkURI(itemId);
|
||||
this._ts.untagURI(URI, null);
|
||||
this._ts.tagURI(tagsURI, command.data.tags);
|
||||
break;
|
||||
case "keyword":
|
||||
this._bms.setKeywordForBookmark(itemId, command.data.keyword);
|
||||
break;
|
||||
case "generatorURI":
|
||||
let micsumURI = makeURI(this._bms.getBookmarkURI(itemId));
|
||||
let genURI = makeURI(command.data.generatorURI);
|
||||
let micsum = this._ms.createMicrosummary(micsumURI, genURI);
|
||||
this._ms.setMicrosummary(itemId, micsum);
|
||||
break;
|
||||
case "siteURI":
|
||||
this._ls.setSiteURI(itemId, makeURI(command.data.siteURI));
|
||||
break;
|
||||
case "feedURI":
|
||||
this._ls.setFeedURI(itemId, makeURI(command.data.feedURI));
|
||||
break;
|
||||
default:
|
||||
this._log.warn("Can't change item property: " + key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
wrap: function BStore_wrap() {
|
||||
let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder);
|
||||
let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder);
|
||||
let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder);
|
||||
|
||||
for (let guid in unfiled) {
|
||||
if (!(guid in filed))
|
||||
filed[guid] = unfiled[guid];
|
||||
}
|
||||
|
||||
for (let guid in toolbar) {
|
||||
if (!(guid in filed))
|
||||
filed[guid] = toolbar[guid];
|
||||
}
|
||||
|
||||
return filed; // (combined)
|
||||
},
|
||||
|
||||
resetGUIDs: function BStore_resetGUIDs() {
|
||||
this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder));
|
||||
this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder));
|
||||
this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder));
|
||||
},
|
||||
|
||||
applyCommands: function BStore_applyCommands(commandList) {
|
||||
for (var i = 0; i < commandList.length; i++) {
|
||||
var command = commandList[i];
|
||||
this._log.debug("Processing command: " + uneval(command));
|
||||
switch (command["action"]) {
|
||||
case "create":
|
||||
this._createCommand(command);
|
||||
break;
|
||||
case "remove":
|
||||
this._removeCommand(command);
|
||||
break;
|
||||
case "edit":
|
||||
this._editCommand(command);
|
||||
break;
|
||||
default:
|
||||
this._log.error("unknown action in command: " + command["action"]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
BookmarksStore.prototype.__proto__ = new Store();
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,413 @@
|
|||
/* ***** 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 = ['SyncCore', 'BookmarksSyncCore'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/constants.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
|
||||
Function.prototype.async = generatorAsync;
|
||||
|
||||
/*
|
||||
* SyncCore objects
|
||||
* Sync cores deal with diff creation and conflict resolution.
|
||||
* Tree data structures where all nodes have GUIDs only need to be
|
||||
* subclassed for each data type to implement commandLike and
|
||||
* itemExists.
|
||||
*/
|
||||
|
||||
function SyncCore() {
|
||||
this._init();
|
||||
}
|
||||
SyncCore.prototype = {
|
||||
_logName: "Sync",
|
||||
|
||||
_init: function SC__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
},
|
||||
|
||||
// FIXME: this won't work for deep objects, or objects with optional properties
|
||||
_getEdits: function SC__getEdits(a, b) {
|
||||
let ret = {numProps: 0, props: {}};
|
||||
for (prop in a) {
|
||||
if (!deepEquals(a[prop], b[prop])) {
|
||||
ret.numProps++;
|
||||
ret.props[prop] = b[prop];
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
_nodeParents: function SC__nodeParents(GUID, tree) {
|
||||
return this._nodeParentsInt(GUID, tree, []);
|
||||
},
|
||||
|
||||
_nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) {
|
||||
if (!tree[GUID] || !tree[GUID].parentGUID)
|
||||
return parents;
|
||||
parents.push(tree[GUID].parentGUID);
|
||||
return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents);
|
||||
},
|
||||
|
||||
_detectUpdates: function SC__detectUpdates(onComplete, a, b) {
|
||||
let [self, cont] = yield;
|
||||
let listener = new EventListener(cont);
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
|
||||
let cmds = [];
|
||||
|
||||
try {
|
||||
for (let GUID in a) {
|
||||
|
||||
timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT);
|
||||
yield; // Yield to main loop
|
||||
|
||||
if (GUID in b) {
|
||||
let edits = this._getEdits(a[GUID], b[GUID]);
|
||||
if (edits.numProps == 0) // no changes - skip
|
||||
continue;
|
||||
let parents = this._nodeParents(GUID, b);
|
||||
cmds.push({action: "edit", GUID: GUID,
|
||||
depth: parents.length, parents: parents,
|
||||
data: edits.props});
|
||||
} else {
|
||||
let parents = this._nodeParents(GUID, a); // ???
|
||||
cmds.push({action: "remove", GUID: GUID,
|
||||
depth: parents.length, parents: parents});
|
||||
}
|
||||
}
|
||||
for (let GUID in b) {
|
||||
|
||||
timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT);
|
||||
yield; // Yield to main loop
|
||||
|
||||
if (GUID in a)
|
||||
continue;
|
||||
let parents = this._nodeParents(GUID, b);
|
||||
cmds.push({action: "create", GUID: GUID,
|
||||
depth: parents.length, parents: parents,
|
||||
data: b[GUID]});
|
||||
}
|
||||
cmds.sort(function(a, b) {
|
||||
if (a.depth > b.depth)
|
||||
return 1;
|
||||
if (a.depth < b.depth)
|
||||
return -1;
|
||||
if (a.index > b.index)
|
||||
return -1;
|
||||
if (a.index < b.index)
|
||||
return 1;
|
||||
return 0; // should never happen, but not a big deal if it does
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
timer = null;
|
||||
generatorDone(this, self, onComplete, cmds);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
_commandLike: function SC__commandLike(a, b) {
|
||||
this._log.error("commandLike needs to be subclassed");
|
||||
|
||||
// Check that neither command is null, and verify that the GUIDs
|
||||
// are different (otherwise we need to check for edits)
|
||||
if (!a || !b || a.GUID == b.GUID)
|
||||
return false;
|
||||
|
||||
// Check that all other properties are the same
|
||||
// FIXME: could be optimized...
|
||||
for (let key in a) {
|
||||
if (key != "GUID" && !deepEquals(a[key], b[key]))
|
||||
return false;
|
||||
}
|
||||
for (let key in b) {
|
||||
if (key != "GUID" && !deepEquals(a[key], b[key]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// When we change the GUID of a local item (because we detect it as
|
||||
// being the same item as a remote one), we need to fix any other
|
||||
// local items that have it as their parent
|
||||
_fixParents: function SC__fixParents(list, oldGUID, newGUID) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (!list[i])
|
||||
continue;
|
||||
if (list[i].data.parentGUID == oldGUID)
|
||||
list[i].data.parentGUID = newGUID;
|
||||
for (let j = 0; j < list[i].parents.length; j++) {
|
||||
if (list[i].parents[j] == oldGUID)
|
||||
list[i].parents[j] = newGUID;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_conflicts: function SC__conflicts(a, b) {
|
||||
if ((a.GUID == b.GUID) && !deepEquals(a, b))
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
_getPropagations: function SC__getPropagations(commands, conflicts, propagations) {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
let alsoConflicts = function(elt) {
|
||||
return (elt.action == "create" || elt.action == "remove") &&
|
||||
commands[i].parents.indexOf(elt.GUID) >= 0;
|
||||
};
|
||||
if (conflicts.some(alsoConflicts))
|
||||
conflicts.push(commands[i]);
|
||||
|
||||
let cmdConflicts = function(elt) {
|
||||
return elt.GUID == commands[i].GUID;
|
||||
};
|
||||
if (!conflicts.some(cmdConflicts))
|
||||
propagations.push(commands[i]);
|
||||
}
|
||||
},
|
||||
|
||||
_itemExists: function SC__itemExists(GUID) {
|
||||
this._log.error("itemExists needs to be subclassed");
|
||||
return false;
|
||||
},
|
||||
|
||||
_reconcile: function SC__reconcile(onComplete, listA, listB) {
|
||||
let [self, cont] = yield;
|
||||
let listener = new EventListener(cont);
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
|
||||
let propagations = [[], []];
|
||||
let conflicts = [[], []];
|
||||
let ret = {propagations: propagations, conflicts: conflicts};
|
||||
this._log.debug("Reconciling " + listA.length +
|
||||
" against " + listB.length + "commands");
|
||||
|
||||
try {
|
||||
let guidChanges = [];
|
||||
for (let i = 0; i < listA.length; i++) {
|
||||
let a = listA[i];
|
||||
timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT);
|
||||
yield; // Yield to main loop
|
||||
|
||||
//this._log.debug("comparing " + i + ", listB length: " + listB.length);
|
||||
|
||||
let skip = false;
|
||||
listB = listB.filter(function(b) {
|
||||
// fast path for when we already found a matching command
|
||||
if (skip)
|
||||
return true;
|
||||
|
||||
if (deepEquals(a, b)) {
|
||||
delete listA[i]; // a
|
||||
skip = true;
|
||||
return false; // b
|
||||
|
||||
} else if (this._commandLike(a, b)) {
|
||||
this._fixParents(listA, a.GUID, b.GUID);
|
||||
guidChanges.push({action: "edit",
|
||||
GUID: a.GUID,
|
||||
data: {GUID: b.GUID}});
|
||||
delete listA[i]; // a
|
||||
skip = true;
|
||||
return false; // b, but we add it back from guidChanges
|
||||
}
|
||||
|
||||
// watch out for create commands with GUIDs that already exist
|
||||
if (b.action == "create" && this._itemExists(b.GUID)) {
|
||||
this._log.error("Remote command has GUID that already exists " +
|
||||
"locally. Dropping command.");
|
||||
return false; // delete b
|
||||
}
|
||||
return true; // keep b
|
||||
}, this);
|
||||
}
|
||||
|
||||
listA = listA.filter(function(elt) { return elt });
|
||||
listB = listB.concat(guidChanges);
|
||||
|
||||
for (let i = 0; i < listA.length; i++) {
|
||||
for (let j = 0; j < listB.length; j++) {
|
||||
|
||||
timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT);
|
||||
yield; // Yield to main loop
|
||||
|
||||
if (this._conflicts(listA[i], listB[j]) ||
|
||||
this._conflicts(listB[j], listA[i])) {
|
||||
if (!conflicts[0].some(
|
||||
function(elt) { return elt.GUID == listA[i].GUID }))
|
||||
conflicts[0].push(listA[i]);
|
||||
if (!conflicts[1].some(
|
||||
function(elt) { return elt.GUID == listB[j].GUID }))
|
||||
conflicts[1].push(listB[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._getPropagations(listA, conflicts[0], propagations[1]);
|
||||
|
||||
timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT);
|
||||
yield; // Yield to main loop
|
||||
|
||||
this._getPropagations(listB, conflicts[1], propagations[0]);
|
||||
ret = {propagations: propagations, conflicts: conflicts};
|
||||
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + e.message);
|
||||
|
||||
} finally {
|
||||
timer = null;
|
||||
generatorDone(this, self, onComplete, ret);
|
||||
yield; // onComplete is responsible for closing the generator
|
||||
}
|
||||
this._log.warn("generator not properly closed");
|
||||
},
|
||||
|
||||
// Public methods
|
||||
|
||||
detectUpdates: function SC_detectUpdates(onComplete, a, b) {
|
||||
return this._detectUpdates.async(this, onComplete, a, b);
|
||||
},
|
||||
|
||||
reconcile: function SC_reconcile(onComplete, listA, listB) {
|
||||
return this._reconcile.async(this, onComplete, listA, listB);
|
||||
}
|
||||
};
|
||||
|
||||
function BookmarksSyncCore() {
|
||||
this._init();
|
||||
}
|
||||
BookmarksSyncCore.prototype = {
|
||||
_logName: "BMSync",
|
||||
|
||||
__bms: null,
|
||||
get _bms() {
|
||||
if (!this.__bms)
|
||||
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
return this.__bms;
|
||||
},
|
||||
|
||||
// NOTE: Needs to be subclassed
|
||||
_itemExists: function BSC__itemExists(GUID) {
|
||||
return this._bms.getItemIdForGUID(GUID) >= 0;
|
||||
},
|
||||
|
||||
_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.
|
||||
// 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.data.type != b.data.type ||
|
||||
a.data.parentGUID != b.data.parentGUID ||
|
||||
a.GUID == b.GUID)
|
||||
return false;
|
||||
|
||||
// Bookmarks are allowed to be in a different index as long as
|
||||
// they are in the same folder. Folders and separators must be at
|
||||
// the same index to qualify for 'likeness'.
|
||||
switch (a.data.type) {
|
||||
case "bookmark":
|
||||
if (a.data.URI == b.data.URI &&
|
||||
a.data.title == b.data.title)
|
||||
return true;
|
||||
return false;
|
||||
case "query":
|
||||
if (a.data.URI == b.data.URI &&
|
||||
a.data.title == b.data.title)
|
||||
return true;
|
||||
return false;
|
||||
case "microsummary":
|
||||
if (a.data.URI == b.data.URI &&
|
||||
a.data.generatorURI == b.data.generatorURI)
|
||||
return true;
|
||||
return false;
|
||||
case "folder":
|
||||
if (a.index == b.index &&
|
||||
a.data.title == b.data.title)
|
||||
return true;
|
||||
return false;
|
||||
case "livemark":
|
||||
if (a.data.title == b.data.title &&
|
||||
a.data.siteURI == b.data.siteURI &&
|
||||
a.data.feedURI == b.data.feedURI)
|
||||
return true;
|
||||
return false;
|
||||
case "separator":
|
||||
if (a.index == b.index)
|
||||
return true;
|
||||
return false;
|
||||
default:
|
||||
this._log.error("commandLike: Unknown item type: " + uneval(a));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
BookmarksSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
/* ***** 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 = ['deepEquals', 'makeFile', 'makeURI', 'xpath',
|
||||
'bind2', 'generatorAsync', 'generatorDone', 'EventListener'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}");
|
||||
Cu.import("resource://weave/constants.js");
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
/*
|
||||
* Utility functions
|
||||
*/
|
||||
|
||||
function deepEquals(a, b) {
|
||||
if (!a && !b)
|
||||
return true;
|
||||
if (!a || !b)
|
||||
return false;
|
||||
|
||||
if (typeof(a) != "object" && typeof(b) != "object")
|
||||
return a == b;
|
||||
if (typeof(a) != "object" || typeof(b) != "object")
|
||||
return false;
|
||||
|
||||
for (let key in a) {
|
||||
if (typeof(a[key]) == "object") {
|
||||
if (!typeof(b[key]) == "object")
|
||||
return false;
|
||||
if (!deepEquals(a[key], b[key]))
|
||||
return false;
|
||||
} else {
|
||||
if (a[key] != b[key])
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function makeFile(path) {
|
||||
var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
||||
file.initWithPath(path);
|
||||
return file;
|
||||
}
|
||||
|
||||
function makeURI(URIString) {
|
||||
if (URIString === null || URIString == "")
|
||||
return null;
|
||||
let ioservice = Cc["@mozilla.org/network/io-service;1"].
|
||||
getService(Ci.nsIIOService);
|
||||
return ioservice.newURI(URIString, null, null);
|
||||
}
|
||||
|
||||
function xpath(xmlDoc, xpathString) {
|
||||
let root = xmlDoc.ownerDocument == null ?
|
||||
xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement
|
||||
let nsResolver = xmlDoc.createNSResolver(root);
|
||||
|
||||
return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver,
|
||||
Ci.nsIDOMXPathResult.ANY_TYPE, null);
|
||||
}
|
||||
|
||||
function bind2(object, method) {
|
||||
return function innerBind() { return method.apply(object, arguments); }
|
||||
}
|
||||
|
||||
// Meant to be used like this in code that imports this file:
|
||||
//
|
||||
// Function.prototype.async = generatorAsync;
|
||||
//
|
||||
// So that you can do:
|
||||
//
|
||||
// gen = fooGen.async(...);
|
||||
// ret = yield;
|
||||
//
|
||||
// where fooGen is a generator function, and gen is the running generator.
|
||||
// ret is whatever the generator 'returns' via generatorDone().
|
||||
|
||||
function generatorAsync(self, extra_args) {
|
||||
try {
|
||||
let args = Array.prototype.slice.call(arguments, 1);
|
||||
let gen = this.apply(self, args);
|
||||
gen.next(); // must initialize before sending
|
||||
gen.send([gen, function(data) {continueGenerator(gen, data);}]);
|
||||
return gen;
|
||||
} catch (e) {
|
||||
if (e instanceof StopIteration) {
|
||||
dump("async warning: generator stopped unexpectedly");
|
||||
return null;
|
||||
} else {
|
||||
dump("Exception caught: " + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function continueGenerator(generator, data) {
|
||||
try { generator.send(data); }
|
||||
catch (e) {
|
||||
if (e instanceof StopIteration)
|
||||
dump("continueGenerator warning: generator stopped unexpectedly");
|
||||
else
|
||||
dump("Exception caught: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// generators created using Function.async can't simply call the
|
||||
// callback with the return value, since that would cause the calling
|
||||
// function to end up running (after the yield) from inside the
|
||||
// generator. Instead, generators can call this method which sets up
|
||||
// a timer to call the callback from a timer (and cleans up the timer
|
||||
// to avoid leaks). It also closes generators after the timeout, to
|
||||
// keep things clean.
|
||||
function generatorDone(object, generator, callback, retval) {
|
||||
if (object._timer)
|
||||
throw "Called generatorDone when there is a timer already set."
|
||||
|
||||
let cb = bind2(object, function(event) {
|
||||
generator.close();
|
||||
generator = null;
|
||||
object._timer = null;
|
||||
if (callback)
|
||||
callback(retval);
|
||||
});
|
||||
|
||||
object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
object._timer.initWithCallback(new EventListener(cb),
|
||||
0, object._timer.TYPE_ONE_SHOT);
|
||||
}
|
||||
|
||||
/*
|
||||
* Event listener object
|
||||
* Used to handle XMLHttpRequest and nsITimer callbacks
|
||||
*/
|
||||
|
||||
function EventListener(handler, eventName) {
|
||||
this._handler = handler;
|
||||
this._eventName = eventName;
|
||||
this._log = Log4Moz.Service.getLogger("Service.EventHandler");
|
||||
}
|
||||
EventListener.prototype = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]),
|
||||
|
||||
// DOM event listener
|
||||
handleEvent: function EL_handleEvent(event) {
|
||||
this._log.debug("Handling event " + this._eventName);
|
||||
this._handler(event);
|
||||
},
|
||||
|
||||
// nsITimerCallback
|
||||
notify: function EL_notify(timer) {
|
||||
this._log.trace("Timer fired");
|
||||
this._handler(timer);
|
||||
}
|
||||
};
|
||||
|
||||
function addModuleAlias(alias, extensionId) {
|
||||
let ioSvc = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let resProt = ioSvc.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
if (!resProt.hasSubstitution(alias)) {
|
||||
let extMgr = Cc["@mozilla.org/extensions/manager;1"]
|
||||
.getService(Ci.nsIExtensionManager);
|
||||
let loc = extMgr.getInstallLocation(extensionId);
|
||||
let extD = loc.getItemLocation(extensionId);
|
||||
extD.append("modules");
|
||||
resProt.setSubstitution(alias, ioSvc.newFileURI(extD));
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче