зеркало из https://github.com/mozilla/pjs.git
merge upstream changes
This commit is contained in:
Коммит
a2659cdec7
|
@ -24,6 +24,7 @@
|
|||
<!ENTITY cookiesCheckbox.label "Cookies">
|
||||
<!ENTITY passwordsCheckbox.label "Saved Passwords">
|
||||
<!ENTITY formsCheckbox.label "Saved Form Data">
|
||||
<!ENTITY tabsCheckbox.label "Tabs">
|
||||
|
||||
<!ENTITY addonsGroupbox.description "This is where you will find, install, and manage Weave add-ons.">
|
||||
|
||||
|
|
|
@ -7,3 +7,10 @@
|
|||
<!ENTITY openLogItem.label "Activity Log...">
|
||||
<!ENTITY status.offline.label "Offline">
|
||||
|
||||
<!ENTITY syncOpenTabsPanelButton.label "Tabs">
|
||||
<!ENTITY syncTabsPanel.title "Tabs From Other Computers">
|
||||
<!ENTITY syncTabsPanel.description "These tabs were open on other computers. Select the ones you want to open on this computer.">
|
||||
<!ENTITY syncCancelTabsPanelButton.label "Cancel">
|
||||
<!ENTITY syncTabsButton.label "OK">
|
||||
<!ENTITY syncTabsMenu.label "Tabs From Other Computers">
|
||||
<!ENTITY syncNoTabsMenuItem.label "No Tabs Available">
|
||||
|
|
|
@ -4,4 +4,5 @@ status.idle = Idle
|
|||
status.active = Working...
|
||||
status.offline = Offline
|
||||
status.error = Error
|
||||
shareBookmark.menuItem = Share This Folder...
|
||||
shareBookmark.menuItem = Share This Folder...
|
||||
unShareBookmark.menuItem = Stop Sharing This Folder
|
|
@ -19,6 +19,7 @@
|
|||
*
|
||||
* Contributor(s):
|
||||
* Dan Mills <thunder@mozilla.com>
|
||||
* Myk Melez <myk@mozilla.org>
|
||||
*
|
||||
* 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
|
||||
|
@ -34,9 +35,8 @@
|
|||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
const EXPORTED_SYMBOLS = ['Engines', 'Engine',
|
||||
'BookmarksEngine', 'HistoryEngine', 'CookieEngine',
|
||||
'PasswordEngine', 'FormEngine'];
|
||||
const EXPORTED_SYMBOLS = ['Engines',
|
||||
'Engine'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
@ -169,7 +169,7 @@ Engine.prototype = {
|
|||
},
|
||||
|
||||
get _engineId() {
|
||||
let id = ID.get('Engine:' + this.name)
|
||||
let id = ID.get('Engine:' + this.name);
|
||||
if (!id ||
|
||||
id.username != this._pbeId.username || id.realm != this._pbeId.realm) {
|
||||
let password = null;
|
||||
|
@ -265,7 +265,7 @@ Engine.prototype = {
|
|||
// 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() {
|
||||
_sync: function Engine__sync() {
|
||||
let self = yield;
|
||||
|
||||
this._log.info("Beginning sync");
|
||||
|
@ -597,7 +597,7 @@ Engine.prototype = {
|
|||
this._core.detectUpdates(self.cb, this._snapshot.data, snap.data);
|
||||
ret.updates = yield;
|
||||
|
||||
self.done(ret)
|
||||
self.done(ret);
|
||||
},
|
||||
|
||||
_fullUpload: function Engine__fullUpload() {
|
||||
|
@ -750,263 +750,3 @@ Engine.prototype = {
|
|||
this._notify("reset-client", this._resetClient).async(this, onComplete);
|
||||
}
|
||||
};
|
||||
|
||||
function BookmarksEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
BookmarksEngine.prototype = {
|
||||
get name() { return "bookmarks"; },
|
||||
get logName() { return "BmkEngine"; },
|
||||
get serverPrefix() { return "user-data/bookmarks/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new BookmarksSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new BookmarksStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new BookmarksTracker();
|
||||
return this.__tracker;
|
||||
},
|
||||
|
||||
syncMounts: function BmkEngine_syncMounts(onComplete) {
|
||||
this._syncMounts.async(this, onComplete);
|
||||
},
|
||||
_syncMounts: function BmkEngine__syncMounts() {
|
||||
let self = yield;
|
||||
let mounts = this._store.findMounts();
|
||||
|
||||
for (i = 0; i < mounts.length; i++) {
|
||||
try {
|
||||
this._syncOneMount.async(this, self.cb, mounts[i]);
|
||||
yield;
|
||||
} catch (e) {
|
||||
this._log.warn("Could not sync shared folder from " + mounts[i].userid);
|
||||
this._log.trace(Utils.stackTrace(e));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_syncOneMount: function BmkEngine__syncOneMount(mountData) {
|
||||
let self = yield;
|
||||
let user = mountData.userid;
|
||||
let prefix = DAV.defaultPrefix;
|
||||
let serverURL = Utils.prefs.getCharPref("serverURL");
|
||||
let snap = new SnapshotStore();
|
||||
|
||||
this._log.debug("Syncing shared folder from user " + user);
|
||||
|
||||
try {
|
||||
let hash = Utils.sha1(user);
|
||||
DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly!
|
||||
|
||||
this._getSymKey.async(this, self.cb);
|
||||
yield;
|
||||
|
||||
this._log.trace("Getting status file for " + user);
|
||||
DAV.GET(this.statusFile, self.cb);
|
||||
let resp = yield;
|
||||
Utils.ensureStatus(resp.status, "Could not download status file.");
|
||||
let status = this._json.decode(resp.responseText);
|
||||
|
||||
this._log.trace("Downloading server snapshot for " + user);
|
||||
DAV.GET(this.snapshotFile, self.cb);
|
||||
resp = yield;
|
||||
Utils.ensureStatus(resp.status, "Could not download snapshot.");
|
||||
Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText,
|
||||
this._engineId, status.snapEncryption);
|
||||
let data = yield;
|
||||
snap.data = this._json.decode(data);
|
||||
|
||||
this._log.trace("Downloading server deltas for " + user);
|
||||
DAV.GET(this.deltasFile, self.cb);
|
||||
resp = yield;
|
||||
Utils.ensureStatus(resp.status, "Could not download deltas.");
|
||||
Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText,
|
||||
this._engineId, status.deltasEncryption);
|
||||
data = yield;
|
||||
deltas = this._json.decode(data);
|
||||
}
|
||||
catch (e) { throw e; }
|
||||
finally { DAV.defaultPrefix = prefix; }
|
||||
|
||||
// apply deltas to get current snapshot
|
||||
for (var i = 0; i < deltas.length; i++) {
|
||||
snap.applyCommands.async(snap, self.cb, deltas[i]);
|
||||
yield;
|
||||
}
|
||||
|
||||
// prune tree / get what we want
|
||||
for (let guid in snap.data) {
|
||||
if (snap.data[guid].type != "bookmark")
|
||||
delete snap.data[guid];
|
||||
else
|
||||
snap.data[guid].parentGUID = mountData.rootGUID;
|
||||
}
|
||||
|
||||
this._log.trace("Got bookmarks fror " + user + ", comparing with local copy");
|
||||
this._core.detectUpdates(self.cb, mountData.snapshot, snap.data);
|
||||
let diff = yield;
|
||||
|
||||
// FIXME: should make sure all GUIDs here live under the mountpoint
|
||||
this._log.trace("Applying changes to folder from " + user);
|
||||
this._store.applyCommands.async(this._store, self.cb, diff);
|
||||
yield;
|
||||
|
||||
this._log.trace("Shared folder from " + user + " successfully synced!");
|
||||
}
|
||||
};
|
||||
BookmarksEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function HistoryEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
HistoryEngine.prototype = {
|
||||
get name() { return "history"; },
|
||||
get logName() { return "HistEngine"; },
|
||||
get serverPrefix() { return "user-data/history/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new HistorySyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new HistoryStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new HistoryTracker();
|
||||
return this.__tracker;
|
||||
}
|
||||
};
|
||||
HistoryEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function CookieEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
CookieEngine.prototype = {
|
||||
get name() { return "cookies"; },
|
||||
get logName() { return "CookieEngine"; },
|
||||
get serverPrefix() { return "user-data/cookies/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new CookieSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new CookieStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new CookieTracker();
|
||||
return this.__tracker;
|
||||
}
|
||||
};
|
||||
CookieEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function PasswordEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
PasswordEngine.prototype = {
|
||||
get name() { return "passwords"; },
|
||||
get logName() { return "PasswordEngine"; },
|
||||
get serverPrefix() { return "user-data/passwords/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core) {
|
||||
this.__core = new PasswordSyncCore();
|
||||
this.__core._hashLoginInfo = this._hashLoginInfo;
|
||||
}
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store) {
|
||||
this.__store = new PasswordStore();
|
||||
this.__store._hashLoginInfo = this._hashLoginInfo;
|
||||
}
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
/*
|
||||
* _hashLoginInfo
|
||||
*
|
||||
* nsILoginInfo objects don't have a unique GUID, so we need to generate one
|
||||
* on the fly. This is done by taking a hash of every field in the object.
|
||||
* Note that the resulting GUID could potentiually reveal passwords via
|
||||
* dictionary attacks or brute force. But GUIDs shouldn't be obtainable by
|
||||
* anyone, so this should generally be safe.
|
||||
*/
|
||||
_hashLoginInfo : function (aLogin) {
|
||||
var loginKey = aLogin.hostname + ":" +
|
||||
aLogin.formSubmitURL + ":" +
|
||||
aLogin.httpRealm + ":" +
|
||||
aLogin.username + ":" +
|
||||
aLogin.password + ":" +
|
||||
aLogin.usernameField + ":" +
|
||||
aLogin.passwordField;
|
||||
|
||||
return Utils.sha1(loginKey);
|
||||
}
|
||||
};
|
||||
PasswordEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function FormEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
FormEngine.prototype = {
|
||||
get name() { return "forms"; },
|
||||
get logName() { return "FormEngine"; },
|
||||
get serverPrefix() { return "user-data/forms/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new FormSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new FormStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new FormsTracker();
|
||||
return this.__tracker;
|
||||
}
|
||||
};
|
||||
FormEngine.prototype.__proto__ = new Engine();
|
||||
|
|
|
@ -0,0 +1,717 @@
|
|||
const EXPORTED_SYMBOLS = ['BookmarksEngine'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/dav.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
Cu.import("resource://weave/crypto.js");
|
||||
Cu.import("resource://weave/async.js");
|
||||
Cu.import("resource://weave/engines.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
Cu.import("resource://weave/trackers.js");
|
||||
|
||||
Function.prototype.async = Async.sugar;
|
||||
|
||||
function BookmarksEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
BookmarksEngine.prototype = {
|
||||
get name() { return "bookmarks"; },
|
||||
get logName() { return "BmkEngine"; },
|
||||
get serverPrefix() { return "user-data/bookmarks/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new BookmarksSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new BookmarksStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new BookmarksTracker();
|
||||
return this.__tracker;
|
||||
},
|
||||
|
||||
syncMounts: function BmkEngine_syncMounts(onComplete) {
|
||||
this._syncMounts.async(this, onComplete);
|
||||
},
|
||||
_syncMounts: function BmkEngine__syncMounts() {
|
||||
let self = yield;
|
||||
let mounts = this._store.findMounts();
|
||||
|
||||
for (i = 0; i < mounts.length; i++) {
|
||||
try {
|
||||
this._syncOneMount.async(this, self.cb, mounts[i]);
|
||||
yield;
|
||||
} catch (e) {
|
||||
this._log.warn("Could not sync shared folder from " + mounts[i].userid);
|
||||
this._log.trace(Utils.stackTrace(e));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_syncOneMount: function BmkEngine__syncOneMount(mountData) {
|
||||
let self = yield;
|
||||
let user = mountData.userid;
|
||||
let prefix = DAV.defaultPrefix;
|
||||
let serverURL = Utils.prefs.getCharPref("serverURL");
|
||||
let snap = new SnapshotStore();
|
||||
|
||||
this._log.debug("Syncing shared folder from user " + user);
|
||||
|
||||
try {
|
||||
let hash = Utils.sha1(user);
|
||||
DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly!
|
||||
|
||||
this._getSymKey.async(this, self.cb);
|
||||
yield;
|
||||
|
||||
this._log.trace("Getting status file for " + user);
|
||||
DAV.GET(this.statusFile, self.cb);
|
||||
let resp = yield;
|
||||
Utils.ensureStatus(resp.status, "Could not download status file.");
|
||||
let status = this._json.decode(resp.responseText);
|
||||
|
||||
this._log.trace("Downloading server snapshot for " + user);
|
||||
DAV.GET(this.snapshotFile, self.cb);
|
||||
resp = yield;
|
||||
Utils.ensureStatus(resp.status, "Could not download snapshot.");
|
||||
Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText,
|
||||
this._engineId, status.snapEncryption);
|
||||
let data = yield;
|
||||
snap.data = this._json.decode(data);
|
||||
|
||||
this._log.trace("Downloading server deltas for " + user);
|
||||
DAV.GET(this.deltasFile, self.cb);
|
||||
resp = yield;
|
||||
Utils.ensureStatus(resp.status, "Could not download deltas.");
|
||||
Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText,
|
||||
this._engineId, status.deltasEncryption);
|
||||
data = yield;
|
||||
deltas = this._json.decode(data);
|
||||
}
|
||||
catch (e) { throw e; }
|
||||
finally { DAV.defaultPrefix = prefix; }
|
||||
|
||||
// apply deltas to get current snapshot
|
||||
for (var i = 0; i < deltas.length; i++) {
|
||||
snap.applyCommands.async(snap, self.cb, deltas[i]);
|
||||
yield;
|
||||
}
|
||||
|
||||
// prune tree / get what we want
|
||||
for (let guid in snap.data) {
|
||||
if (snap.data[guid].type != "bookmark")
|
||||
delete snap.data[guid];
|
||||
else
|
||||
snap.data[guid].parentGUID = mountData.rootGUID;
|
||||
}
|
||||
|
||||
this._log.trace("Got bookmarks fror " + user + ", comparing with local copy");
|
||||
this._core.detectUpdates(self.cb, mountData.snapshot, snap.data);
|
||||
let diff = yield;
|
||||
|
||||
// FIXME: should make sure all GUIDs here live under the mountpoint
|
||||
this._log.trace("Applying changes to folder from " + user);
|
||||
this._store.applyCommands.async(this._store, self.cb, diff);
|
||||
yield;
|
||||
|
||||
this._log.trace("Shared folder from " + user + " successfully synced!");
|
||||
}
|
||||
};
|
||||
BookmarksEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
_itemExists: function BSC__itemExists(GUID) {
|
||||
return this._bms.getItemIdForGUID(GUID) >= 0;
|
||||
},
|
||||
|
||||
_getEdits: function BSC__getEdits(a, b) {
|
||||
// NOTE: we do not increment ret.numProps, as that would cause
|
||||
// edit commands to always get generated
|
||||
let ret = SyncCore.prototype._getEdits.call(this, a, b);
|
||||
ret.props.type = a.type;
|
||||
return ret;
|
||||
},
|
||||
|
||||
// compares properties
|
||||
// returns true if the property is not set in either object
|
||||
// returns true if the property is set and equal in both objects
|
||||
// returns false otherwise
|
||||
_comp: function BSC__comp(a, b, prop) {
|
||||
return (!a.data[prop] && !b.data[prop]) ||
|
||||
(a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop]));
|
||||
},
|
||||
|
||||
_commandLike: function BSC__commandLike(a, b) {
|
||||
// Check that neither command is null, that their actions, types,
|
||||
// and parents are the same, and that they don't have the same
|
||||
// GUID.
|
||||
// * Items with the same GUID do not qualify for 'likeness' because
|
||||
// we already consider them to be the same object, and therefore
|
||||
// we need to process any edits.
|
||||
// * Remove or edit commands don't qualify for likeness either,
|
||||
// since remove or edit commands with different GUIDs are
|
||||
// guaranteed to refer to two different items
|
||||
// * The parent GUID check works because reconcile() fixes up the
|
||||
// parent GUIDs as it runs, and the command list is sorted by
|
||||
// depth
|
||||
if (!a || !b ||
|
||||
a.action != b.action ||
|
||||
a.action != "create" ||
|
||||
a.data.type != b.data.type ||
|
||||
a.data.parentGUID != b.data.parentGUID ||
|
||||
a.GUID == b.GUID)
|
||||
return false;
|
||||
|
||||
// Bookmarks and folders are allowed to be in a different index as long as
|
||||
// they are in the same folder. Separators must be at
|
||||
// the same index to qualify for 'likeness'.
|
||||
switch (a.data.type) {
|
||||
case "bookmark":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "query":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "microsummary":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'generatorURI'))
|
||||
return true;
|
||||
return false;
|
||||
case "folder":
|
||||
if (this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "livemark":
|
||||
if (this._comp(a, b, 'title') &&
|
||||
this._comp(a, b, 'siteURI') &&
|
||||
this._comp(a, b, 'feedURI'))
|
||||
return true;
|
||||
return false;
|
||||
case "separator":
|
||||
if (this._comp(a, b, 'index'))
|
||||
return true;
|
||||
return false;
|
||||
default:
|
||||
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||
this._log.error("commandLike: Unknown item type: " + json.encode(a));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
BookmarksSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
_getItemIdForGUID: function BStore__getItemIdForGUID(GUID) {
|
||||
switch (GUID) {
|
||||
case "menu":
|
||||
return this._bms.bookmarksMenuFolder;
|
||||
case "toolbar":
|
||||
return this._bms.toolbarFolder;
|
||||
case "unfiled":
|
||||
return this._bms.unfiledBookmarksFolder;
|
||||
default:
|
||||
return this._bms.getItemIdForGUID(GUID);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
_createCommand: function BStore__createCommand(command) {
|
||||
let newId;
|
||||
let parentId = this._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.debug(" -> creating bookmark \"" + command.data.title + "\"");
|
||||
let URI = Utils.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.description) {
|
||||
this._ans.setItemAnnotation(newId, "bookmarkProperties/description",
|
||||
command.data.description, 0,
|
||||
this._ans.EXPIRE_NEVER);
|
||||
}
|
||||
|
||||
if (command.data.type == "microsummary") {
|
||||
this._log.debug(" \-> is a microsummary");
|
||||
this._ans.setItemAnnotation(newId, "bookmarks/staticTitle",
|
||||
command.data.staticTitle || "", 0, this._ans.EXPIRE_NEVER);
|
||||
let genURI = Utils.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.debug(" -> creating folder \"" + command.data.title + "\"");
|
||||
newId = this._bms.createFolder(parentId,
|
||||
command.data.title,
|
||||
command.data.index);
|
||||
break;
|
||||
case "livemark":
|
||||
this._log.debug(" -> creating livemark \"" + command.data.title + "\"");
|
||||
newId = this._ls.createLivemark(parentId,
|
||||
command.data.title,
|
||||
Utils.makeURI(command.data.siteURI),
|
||||
Utils.makeURI(command.data.feedURI),
|
||||
command.data.index);
|
||||
break;
|
||||
case "mounted-share":
|
||||
this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\"");
|
||||
newId = this._bms.createFolder(parentId,
|
||||
command.data.title,
|
||||
command.data.index);
|
||||
|
||||
this._ans.setItemAnnotation(newId, "weave/mounted-share-id",
|
||||
command.data.mountId, 0, this._ans.EXPIRE_NEVER);
|
||||
break;
|
||||
case "separator":
|
||||
this._log.debug(" -> 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) {
|
||||
if (command.GUID == "menu" ||
|
||||
command.GUID == "toolbar" ||
|
||||
command.GUID == "unfiled") {
|
||||
this._log.warn("Attempted to remove root node (" + command.GUID +
|
||||
"). Skipping command.");
|
||||
return;
|
||||
}
|
||||
|
||||
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.debug(" -> removing bookmark " + command.GUID);
|
||||
this._bms.removeItem(itemId);
|
||||
break;
|
||||
case this._bms.TYPE_FOLDER:
|
||||
this._log.debug(" -> removing folder " + command.GUID);
|
||||
this._bms.removeFolder(itemId);
|
||||
break;
|
||||
case this._bms.TYPE_SEPARATOR:
|
||||
this._log.debug(" -> removing separator " + command.GUID);
|
||||
this._bms.removeItem(itemId);
|
||||
break;
|
||||
default:
|
||||
this._log.error("removeCommand: Unknown item type: " + type);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_editCommand: function BStore__editCommand(command) {
|
||||
if (command.GUID == "menu" ||
|
||||
command.GUID == "toolbar" ||
|
||||
command.GUID == "unfiled") {
|
||||
this._log.warn("Attempted to edit root node (" + command.GUID +
|
||||
"). Skipping command.");
|
||||
return;
|
||||
}
|
||||
|
||||
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 "type":
|
||||
// all commands have this to help in reconciliation, but it makes
|
||||
// no sense to edit it
|
||||
break;
|
||||
case "GUID":
|
||||
var existing = this._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, Utils.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._getItemIdForGUID(command.data.parentGUID), index);
|
||||
} break;
|
||||
case "tags": {
|
||||
let tagsURI = this._bms.getBookmarkURI(itemId);
|
||||
this._ts.untagURI(tagsURI, null);
|
||||
this._ts.tagURI(tagsURI, command.data.tags);
|
||||
} break;
|
||||
case "keyword":
|
||||
this._bms.setKeywordForBookmark(itemId, command.data.keyword);
|
||||
break;
|
||||
case "description":
|
||||
if (command.data.description) {
|
||||
this._ans.setItemAnnotation(itemId, "bookmarkProperties/description",
|
||||
command.data.description, 0,
|
||||
this._ans.EXPIRE_NEVER);
|
||||
}
|
||||
break;
|
||||
case "generatorURI": {
|
||||
let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId));
|
||||
let genURI = Utils.makeURI(command.data.generatorURI);
|
||||
let micsum = this._ms.createMicrosummary(micsumURI, genURI);
|
||||
this._ms.setMicrosummary(itemId, micsum);
|
||||
} break;
|
||||
case "siteURI":
|
||||
this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI));
|
||||
break;
|
||||
case "feedURI":
|
||||
this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI));
|
||||
break;
|
||||
default:
|
||||
this._log.warn("Can't change item property: " + key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getNode: function BSS__getNode(folder) {
|
||||
let query = this._hsvc.getNewQuery();
|
||||
query.setFolders([folder], 1);
|
||||
return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root;
|
||||
},
|
||||
|
||||
__wrap: function BSS___wrap(node, items, parentGUID, index, guidOverride) {
|
||||
let GUID, item;
|
||||
|
||||
// we override the guid for the root items, "menu", "toolbar", etc.
|
||||
if (guidOverride) {
|
||||
GUID = guidOverride;
|
||||
item = {};
|
||||
} else {
|
||||
GUID = this._bms.getItemGUID(node.itemId);
|
||||
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 if (this._ans.itemHasAnnotation(node.itemId,
|
||||
"weave/mounted-share-id")) {
|
||||
item.type = "mounted-share";
|
||||
item.title = node.title;
|
||||
item.mountId = this._ans.getItemAnnotation(node.itemId,
|
||||
"weave/mounted-share-id");
|
||||
|
||||
} else {
|
||||
item.type = "folder";
|
||||
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
node.containerOpen = true;
|
||||
for (var i = 0; i < node.childCount; i++) {
|
||||
this.__wrap(node.getChild(i), items, GUID, i);
|
||||
}
|
||||
}
|
||||
if (!guidOverride)
|
||||
item.title = node.title; // no titles for root nodes
|
||||
|
||||
} 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
|
||||
item.staticTitle = this._ans.getItemAnnotation(node.itemId, "bookmarks/staticTitle");
|
||||
} else if (node.type == node.RESULT_TYPE_QUERY) {
|
||||
item.type = "query";
|
||||
item.title = node.title;
|
||||
} else {
|
||||
item.type = "bookmark";
|
||||
item.title = node.title;
|
||||
}
|
||||
|
||||
try {
|
||||
item.description =
|
||||
this._ans.getItemAnnotation(node.itemId, "bookmarkProperties/description");
|
||||
} catch (e) {
|
||||
item.description = undefined;
|
||||
}
|
||||
|
||||
item.URI = node.uri;
|
||||
|
||||
// This will throw if makeURI can't make an nsIURI object out of the
|
||||
// node.uri string (or return null if node.uri is null), in which case
|
||||
// we won't be able to get tags for the bookmark (but we'll still sync
|
||||
// the rest of the record).
|
||||
let uri;
|
||||
try {
|
||||
uri = Utils.makeURI(node.uri);
|
||||
}
|
||||
catch(e) {
|
||||
this._log.error("error parsing URI string <" + node.uri + "> " +
|
||||
"for item " + node.itemId + " (" + node.title + "): " +
|
||||
e);
|
||||
}
|
||||
|
||||
if (uri)
|
||||
item.tags = this._ts.getTagsForURI(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;
|
||||
},
|
||||
|
||||
// helper
|
||||
_wrap: function BStore__wrap(node, items, rootName) {
|
||||
return this.__wrap(node, items, null, null, rootName);
|
||||
},
|
||||
|
||||
_wrapMount: function BStore__wrapMount(node, id) {
|
||||
if (node.type != node.RESULT_TYPE_FOLDER)
|
||||
throw "Trying to wrap a non-folder mounted share";
|
||||
|
||||
let GUID = this._bms.getItemGUID(node.itemId);
|
||||
let ret = {rootGUID: GUID, userid: id, snapshot: {}};
|
||||
|
||||
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
node.containerOpen = true;
|
||||
for (var i = 0; i < node.childCount; i++) {
|
||||
this.__wrap(node.getChild(i), ret.snapshot, GUID, i);
|
||||
}
|
||||
|
||||
// remove any share mountpoints
|
||||
for (let guid in ret.snapshot) {
|
||||
if (ret.snapshot[guid].type == "mounted-share")
|
||||
delete ret.snapshot[guid];
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
_resetGUIDs: function BSS__resetGUIDs(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._resetGUIDs(node.getChild(i));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
findMounts: function BStore_findMounts() {
|
||||
let ret = [];
|
||||
let a = this._ans.getItemsWithAnnotation("weave/mounted-share-id", {});
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
let id = this._ans.getItemAnnotation(a[i], "weave/mounted-share-id");
|
||||
ret.push(this._wrapMount(this._getNode(a[i]), id));
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
wrap: function BStore_wrap() {
|
||||
var items = {};
|
||||
this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu");
|
||||
this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar");
|
||||
this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled");
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function BStore_wipe() {
|
||||
this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder);
|
||||
this._bms.removeFolderChildren(this._bms.toolbarFolder);
|
||||
this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder);
|
||||
},
|
||||
|
||||
resetGUIDs: function BStore_resetGUIDs() {
|
||||
this._resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder));
|
||||
this._resetGUIDs(this._getNode(this._bms.toolbarFolder));
|
||||
this._resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder));
|
||||
}
|
||||
};
|
||||
BookmarksStore.prototype.__proto__ = new Store();
|
||||
|
||||
/*
|
||||
* Tracker objects for each engine may need to subclass the
|
||||
* getScore routine, which returns the current 'score' for that
|
||||
* engine. How the engine decides to set the score is upto it,
|
||||
* as long as the value between 0 and 100 actually corresponds
|
||||
* to its urgency to sync.
|
||||
*
|
||||
* Here's an example BookmarksTracker. We don't subclass getScore
|
||||
* because the observer methods take care of updating _score which
|
||||
* getScore returns by default.
|
||||
*/
|
||||
function BookmarksTracker() {
|
||||
this._init();
|
||||
}
|
||||
BookmarksTracker.prototype = {
|
||||
_logName: "BMTracker",
|
||||
|
||||
/* We don't care about the first three */
|
||||
onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {
|
||||
|
||||
},
|
||||
onEndUpdateBatch: function BMT_onEndUpdateBatch() {
|
||||
|
||||
},
|
||||
onItemVisited: function BMT_onItemVisited() {
|
||||
|
||||
},
|
||||
|
||||
/* Every add or remove is worth 4 points,
|
||||
* on the basis that adding or removing 20 bookmarks
|
||||
* means its time to sync?
|
||||
*/
|
||||
onItemAdded: function BMT_onEndUpdateBatch() {
|
||||
this._score += 4;
|
||||
},
|
||||
onItemRemoved: function BMT_onItemRemoved() {
|
||||
this._score += 4;
|
||||
},
|
||||
/* Changes are worth 2 points? */
|
||||
onItemChanged: function BMT_onItemChanged() {
|
||||
this._score += 2;
|
||||
},
|
||||
|
||||
_init: function BMT__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
|
||||
Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService).
|
||||
addObserver(this, false);
|
||||
}
|
||||
}
|
||||
BookmarksTracker.prototype.__proto__ = new Tracker();
|
|
@ -0,0 +1,329 @@
|
|||
const EXPORTED_SYMBOLS = ['CookieEngine', 'CookieTracker', 'CookieStore'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/engines.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
Cu.import("resource://weave/trackers.js");
|
||||
|
||||
function CookieEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
CookieEngine.prototype = {
|
||||
get name() { return "cookies"; },
|
||||
get logName() { return "CookieEngine"; },
|
||||
get serverPrefix() { return "user-data/cookies/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new CookieSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new CookieStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new CookieTracker();
|
||||
return this.__tracker;
|
||||
}
|
||||
};
|
||||
CookieEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function CookieSyncCore() {
|
||||
this._init();
|
||||
}
|
||||
CookieSyncCore.prototype = {
|
||||
_logName: "CookieSync",
|
||||
|
||||
__cookieManager: null,
|
||||
get _cookieManager() {
|
||||
if (!this.__cookieManager)
|
||||
this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"].
|
||||
getService(Ci.nsICookieManager2);
|
||||
/* need the 2nd revision of the ICookieManager interface
|
||||
because it supports add() and the 1st one doesn't. */
|
||||
return this.__cookieManager;
|
||||
},
|
||||
|
||||
|
||||
_itemExists: function CSC__itemExists(GUID) {
|
||||
/* true if a cookie with the given GUID exists.
|
||||
The GUID that we are passed should correspond to the keys
|
||||
that we define in the JSON returned by CookieStore.wrap()
|
||||
That is, it will be a string of the form
|
||||
"host:path:name". */
|
||||
|
||||
/* TODO verify that colons can't normally appear in any of
|
||||
the fields -- if they did it then we can't rely on .split(":")
|
||||
to parse correctly.*/
|
||||
|
||||
let cookieArray = GUID.split( ":" );
|
||||
let cookieHost = cookieArray[0];
|
||||
let cookiePath = cookieArray[1];
|
||||
let cookieName = cookieArray[2];
|
||||
|
||||
/* alternate implementation would be to instantiate a cookie from
|
||||
cookieHost, cookiePath, and cookieName, then call
|
||||
cookieManager.cookieExists(). Maybe that would have better
|
||||
performance? This implementation seems pretty slow.*/
|
||||
let enumerator = this._cookieManager.enumerator;
|
||||
while (enumerator.hasMoreElements())
|
||||
{
|
||||
let aCookie = enumerator.getNext();
|
||||
if (aCookie.host == cookieHost &&
|
||||
aCookie.path == cookiePath &&
|
||||
aCookie.name == cookieName ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
/* Note: We can't just call cookieManager.cookieExists() with a generic
|
||||
javascript object with .host, .path, and .name attributes attatched.
|
||||
cookieExists is implemented in C and does a hard static_cast to an
|
||||
nsCookie object, so duck typing doesn't work (and in fact makes
|
||||
Firefox hard-crash as the static_cast returns null and is not checked.)
|
||||
*/
|
||||
},
|
||||
|
||||
_commandLike: function CSC_commandLike(a, b) {
|
||||
/* Method required to be overridden.
|
||||
a and b each have a .data and a .GUID
|
||||
If this function returns true, an editCommand will be
|
||||
generated to try to resolve the thing.
|
||||
but are a and b objects of the type in the Store or
|
||||
are they "commands"?? */
|
||||
return false;
|
||||
}
|
||||
};
|
||||
CookieSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
function CookieStore( cookieManagerStub ) {
|
||||
/* If no argument is passed in, this store will query/write to the real
|
||||
Mozilla cookie manager component. This is the normal way to use this
|
||||
class in production code. But for unit-testing purposes, you can pass
|
||||
in a stub object that will be used in place of the cookieManager. */
|
||||
this._init();
|
||||
this._cookieManagerStub = cookieManagerStub;
|
||||
}
|
||||
CookieStore.prototype = {
|
||||
_logName: "CookieStore",
|
||||
|
||||
|
||||
// Documentation of the nsICookie interface says:
|
||||
// name ACString The name of the cookie. Read only.
|
||||
// value ACString The cookie value. Read only.
|
||||
// isDomain boolean True if the cookie is a domain cookie, false otherwise. Read only.
|
||||
// host AUTF8String The host (possibly fully qualified) of the cookie. Read only.
|
||||
// path AUTF8String The path pertaining to the cookie. Read only.
|
||||
// isSecure boolean True if the cookie was transmitted over ssl, false otherwise. Read only.
|
||||
// expires PRUint64 Expiration time (local timezone) expressed as number of seconds since Jan 1, 1970. Read only.
|
||||
// status nsCookieStatus Holds the P3P status of cookie. Read only.
|
||||
// policy nsCookiePolicy Holds the site's compact policy value. Read only.
|
||||
// nsICookie2 deprecates expires, status, and policy, and adds:
|
||||
//rawHost AUTF8String The host (possibly fully qualified) of the cookie without a leading dot to represent if it is a domain cookie. Read only.
|
||||
//isSession boolean True if the cookie is a session cookie. Read only.
|
||||
//expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only.
|
||||
//isHttpOnly boolean True if the cookie is an http only cookie. Read only.
|
||||
|
||||
__cookieManager: null,
|
||||
get _cookieManager() {
|
||||
if ( this._cookieManagerStub != undefined ) {
|
||||
return this._cookieManagerStub;
|
||||
}
|
||||
// otherwise, use the real one
|
||||
if (!this.__cookieManager)
|
||||
this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"].
|
||||
getService(Ci.nsICookieManager2);
|
||||
// need the 2nd revision of the ICookieManager interface
|
||||
// because it supports add() and the 1st one doesn't.
|
||||
return this.__cookieManager;
|
||||
},
|
||||
|
||||
_createCommand: function CookieStore__createCommand(command) {
|
||||
/* we got a command to create a cookie in the local browser
|
||||
in order to sync with the server. */
|
||||
|
||||
this._log.info("CookieStore got createCommand: " + command );
|
||||
// this assumes command.data fits the nsICookie2 interface
|
||||
if ( !command.data.isSession ) {
|
||||
// Add only persistent cookies ( not session cookies )
|
||||
this._cookieManager.add( command.data.host,
|
||||
command.data.path,
|
||||
command.data.name,
|
||||
command.data.value,
|
||||
command.data.isSecure,
|
||||
command.data.isHttpOnly,
|
||||
command.data.isSession,
|
||||
command.data.expiry );
|
||||
}
|
||||
},
|
||||
|
||||
_removeCommand: function CookieStore__removeCommand(command) {
|
||||
/* we got a command to remove a cookie from the local browser
|
||||
in order to sync with the server.
|
||||
command.data appears to be equivalent to what wrap() puts in
|
||||
the JSON dictionary. */
|
||||
|
||||
this._log.info("CookieStore got removeCommand: " + command );
|
||||
|
||||
/* I think it goes like this, according to
|
||||
http://developer.mozilla.org/en/docs/nsICookieManager
|
||||
the last argument is "always block cookies from this domain?"
|
||||
and the answer is "no". */
|
||||
this._cookieManager.remove( command.data.host,
|
||||
command.data.name,
|
||||
command.data.path,
|
||||
false );
|
||||
},
|
||||
|
||||
_editCommand: function CookieStore__editCommand(command) {
|
||||
/* we got a command to change a cookie in the local browser
|
||||
in order to sync with the server. */
|
||||
this._log.info("CookieStore got editCommand: " + command );
|
||||
|
||||
/* Look up the cookie that matches the one in the command: */
|
||||
var iter = this._cookieManager.enumerator;
|
||||
var matchingCookie = null;
|
||||
while (iter.hasMoreElements()){
|
||||
let cookie = iter.getNext();
|
||||
if (cookie.QueryInterface( Ci.nsICookie ) ){
|
||||
// see if host:path:name of cookie matches GUID given in command
|
||||
let key = cookie.host + ":" + cookie.path + ":" + cookie.name;
|
||||
if (key == command.GUID) {
|
||||
matchingCookie = cookie;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update values in the cookie:
|
||||
for (var key in command.data) {
|
||||
// Whatever values command.data has, use them
|
||||
matchingCookie[ key ] = command.data[ key ];
|
||||
}
|
||||
// Remove the old incorrect cookie from the manager:
|
||||
this._cookieManager.remove( matchingCookie.host,
|
||||
matchingCookie.name,
|
||||
matchingCookie.path,
|
||||
false );
|
||||
|
||||
// Re-add the new updated cookie:
|
||||
if ( !command.data.isSession ) {
|
||||
/* ignore single-session cookies, add only persistent cookies. */
|
||||
this._cookieManager.add( matchingCookie.host,
|
||||
matchingCookie.path,
|
||||
matchingCookie.name,
|
||||
matchingCookie.value,
|
||||
matchingCookie.isSecure,
|
||||
matchingCookie.isHttpOnly,
|
||||
matchingCookie.isSession,
|
||||
matchingCookie.expiry );
|
||||
}
|
||||
|
||||
// Also, there's an exception raised because
|
||||
// this._data[comand.GUID] is undefined
|
||||
},
|
||||
|
||||
wrap: function CookieStore_wrap() {
|
||||
/* Return contents of this store, as JSON.
|
||||
A dictionary of cookies where the keys are GUIDs and the
|
||||
values are sub-dictionaries containing all cookie fields. */
|
||||
|
||||
let items = {};
|
||||
var iter = this._cookieManager.enumerator;
|
||||
while (iter.hasMoreElements()){
|
||||
var cookie = iter.getNext();
|
||||
if (cookie.QueryInterface( Ci.nsICookie )){
|
||||
// String used to identify cookies is
|
||||
// host:path:name
|
||||
if ( cookie.isSession ) {
|
||||
/* Skip session-only cookies, sync only persistent cookies. */
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = cookie.host + ":" + cookie.path + ":" + cookie.name;
|
||||
items[ key ] = { parentGUID: '',
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
isDomain: cookie.isDomain,
|
||||
host: cookie.host,
|
||||
path: cookie.path,
|
||||
isSecure: cookie.isSecure,
|
||||
// nsICookie2 values:
|
||||
rawHost: cookie.rawHost,
|
||||
isSession: cookie.isSession,
|
||||
expiry: cookie.expiry,
|
||||
isHttpOnly: cookie.isHttpOnly };
|
||||
|
||||
/* See http://developer.mozilla.org/en/docs/nsICookie
|
||||
Note: not syncing "expires", "status", or "policy"
|
||||
since they're deprecated. */
|
||||
|
||||
}
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function CookieStore_wipe() {
|
||||
/* Remove everything from the store. Return nothing.
|
||||
TODO are the semantics of this just wiping out an internal
|
||||
buffer, or am I supposed to wipe out all cookies from
|
||||
the browser itself for reals? */
|
||||
this._cookieManager.removeAll();
|
||||
},
|
||||
|
||||
resetGUIDs: function CookieStore_resetGUIDs() {
|
||||
/* called in the case where remote/local sync GUIDs do not
|
||||
match. We do need to override this, but since we're deriving
|
||||
GUIDs from the cookie data itself and not generating them,
|
||||
there's basically no way they can get "out of sync" so there's
|
||||
nothing to do here. */
|
||||
}
|
||||
};
|
||||
CookieStore.prototype.__proto__ = new Store();
|
||||
|
||||
function CookieTracker() {
|
||||
this._init();
|
||||
}
|
||||
CookieTracker.prototype = {
|
||||
_logName: "CookieTracker",
|
||||
|
||||
_init: function CT__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
/* cookieService can't register observers, but what we CAN do is
|
||||
register a general observer with the global observerService
|
||||
to watch for the 'cookie-changed' message. */
|
||||
let observerService = Cc["@mozilla.org/observer-service;1"].
|
||||
getService(Ci.nsIObserverService);
|
||||
observerService.addObserver( this, 'cookie-changed', false );
|
||||
},
|
||||
|
||||
// implement observe method to satisfy nsIObserver interface
|
||||
observe: function ( aSubject, aTopic, aData ) {
|
||||
/* This gets called when any cookie is added, changed, or removed.
|
||||
aData will contain a string "added", "changed", etc. to tell us which,
|
||||
but for now we can treat them all the same. aSubject is the new
|
||||
cookie object itself. */
|
||||
var newCookie = aSubject.QueryInterface( Ci.nsICookie2 );
|
||||
if ( newCookie ) {
|
||||
if ( !newCookie.isSession ) {
|
||||
/* Any modification to a persistent cookie is worth
|
||||
10 points out of 100. Ignore session cookies. */
|
||||
this._score += 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CookieTracker.prototype.__proto__ = new Tracker();
|
|
@ -0,0 +1,225 @@
|
|||
const EXPORTED_SYMBOLS = ['FormEngine'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
Cu.import("resource://weave/engines.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
Cu.import("resource://weave/trackers.js");
|
||||
|
||||
function FormEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
FormEngine.prototype = {
|
||||
get name() { return "forms"; },
|
||||
get logName() { return "FormEngine"; },
|
||||
get serverPrefix() { return "user-data/forms/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new FormSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new FormStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new FormsTracker();
|
||||
return this.__tracker;
|
||||
}
|
||||
};
|
||||
FormEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function FormSyncCore() {
|
||||
this._init();
|
||||
}
|
||||
FormSyncCore.prototype = {
|
||||
_logName: "FormSync",
|
||||
|
||||
__formDB: null,
|
||||
get _formDB() {
|
||||
if (!this.__formDB) {
|
||||
var file = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties).
|
||||
get("ProfD", Ci.nsIFile);
|
||||
file.append("formhistory.sqlite");
|
||||
var stor = Cc["@mozilla.org/storage/service;1"].
|
||||
getService(Ci.mozIStorageService);
|
||||
this.__formDB = stor.openDatabase(file);
|
||||
}
|
||||
return this.__formDB;
|
||||
},
|
||||
|
||||
_itemExists: function FSC__itemExists(GUID) {
|
||||
var found = false;
|
||||
var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory");
|
||||
|
||||
/* Same performance restrictions as PasswordSyncCore apply here:
|
||||
caching required */
|
||||
while (stmnt.executeStep()) {
|
||||
var nam = stmnt.getUTF8String(1);
|
||||
var val = stmnt.getUTF8String(2);
|
||||
var key = Utils.sha1(nam + val);
|
||||
|
||||
if (key == GUID)
|
||||
found = true;
|
||||
}
|
||||
|
||||
return found;
|
||||
},
|
||||
|
||||
_commandLike: function FSC_commandLike(a, b) {
|
||||
/* Not required as GUIDs for similar data sets will be the same */
|
||||
return false;
|
||||
}
|
||||
};
|
||||
FormSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
function FormStore() {
|
||||
this._init();
|
||||
}
|
||||
FormStore.prototype = {
|
||||
_logName: "FormStore",
|
||||
|
||||
__formDB: null,
|
||||
get _formDB() {
|
||||
if (!this.__formDB) {
|
||||
var file = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties).
|
||||
get("ProfD", Ci.nsIFile);
|
||||
file.append("formhistory.sqlite");
|
||||
var stor = Cc["@mozilla.org/storage/service;1"].
|
||||
getService(Ci.mozIStorageService);
|
||||
this.__formDB = stor.openDatabase(file);
|
||||
}
|
||||
return this.__formDB;
|
||||
},
|
||||
|
||||
__formHistory: null,
|
||||
get _formHistory() {
|
||||
if (!this.__formHistory)
|
||||
this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"].
|
||||
getService(Ci.nsIFormHistory2);
|
||||
return this.__formHistory;
|
||||
},
|
||||
|
||||
_createCommand: function FormStore__createCommand(command) {
|
||||
this._log.info("FormStore got createCommand: " + command );
|
||||
this._formHistory.addEntry(command.data.name, command.data.value);
|
||||
},
|
||||
|
||||
_removeCommand: function FormStore__removeCommand(command) {
|
||||
this._log.info("FormStore got removeCommand: " + command );
|
||||
this._formHistory.removeEntry(command.data.name, command.data.value);
|
||||
},
|
||||
|
||||
_editCommand: function FormStore__editCommand(command) {
|
||||
this._log.info("FormStore got editCommand: " + command );
|
||||
this._log.warn("Form syncs are expected to only be create/remove!");
|
||||
},
|
||||
|
||||
wrap: function FormStore_wrap() {
|
||||
var items = [];
|
||||
var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory");
|
||||
|
||||
while (stmnt.executeStep()) {
|
||||
var nam = stmnt.getUTF8String(1);
|
||||
var val = stmnt.getUTF8String(2);
|
||||
var key = Utils.sha1(nam + val);
|
||||
|
||||
items[key] = { name: nam, value: val };
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function FormStore_wipe() {
|
||||
this._formHistory.removeAllEntries();
|
||||
},
|
||||
|
||||
resetGUIDs: function FormStore_resetGUIDs() {
|
||||
// Not needed.
|
||||
}
|
||||
};
|
||||
FormStore.prototype.__proto__ = new Store();
|
||||
|
||||
function FormsTracker() {
|
||||
this._init();
|
||||
}
|
||||
FormsTracker.prototype = {
|
||||
_logName: "FormsTracker",
|
||||
|
||||
__formDB: null,
|
||||
get _formDB() {
|
||||
if (!this.__formDB) {
|
||||
var file = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties).
|
||||
get("ProfD", Ci.nsIFile);
|
||||
file.append("formhistory.sqlite");
|
||||
var stor = Cc["@mozilla.org/storage/service;1"].
|
||||
getService(Ci.mozIStorageService);
|
||||
this.__formDB = stor.openDatabase(file);
|
||||
}
|
||||
|
||||
return this.__formDB;
|
||||
},
|
||||
|
||||
/* nsIFormSubmitObserver is not available in JS.
|
||||
* To calculate scores, we instead just count the changes in
|
||||
* the database since the last time we were asked.
|
||||
*
|
||||
* FIXME!: Buggy, because changes in a row doesn't result in
|
||||
* an increment of our score. A possible fix is to do a
|
||||
* SELECT for each fieldname and compare those instead of the
|
||||
* whole row count.
|
||||
*
|
||||
* Each change is worth 2 points. At some point, we may
|
||||
* want to differentiate between search-history rows and other
|
||||
* form items, and assign different scores.
|
||||
*/
|
||||
_rowCount: 0,
|
||||
get score() {
|
||||
var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory");
|
||||
stmnt.executeStep();
|
||||
var count = stmnt.getInt32(0);
|
||||
stmnt.reset();
|
||||
|
||||
this._score = Math.abs(this._rowCount - count) * 2;
|
||||
|
||||
if (this._score >= 100)
|
||||
return 100;
|
||||
else
|
||||
return this._score;
|
||||
},
|
||||
|
||||
resetScore: function FormsTracker_resetScore() {
|
||||
var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory");
|
||||
stmnt.executeStep();
|
||||
this._rowCount = stmnt.getInt32(0);
|
||||
stmnt.reset();
|
||||
this._score = 0;
|
||||
},
|
||||
|
||||
_init: function FormsTracker__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
|
||||
var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory");
|
||||
stmnt.executeStep();
|
||||
this._rowCount = stmnt.getInt32(0);
|
||||
stmnt.reset();
|
||||
}
|
||||
}
|
||||
FormsTracker.prototype.__proto__ = new Tracker();
|
|
@ -0,0 +1,193 @@
|
|||
const EXPORTED_SYMBOLS = ['HistoryEngine'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://weave/log4moz.js");
|
||||
Cu.import("resource://weave/util.js");
|
||||
Cu.import("resource://weave/engines.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
Cu.import("resource://weave/trackers.js");
|
||||
|
||||
function HistoryEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
HistoryEngine.prototype = {
|
||||
get name() { return "history"; },
|
||||
get logName() { return "HistEngine"; },
|
||||
get serverPrefix() { return "user-data/history/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new HistorySyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new HistoryStore();
|
||||
return this.__store;
|
||||
},
|
||||
|
||||
__tracker: null,
|
||||
get _tracker() {
|
||||
if (!this.__tracker)
|
||||
this.__tracker = new HistoryTracker();
|
||||
return this.__tracker;
|
||||
}
|
||||
};
|
||||
HistoryEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function HistorySyncCore() {
|
||||
this._init();
|
||||
}
|
||||
HistorySyncCore.prototype = {
|
||||
_logName: "HistSync",
|
||||
|
||||
_itemExists: function HSC__itemExists(GUID) {
|
||||
// we don't care about already-existing items; just try to re-add them
|
||||
return false;
|
||||
},
|
||||
|
||||
_commandLike: function HSC_commandLike(a, b) {
|
||||
// History commands never qualify for likeness. We will always
|
||||
// take the union of all client/server items. We use the URL as
|
||||
// the GUID, so the same sites will map to the same item (same
|
||||
// GUID), without our intervention.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
HistorySyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
function HistoryStore() {
|
||||
this._init();
|
||||
}
|
||||
HistoryStore.prototype = {
|
||||
_logName: "HistStore",
|
||||
|
||||
__hsvc: null,
|
||||
get _hsvc() {
|
||||
if (!this.__hsvc) {
|
||||
this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
this.__hsvc.QueryInterface(Ci.nsIGlobalHistory2);
|
||||
this.__hsvc.QueryInterface(Ci.nsIBrowserHistory);
|
||||
}
|
||||
return this.__hsvc;
|
||||
},
|
||||
|
||||
_createCommand: function HistStore__createCommand(command) {
|
||||
this._log.debug(" -> creating history entry: " + command.GUID);
|
||||
try {
|
||||
let uri = Utils.makeURI(command.data.URI);
|
||||
this._hsvc.addVisit(uri, command.data.time, null,
|
||||
this._hsvc.TRANSITION_TYPED, false, null);
|
||||
this._hsvc.setPageTitle(uri, command.data.title);
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + (e.message? e.message : e));
|
||||
}
|
||||
},
|
||||
|
||||
_removeCommand: function HistStore__removeCommand(command) {
|
||||
this._log.trace(" -> NOT removing history entry: " + command.GUID);
|
||||
// we can't remove because we only sync the last 1000 items, not
|
||||
// the whole store. So we don't know if remove commands were
|
||||
// generated due to the user removing an entry or because it
|
||||
// dropped past the 1000 item mark.
|
||||
},
|
||||
|
||||
_editCommand: function HistStore__editCommand(command) {
|
||||
this._log.trace(" -> FIXME: NOT editing history entry: " + command.GUID);
|
||||
// FIXME: implement!
|
||||
},
|
||||
|
||||
_historyRoot: function HistStore__historyRoot() {
|
||||
let query = this._hsvc.getNewQuery(),
|
||||
options = this._hsvc.getNewQueryOptions();
|
||||
|
||||
query.minVisits = 1;
|
||||
options.maxResults = 1000;
|
||||
options.resultType = options.RESULTS_AS_VISIT; // FULL_VISIT does not work
|
||||
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
|
||||
options.queryType = options.QUERY_TYPE_HISTORY;
|
||||
|
||||
let root = this._hsvc.executeQuery(query, options).root;
|
||||
root.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
return root;
|
||||
},
|
||||
|
||||
wrap: function HistStore_wrap() {
|
||||
let root = this._historyRoot();
|
||||
root.containerOpen = true;
|
||||
let items = {};
|
||||
for (let i = 0; i < root.childCount; i++) {
|
||||
let item = root.getChild(i);
|
||||
let guid = item.time + ":" + item.uri
|
||||
items[guid] = {parentGUID: '',
|
||||
title: item.title,
|
||||
URI: item.uri,
|
||||
time: item.time
|
||||
};
|
||||
// FIXME: sync transition type - requires FULL_VISITs
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function HistStore_wipe() {
|
||||
this._hsvc.removeAllPages();
|
||||
}
|
||||
};
|
||||
HistoryStore.prototype.__proto__ = new Store();
|
||||
|
||||
function HistoryTracker() {
|
||||
this._init();
|
||||
}
|
||||
HistoryTracker.prototype = {
|
||||
_logName: "HistoryTracker",
|
||||
|
||||
/* We don't care about the first four */
|
||||
onBeginUpdateBatch: function HT_onBeginUpdateBatch() {
|
||||
|
||||
},
|
||||
onEndUpdateBatch: function HT_onEndUpdateBatch() {
|
||||
|
||||
},
|
||||
onPageChanged: function HT_onPageChanged() {
|
||||
|
||||
},
|
||||
onTitleChanged: function HT_onTitleChanged() {
|
||||
|
||||
},
|
||||
|
||||
/* Every add or remove is worth 1 point.
|
||||
* Clearing the whole history is worth 50 points,
|
||||
* to ensure we're above the cutoff for syncing
|
||||
* ASAP.
|
||||
*/
|
||||
onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) {
|
||||
this._score += 1;
|
||||
},
|
||||
onPageExpired: function HT_onPageExpired(uri, time, entry) {
|
||||
this._score += 1;
|
||||
},
|
||||
onDeleteURI: function HT_onDeleteURI(uri) {
|
||||
this._score += 1;
|
||||
},
|
||||
onClearHistory: function HT_onClearHistory() {
|
||||
this._score += 50;
|
||||
},
|
||||
|
||||
_init: function HT__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
|
||||
Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService).
|
||||
addObserver(this, false);
|
||||
}
|
||||
}
|
||||
HistoryTracker.prototype.__proto__ = new Tracker();
|
|
@ -0,0 +1,183 @@
|
|||
const EXPORTED_SYMBOLS = ['PasswordEngine'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://weave/util.js");
|
||||
Cu.import("resource://weave/engines.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
|
||||
/*
|
||||
* _hashLoginInfo
|
||||
*
|
||||
* nsILoginInfo objects don't have a unique GUID, so we need to generate one
|
||||
* on the fly. This is done by taking a hash of every field in the object.
|
||||
* Note that the resulting GUID could potentiually reveal passwords via
|
||||
* dictionary attacks or brute force. But GUIDs shouldn't be obtainable by
|
||||
* anyone, so this should generally be safe.
|
||||
*/
|
||||
function _hashLoginInfo(aLogin) {
|
||||
var loginKey = aLogin.hostname + ":" +
|
||||
aLogin.formSubmitURL + ":" +
|
||||
aLogin.httpRealm + ":" +
|
||||
aLogin.username + ":" +
|
||||
aLogin.password + ":" +
|
||||
aLogin.usernameField + ":" +
|
||||
aLogin.passwordField;
|
||||
|
||||
return Utils.sha1(loginKey);
|
||||
}
|
||||
|
||||
function PasswordEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
PasswordEngine.prototype = {
|
||||
get name() { return "passwords"; },
|
||||
get logName() { return "PasswordEngine"; },
|
||||
get serverPrefix() { return "user-data/passwords/"; },
|
||||
|
||||
__core: null,
|
||||
get _core() {
|
||||
if (!this.__core)
|
||||
this.__core = new PasswordSyncCore();
|
||||
return this.__core;
|
||||
},
|
||||
|
||||
__store: null,
|
||||
get _store() {
|
||||
if (!this.__store)
|
||||
this.__store = new PasswordStore();
|
||||
return this.__store;
|
||||
}
|
||||
};
|
||||
PasswordEngine.prototype.__proto__ = new Engine();
|
||||
|
||||
function PasswordSyncCore() {
|
||||
this._init();
|
||||
}
|
||||
PasswordSyncCore.prototype = {
|
||||
_logName: "PasswordSync",
|
||||
|
||||
__loginManager : null,
|
||||
get _loginManager() {
|
||||
if (!this.__loginManager)
|
||||
this.__loginManager = Cc["@mozilla.org/login-manager;1"].
|
||||
getService(Ci.nsILoginManager);
|
||||
return this.__loginManager;
|
||||
},
|
||||
|
||||
_itemExists: function PSC__itemExists(GUID) {
|
||||
var found = false;
|
||||
var logins = this._loginManager.getAllLogins({});
|
||||
|
||||
// XXX It would be more efficient to compute all the hashes in one shot,
|
||||
// cache the results, and check the cache here. That would need to happen
|
||||
// once per sync -- not sure how to invalidate cache after current sync?
|
||||
for (var i = 0; i < logins.length && !found; i++) {
|
||||
var hash = _hashLoginInfo(logins[i]);
|
||||
if (hash == GUID)
|
||||
found = true;;
|
||||
}
|
||||
|
||||
return found;
|
||||
},
|
||||
|
||||
_commandLike: function PSC_commandLike(a, b) {
|
||||
// Not used.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
PasswordSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
function PasswordStore() {
|
||||
this._init();
|
||||
}
|
||||
PasswordStore.prototype = {
|
||||
_logName: "PasswordStore",
|
||||
|
||||
__loginManager : null,
|
||||
get _loginManager() {
|
||||
if (!this.__loginManager)
|
||||
this.__loginManager = Cc["@mozilla.org/login-manager;1"].
|
||||
getService(Ci.nsILoginManager);
|
||||
return this.__loginManager;
|
||||
},
|
||||
|
||||
__nsLoginInfo : null,
|
||||
get _nsLoginInfo() {
|
||||
if (!this.__nsLoginInfo)
|
||||
this.__nsLoginInfo = new Components.Constructor(
|
||||
"@mozilla.org/login-manager/loginInfo;1",
|
||||
Ci.nsILoginInfo, "init");
|
||||
return this.__nsLoginInfo;
|
||||
},
|
||||
|
||||
|
||||
_createCommand: function PasswordStore__createCommand(command) {
|
||||
this._log.info("PasswordStore got createCommand: " + command );
|
||||
|
||||
var login = new this._nsLoginInfo(command.data.hostname,
|
||||
command.data.formSubmitURL,
|
||||
command.data.httpRealm,
|
||||
command.data.username,
|
||||
command.data.password,
|
||||
command.data.usernameField,
|
||||
command.data.passwordField);
|
||||
|
||||
this._loginManager.addLogin(login);
|
||||
},
|
||||
|
||||
_removeCommand: function PasswordStore__removeCommand(command) {
|
||||
this._log.info("PasswordStore got removeCommand: " + command );
|
||||
|
||||
var login = new this._nsLoginInfo(command.data.hostname,
|
||||
command.data.formSubmitURL,
|
||||
command.data.httpRealm,
|
||||
command.data.username,
|
||||
command.data.password,
|
||||
command.data.usernameField,
|
||||
command.data.passwordField);
|
||||
|
||||
this._loginManager.removeLogin(login);
|
||||
},
|
||||
|
||||
_editCommand: function PasswordStore__editCommand(command) {
|
||||
this._log.info("PasswordStore got editCommand: " + command );
|
||||
throw "Password syncs are expected to only be create/remove!";
|
||||
},
|
||||
|
||||
wrap: function PasswordStore_wrap() {
|
||||
/* Return contents of this store, as JSON. */
|
||||
var items = {};
|
||||
|
||||
var logins = this._loginManager.getAllLogins({});
|
||||
|
||||
for (var i = 0; i < logins.length; i++) {
|
||||
var login = logins[i];
|
||||
|
||||
var key = _hashLoginInfo(login);
|
||||
|
||||
items[key] = { hostname : login.hostname,
|
||||
formSubmitURL : login.formSubmitURL,
|
||||
httpRealm : login.httpRealm,
|
||||
username : login.username,
|
||||
password : login.password,
|
||||
usernameField : login.usernameField,
|
||||
passwordField : login.passwordField };
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function PasswordStore_wipe() {
|
||||
this._loginManager.removeAllLogins();
|
||||
},
|
||||
|
||||
resetGUIDs: function PasswordStore_resetGUIDs() {
|
||||
// Not needed.
|
||||
}
|
||||
};
|
||||
PasswordStore.prototype.__proto__ = new Store();
|
||||
|
|
@ -0,0 +1,475 @@
|
|||
const EXPORTED_SYMBOLS = ['TabEngine'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://weave/util.js");
|
||||
Cu.import("resource://weave/async.js");
|
||||
Cu.import("resource://weave/engines.js");
|
||||
Cu.import("resource://weave/syncCores.js");
|
||||
Cu.import("resource://weave/stores.js");
|
||||
Cu.import("resource://weave/trackers.js");
|
||||
|
||||
Function.prototype.async = Async.sugar;
|
||||
|
||||
function TabEngine(pbeId) {
|
||||
this._init(pbeId);
|
||||
}
|
||||
|
||||
TabEngine.prototype = {
|
||||
__proto__: new Engine(),
|
||||
|
||||
get name() "tabs",
|
||||
get logName() "TabEngine",
|
||||
get serverPrefix() "user-data/tabs/",
|
||||
get store() this._store,
|
||||
|
||||
get _core() {
|
||||
let core = new TabSyncCore(this);
|
||||
this.__defineGetter__("_core", function() core);
|
||||
return this._core;
|
||||
},
|
||||
|
||||
get _store() {
|
||||
let store = new TabStore();
|
||||
this.__defineGetter__("_store", function() store);
|
||||
return this._store;
|
||||
},
|
||||
|
||||
get _tracker() {
|
||||
let tracker = new TabTracker(this);
|
||||
this.__defineGetter__("_tracker", function() tracker);
|
||||
return this._tracker;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function TabSyncCore(engine) {
|
||||
this._engine = engine;
|
||||
this._init();
|
||||
}
|
||||
TabSyncCore.prototype = {
|
||||
__proto__: new SyncCore(),
|
||||
|
||||
_logName: "TabSync",
|
||||
|
||||
_engine: null,
|
||||
|
||||
get _sessionStore() {
|
||||
let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"].
|
||||
getService(Ci.nsISessionStore);
|
||||
this.__defineGetter__("_sessionStore", function() sessionStore);
|
||||
return this._sessionStore;
|
||||
},
|
||||
|
||||
_itemExists: function TSC__itemExists(GUID) {
|
||||
// Note: this method returns true if the tab exists in any window, not just
|
||||
// the window from which the tab came. In the future, if we care about
|
||||
// windows, we might need to make this more specific, although in that case
|
||||
// we'll have to identify tabs by something other than URL, since even
|
||||
// window-specific tabs look the same when identified by URL.
|
||||
|
||||
// Get the set of all real and virtual tabs.
|
||||
let tabs = this._engine.store.wrap();
|
||||
|
||||
// XXX Should we convert both to nsIURIs and then use nsIURI::equals
|
||||
// to compare them?
|
||||
if (GUID in tabs) {
|
||||
this._log.debug("_itemExists: " + GUID + " exists");
|
||||
return true;
|
||||
}
|
||||
|
||||
this._log.debug("_itemExists: " + GUID + " doesn't exist");
|
||||
return false;
|
||||
},
|
||||
|
||||
_commandLike: function TSC_commandLike(a, b) {
|
||||
// Not implemented.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function TabStore() {
|
||||
this._virtualTabs = {};
|
||||
this._init();
|
||||
}
|
||||
TabStore.prototype = {
|
||||
__proto__: new Store(),
|
||||
|
||||
_logName: "TabStore",
|
||||
|
||||
get _sessionStore() {
|
||||
let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"].
|
||||
getService(Ci.nsISessionStore);
|
||||
this.__defineGetter__("_sessionStore", function() sessionStore);
|
||||
return this._sessionStore;
|
||||
},
|
||||
|
||||
get _windowMediator() {
|
||||
let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].
|
||||
getService(Ci.nsIWindowMediator);
|
||||
this.__defineGetter__("_windowMediator", function() windowMediator);
|
||||
return this._windowMediator;
|
||||
},
|
||||
|
||||
get _os() {
|
||||
let os = Cc["@mozilla.org/observer-service;1"].
|
||||
getService(Ci.nsIObserverService);
|
||||
this.__defineGetter__("_os", function() os);
|
||||
return this._os;
|
||||
},
|
||||
|
||||
get _dirSvc() {
|
||||
let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties);
|
||||
this.__defineGetter__("_dirSvc", function() dirSvc);
|
||||
return this._dirSvc;
|
||||
},
|
||||
|
||||
/**
|
||||
* A cache of "virtual" tabs from other devices synced to the server
|
||||
* that the user hasn't opened locally. Unlike other stores, we don't
|
||||
* immediately apply create commands, which would be jarring to users.
|
||||
* Instead, we store them in this cache and prompt the user to pick
|
||||
* which ones she wants to open.
|
||||
*
|
||||
* We also persist this cache on disk and include it in the list of tabs
|
||||
* we generate in this.wrap to reduce ping-pong updates between clients
|
||||
* running simultaneously and to maintain a consistent state across restarts.
|
||||
*/
|
||||
_virtualTabs: null,
|
||||
|
||||
get virtualTabs() {
|
||||
// Make sure the list of virtual tabs is completely up-to-date (the user
|
||||
// might have independently opened some of these virtual tabs since the last
|
||||
// time we synced).
|
||||
let realTabs = this._wrapRealTabs();
|
||||
let virtualTabsChanged = false;
|
||||
for (let id in this._virtualTabs) {
|
||||
if (id in realTabs) {
|
||||
this._log.warn("get virtualTabs: both real and virtual tabs exist for "
|
||||
+ id + "; removing virtual one");
|
||||
delete this._virtualTabs[id];
|
||||
virtualTabsChanged = true;
|
||||
}
|
||||
}
|
||||
if (virtualTabsChanged)
|
||||
this._saveVirtualTabs();
|
||||
|
||||
return this._virtualTabs;
|
||||
},
|
||||
|
||||
set virtualTabs(newValue) {
|
||||
this._virtualTabs = newValue;
|
||||
this._saveVirtualTabs();
|
||||
},
|
||||
|
||||
// The file in which we store the state of virtual tabs.
|
||||
get _file() {
|
||||
let file = this._dirSvc.get("ProfD", Ci.nsILocalFile);
|
||||
file.append("weave");
|
||||
file.append("store");
|
||||
file.append("tabs");
|
||||
file.append("virtual.json");
|
||||
this.__defineGetter__("_file", function() file);
|
||||
return this._file;
|
||||
},
|
||||
|
||||
_saveVirtualTabs: function TabStore__saveVirtualTabs() {
|
||||
try {
|
||||
if (!this._file.exists())
|
||||
this._file.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
||||
let out = this._json.encode(this._virtualTabs);
|
||||
let [fos] = Utils.open(this._file, ">");
|
||||
fos.writeString(out);
|
||||
fos.close();
|
||||
}
|
||||
catch(ex) {
|
||||
this._log.warn("could not serialize virtual tabs to disk: " + ex);
|
||||
}
|
||||
},
|
||||
|
||||
_restoreVirtualTabs: function TabStore__restoreVirtualTabs() {
|
||||
try {
|
||||
if (this._file.exists()) {
|
||||
let [is] = Utils.open(this._file, "<");
|
||||
let json = Utils.readStream(is);
|
||||
is.close();
|
||||
this._virtualTabs = this._json.decode(json);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
this._log.warn("could not parse virtual tabs from disk: " + ex);
|
||||
}
|
||||
},
|
||||
|
||||
_init: function TabStore__init() {
|
||||
this._restoreVirtualTabs();
|
||||
|
||||
this.__proto__.__proto__._init();
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply commands generated by a diff during a sync operation. This method
|
||||
* overrides the one in its superclass so it can save a copy of the latest set
|
||||
* of virtual tabs to disk so they can be restored on startup.
|
||||
*/
|
||||
applyCommands: function TabStore_applyCommands(commandList) {
|
||||
let self = yield;
|
||||
|
||||
this.__proto__.__proto__.applyCommands.async(this, self.cb, commandList);
|
||||
yield;
|
||||
|
||||
this._saveVirtualTabs();
|
||||
|
||||
self.done();
|
||||
},
|
||||
|
||||
_createCommand: function TabStore__createCommand(command) {
|
||||
this._log.debug("_createCommand: " + command.GUID);
|
||||
|
||||
if (command.GUID in this._virtualTabs || command.GUID in this._wrapRealTabs())
|
||||
throw "trying to create a tab that already exists; id: " + command.GUID;
|
||||
|
||||
// Cache the tab and notify the UI to prompt the user to open it.
|
||||
this._virtualTabs[command.GUID] = command.data;
|
||||
this._os.notifyObservers(null, "weave:store:tabs:virtual:created", null);
|
||||
},
|
||||
|
||||
_removeCommand: function TabStore__removeCommand(command) {
|
||||
this._log.debug("_removeCommand: " + command.GUID);
|
||||
|
||||
// If this is a virtual tab, it's ok to remove it, since it was never really
|
||||
// added to this session in the first place. But we don't remove it if it's
|
||||
// a real tab, since that would be unexpected, unpleasant, and unwanted.
|
||||
if (command.GUID in this._virtualTabs) {
|
||||
delete this._virtualTabs[command.GUID];
|
||||
this._os.notifyObservers(null, "weave:store:tabs:virtual:removed", null);
|
||||
}
|
||||
},
|
||||
|
||||
_editCommand: function TabStore__editCommand(command) {
|
||||
this._log.debug("_editCommand: " + command.GUID);
|
||||
|
||||
// We don't edit real tabs, because that isn't what the user would expect,
|
||||
// but it's ok to edit virtual tabs, so that if users do open them, they get
|
||||
// the most up-to-date version of them (and also to reduce sync churn).
|
||||
|
||||
if (this._virtualTabs[command.GUID])
|
||||
this._virtualTabs[command.GUID] = command.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize the current state of tabs.
|
||||
*
|
||||
* Note: the state includes both tabs on this device and those on others.
|
||||
* We get the former from the session store. The latter we retrieved from
|
||||
* the Weave server and stored in this._virtualTabs. Including virtual tabs
|
||||
* in the serialized state prevents ping-pong deletes between two clients
|
||||
* running at the same time.
|
||||
*/
|
||||
wrap: function TabStore_wrap() {
|
||||
let items;
|
||||
|
||||
let virtualTabs = this._wrapVirtualTabs();
|
||||
let realTabs = this._wrapRealTabs();
|
||||
|
||||
// Real tabs override virtual ones, which means ping-pong edits when two
|
||||
// clients have the same URL loaded with different history/attributes.
|
||||
// We could fix that by overriding real tabs with virtual ones, but then
|
||||
// we'd have stale tab metadata in same cases.
|
||||
items = virtualTabs;
|
||||
let virtualTabsChanged = false;
|
||||
for (let id in realTabs) {
|
||||
// Since virtual tabs can sometimes get out of sync with real tabs
|
||||
// (the user could have independently opened a new tab that exists
|
||||
// in the virtual tabs cache since the last time we updated the cache),
|
||||
// we sync them up in the process of merging them here.
|
||||
if (this._virtualTabs[id]) {
|
||||
this._log.warn("wrap: both real and virtual tabs exist for " + id +
|
||||
"; removing virtual one");
|
||||
delete this._virtualTabs[id];
|
||||
virtualTabsChanged = true;
|
||||
}
|
||||
|
||||
items[id] = realTabs[id];
|
||||
}
|
||||
if (virtualTabsChanged)
|
||||
this._saveVirtualTabs();
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
_wrapVirtualTabs: function TabStore__wrapVirtualTabs() {
|
||||
let items = {};
|
||||
|
||||
for (let id in this._virtualTabs) {
|
||||
let virtualTab = this._virtualTabs[id];
|
||||
|
||||
// Copy the virtual tab without private properties (those that begin
|
||||
// with an underscore character) so that we don't sync data private to
|
||||
// this particular Weave client (like the _disposed flag).
|
||||
let item = {};
|
||||
for (let property in virtualTab)
|
||||
if (property[0] != "_")
|
||||
item[property] = virtualTab[property];
|
||||
|
||||
items[id] = item;
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
_wrapRealTabs: function TabStore__wrapRealTabs() {
|
||||
let items = {};
|
||||
|
||||
let session = this._json.decode(this._sessionStore.getBrowserState());
|
||||
|
||||
for (let i = 0; i < session.windows.length; i++) {
|
||||
let window = session.windows[i];
|
||||
// For some reason, session store uses one-based array index references,
|
||||
// (f.e. in the "selectedWindow" and each tab's "index" properties), so we
|
||||
// convert them to and from JavaScript's zero-based indexes as needed.
|
||||
let windowID = i + 1;
|
||||
this._log.debug("_wrapRealTabs: window " + windowID);
|
||||
for (let j = 0; j < window.tabs.length; j++) {
|
||||
let tab = window.tabs[j];
|
||||
|
||||
// The session history entry for the page currently loaded in the tab.
|
||||
// We use the URL of the current page as the ID for the tab.
|
||||
let currentEntry = tab.entries[tab.index - 1];
|
||||
|
||||
if (!currentEntry || !currentEntry.url) {
|
||||
this._log.warn("_wrapRealTabs: no current entry or no URL, can't " +
|
||||
"identify " + this._json.encode(tab));
|
||||
continue;
|
||||
}
|
||||
|
||||
let tabID = currentEntry.url;
|
||||
this._log.debug("_wrapRealTabs: tab " + tabID);
|
||||
|
||||
items[tabID] = {
|
||||
// Identify this item as a tab in case we start serializing windows
|
||||
// in the future.
|
||||
type: "tab",
|
||||
|
||||
// The position of this tab relative to other tabs in the window.
|
||||
// For consistency with session store data, we make this one-based.
|
||||
position: j + 1,
|
||||
|
||||
windowID: windowID,
|
||||
|
||||
state: tab
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function TabStore_wipe() {
|
||||
// We're not going to close tabs, since that's probably not what
|
||||
// the user wants, but we'll clear the cache of virtual tabs.
|
||||
this._virtualTabs = {};
|
||||
this._saveVirtualTabs();
|
||||
},
|
||||
|
||||
resetGUIDs: function TabStore_resetGUIDs() {
|
||||
// Not needed.
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function TabTracker(engine) {
|
||||
this._engine = engine;
|
||||
this._init();
|
||||
}
|
||||
TabTracker.prototype = {
|
||||
__proto__: new Tracker(),
|
||||
|
||||
_logName: "TabTracker",
|
||||
|
||||
_engine: null,
|
||||
|
||||
get _json() {
|
||||
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||
this.__defineGetter__("_json", function() json);
|
||||
return this._json;
|
||||
},
|
||||
|
||||
/**
|
||||
* There are two ways we could calculate the score. We could calculate it
|
||||
* incrementally by using the window mediator to watch for windows opening/
|
||||
* closing and FUEL (or some other API) to watch for tabs opening/closing
|
||||
* and changing location.
|
||||
*
|
||||
* Or we could calculate it on demand by comparing the state of tabs
|
||||
* according to the session store with the state according to the snapshot.
|
||||
*
|
||||
* It's hard to say which is better. The incremental approach is less
|
||||
* accurate if it simply increments the score whenever there's a change,
|
||||
* but it might be more performant. The on-demand approach is more accurate,
|
||||
* but it might be less performant depending on how often it's called.
|
||||
*
|
||||
* In this case we've decided to go with the on-demand approach, and we
|
||||
* calculate the score as the percent difference between the snapshot set
|
||||
* and the current tab set, where tabs that only exist in one set are
|
||||
* completely different, while tabs that exist in both sets but whose data
|
||||
* doesn't match (f.e. because of variations in history) are considered
|
||||
* "half different".
|
||||
*
|
||||
* So if the sets don't match at all, we return 100;
|
||||
* if they completely match, we return 0;
|
||||
* if half the tabs match, and their data is the same, we return 50;
|
||||
* and if half the tabs match, but their data is all different, we return 75.
|
||||
*/
|
||||
get score() {
|
||||
// The snapshot data is a singleton that we can't modify, so we have to
|
||||
// copy its unique items to a new hash.
|
||||
let snapshotData = this._engine.snapshot.data;
|
||||
let a = {};
|
||||
|
||||
// The wrapped current state is a unique instance we can munge all we want.
|
||||
let b = this._engine.store.wrap();
|
||||
|
||||
// An array that counts the number of intersecting IDs between a and b
|
||||
// (represented as the length of c) and whether or not their values match
|
||||
// (represented by the boolean value of each item in c).
|
||||
let c = [];
|
||||
|
||||
// Generate c and update a and b to contain only unique items.
|
||||
for (id in snapshotData) {
|
||||
if (id in b) {
|
||||
c.push(this._json.encode(snapshotData[id]) == this._json.encode(b[id]));
|
||||
delete b[id];
|
||||
}
|
||||
else {
|
||||
a[id] = snapshotData[id];
|
||||
}
|
||||
}
|
||||
|
||||
let numShared = c.length;
|
||||
let numUnique = [true for (id in a)].length + [true for (id in b)].length;
|
||||
let numTotal = numShared + numUnique;
|
||||
|
||||
// We're going to divide by the total later, so make sure we don't try
|
||||
// to divide by zero, even though we should never be in a state where there
|
||||
// are no tabs in either set.
|
||||
if (numTotal == 0)
|
||||
return 0;
|
||||
|
||||
// The number of shared items whose data is different.
|
||||
let numChanged = c.filter(function(v) v).length;
|
||||
|
||||
let fractionSimilar = (numShared - (numChanged / 2)) / numTotal;
|
||||
let fractionDissimilar = 1 - fractionSimilar;
|
||||
let percentDissimilar = Math.round(fractionDissimilar * 100);
|
||||
|
||||
return percentDissimilar;
|
||||
},
|
||||
|
||||
resetScore: function FormsTracker_resetScore() {
|
||||
// Not implemented, since we calculate the score on demand.
|
||||
}
|
||||
}
|
|
@ -51,6 +51,12 @@ Cu.import("resource://weave/engines.js");
|
|||
Cu.import("resource://weave/dav.js");
|
||||
Cu.import("resource://weave/identity.js");
|
||||
Cu.import("resource://weave/async.js");
|
||||
Cu.import("resource://weave/engines/cookies.js");
|
||||
Cu.import("resource://weave/engines/bookmarks.js");
|
||||
Cu.import("resource://weave/engines/history.js");
|
||||
Cu.import("resource://weave/engines/passwords.js");
|
||||
Cu.import("resource://weave/engines/forms.js");
|
||||
Cu.import("resource://weave/engines/tabs.js");
|
||||
|
||||
Function.prototype.async = Async.sugar;
|
||||
|
||||
|
@ -93,6 +99,7 @@ function WeaveSvc() {
|
|||
Engines.register(new CookieEngine());
|
||||
Engines.register(new PasswordEngine());
|
||||
Engines.register(new FormEngine());
|
||||
Engines.register(new TabEngine());
|
||||
|
||||
// Other misc startup
|
||||
Utils.prefs.addObserver("", this, false);
|
||||
|
|
|
@ -34,8 +34,8 @@
|
|||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore',
|
||||
'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore'];
|
||||
const EXPORTED_SYMBOLS = ['Store',
|
||||
'SnapshotStore'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
@ -270,836 +270,3 @@ SnapshotStore.prototype = {
|
|||
}
|
||||
};
|
||||
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;
|
||||
},
|
||||
|
||||
_getItemIdForGUID: function BStore__getItemIdForGUID(GUID) {
|
||||
switch (GUID) {
|
||||
case "menu":
|
||||
return this._bms.bookmarksMenuFolder;
|
||||
case "toolbar":
|
||||
return this._bms.toolbarFolder;
|
||||
case "unfiled":
|
||||
return this._bms.unfiledBookmarksFolder;
|
||||
default:
|
||||
return this._bms.getItemIdForGUID(GUID);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
_createCommand: function BStore__createCommand(command) {
|
||||
let newId;
|
||||
let parentId = this._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.debug(" -> creating bookmark \"" + command.data.title + "\"");
|
||||
let URI = Utils.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.description) {
|
||||
this._ans.setItemAnnotation(newId, "bookmarkProperties/description",
|
||||
command.data.description, 0,
|
||||
this._ans.EXPIRE_NEVER);
|
||||
}
|
||||
|
||||
if (command.data.type == "microsummary") {
|
||||
this._log.debug(" \-> is a microsummary");
|
||||
let genURI = Utils.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.debug(" -> creating folder \"" + command.data.title + "\"");
|
||||
newId = this._bms.createFolder(parentId,
|
||||
command.data.title,
|
||||
command.data.index);
|
||||
break;
|
||||
case "livemark":
|
||||
this._log.debug(" -> creating livemark \"" + command.data.title + "\"");
|
||||
newId = this._ls.createLivemark(parentId,
|
||||
command.data.title,
|
||||
Utils.makeURI(command.data.siteURI),
|
||||
Utils.makeURI(command.data.feedURI),
|
||||
command.data.index);
|
||||
break;
|
||||
case "mounted-share":
|
||||
this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\"");
|
||||
newId = this._bms.createFolder(parentId,
|
||||
command.data.title,
|
||||
command.data.index);
|
||||
|
||||
this._ans.setItemAnnotation(newId, "weave/mounted-share-id",
|
||||
command.data.mountId, 0, this._ans.EXPIRE_NEVER);
|
||||
break;
|
||||
case "separator":
|
||||
this._log.debug(" -> 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) {
|
||||
if (command.GUID == "menu" ||
|
||||
command.GUID == "toolbar" ||
|
||||
command.GUID == "unfiled") {
|
||||
this._log.warn("Attempted to remove root node (" + command.GUID +
|
||||
"). Skipping command.");
|
||||
return;
|
||||
}
|
||||
|
||||
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.debug(" -> removing bookmark " + command.GUID);
|
||||
this._bms.removeItem(itemId);
|
||||
break;
|
||||
case this._bms.TYPE_FOLDER:
|
||||
this._log.debug(" -> removing folder " + command.GUID);
|
||||
this._bms.removeFolder(itemId);
|
||||
break;
|
||||
case this._bms.TYPE_SEPARATOR:
|
||||
this._log.debug(" -> removing separator " + command.GUID);
|
||||
this._bms.removeItem(itemId);
|
||||
break;
|
||||
default:
|
||||
this._log.error("removeCommand: Unknown item type: " + type);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_editCommand: function BStore__editCommand(command) {
|
||||
if (command.GUID == "menu" ||
|
||||
command.GUID == "toolbar" ||
|
||||
command.GUID == "unfiled") {
|
||||
this._log.warn("Attempted to edit root node (" + command.GUID +
|
||||
"). Skipping command.");
|
||||
return;
|
||||
}
|
||||
|
||||
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 "type":
|
||||
// all commands have this to help in reconciliation, but it makes
|
||||
// no sense to edit it
|
||||
break;
|
||||
case "GUID":
|
||||
var existing = this._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, Utils.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._getItemIdForGUID(command.data.parentGUID), index);
|
||||
} break;
|
||||
case "tags": {
|
||||
let tagsURI = this._bms.getBookmarkURI(itemId);
|
||||
this._ts.untagURI(tagsURI, null);
|
||||
this._ts.tagURI(tagsURI, command.data.tags);
|
||||
} break;
|
||||
case "keyword":
|
||||
this._bms.setKeywordForBookmark(itemId, command.data.keyword);
|
||||
break;
|
||||
case "description":
|
||||
if (command.data.description) {
|
||||
this._ans.setItemAnnotation(itemId, "bookmarkProperties/description",
|
||||
command.data.description, 0,
|
||||
this._ans.EXPIRE_NEVER);
|
||||
}
|
||||
break;
|
||||
case "generatorURI": {
|
||||
let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId));
|
||||
let genURI = Utils.makeURI(command.data.generatorURI);
|
||||
let micsum = this._ms.createMicrosummary(micsumURI, genURI);
|
||||
this._ms.setMicrosummary(itemId, micsum);
|
||||
} break;
|
||||
case "siteURI":
|
||||
this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI));
|
||||
break;
|
||||
case "feedURI":
|
||||
this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI));
|
||||
break;
|
||||
default:
|
||||
this._log.warn("Can't change item property: " + key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getNode: function BSS__getNode(folder) {
|
||||
let query = this._hsvc.getNewQuery();
|
||||
query.setFolders([folder], 1);
|
||||
return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root;
|
||||
},
|
||||
|
||||
__wrap: function BSS___wrap(node, items, parentGUID, index, guidOverride) {
|
||||
let GUID, item;
|
||||
|
||||
// we override the guid for the root items, "menu", "toolbar", etc.
|
||||
if (guidOverride) {
|
||||
GUID = guidOverride;
|
||||
item = {};
|
||||
} else {
|
||||
GUID = this._bms.getItemGUID(node.itemId);
|
||||
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 if (this._ans.itemHasAnnotation(node.itemId,
|
||||
"weave/mounted-share-id")) {
|
||||
item.type = "mounted-share";
|
||||
item.title = node.title;
|
||||
item.mountId = this._ans.getItemAnnotation(node.itemId,
|
||||
"weave/mounted-share-id");
|
||||
|
||||
} else {
|
||||
item.type = "folder";
|
||||
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
node.containerOpen = true;
|
||||
for (var i = 0; i < node.childCount; i++) {
|
||||
this.__wrap(node.getChild(i), items, GUID, i);
|
||||
}
|
||||
}
|
||||
if (!guidOverride)
|
||||
item.title = node.title; // no titles for root nodes
|
||||
|
||||
} 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;
|
||||
}
|
||||
|
||||
try {
|
||||
item.description =
|
||||
this._ans.getItemAnnotation(node.itemId, "bookmarkProperties/description");
|
||||
} catch (e) {
|
||||
item.description = undefined;
|
||||
}
|
||||
|
||||
item.URI = node.uri;
|
||||
item.tags = this._ts.getTagsForURI(Utils.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;
|
||||
},
|
||||
|
||||
// helper
|
||||
_wrap: function BStore__wrap(node, items, rootName) {
|
||||
return this.__wrap(node, items, null, null, rootName);
|
||||
},
|
||||
|
||||
_wrapMount: function BStore__wrapMount(node, id) {
|
||||
if (node.type != node.RESULT_TYPE_FOLDER)
|
||||
throw "Trying to wrap a non-folder mounted share";
|
||||
|
||||
let GUID = this._bms.getItemGUID(node.itemId);
|
||||
let ret = {rootGUID: GUID, userid: id, snapshot: {}};
|
||||
|
||||
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
node.containerOpen = true;
|
||||
for (var i = 0; i < node.childCount; i++) {
|
||||
this.__wrap(node.getChild(i), ret.snapshot, GUID, i);
|
||||
}
|
||||
|
||||
// remove any share mountpoints
|
||||
for (let guid in ret.snapshot) {
|
||||
if (ret.snapshot[guid].type == "mounted-share")
|
||||
delete ret.snapshot[guid];
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
_resetGUIDs: function BSS__resetGUIDs(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._resetGUIDs(node.getChild(i));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
findMounts: function BStore_findMounts() {
|
||||
let ret = [];
|
||||
let a = this._ans.getItemsWithAnnotation("weave/mounted-share-id", {});
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
let id = this._ans.getItemAnnotation(a[i], "weave/mounted-share-id");
|
||||
ret.push(this._wrapMount(this._getNode(a[i]), id));
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
wrap: function BStore_wrap() {
|
||||
var items = {};
|
||||
this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu");
|
||||
this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar");
|
||||
this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled");
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function BStore_wipe() {
|
||||
this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder);
|
||||
this._bms.removeFolderChildren(this._bms.toolbarFolder);
|
||||
this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder);
|
||||
},
|
||||
|
||||
resetGUIDs: function BStore_resetGUIDs() {
|
||||
this._resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder));
|
||||
this._resetGUIDs(this._getNode(this._bms.toolbarFolder));
|
||||
this._resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder));
|
||||
}
|
||||
};
|
||||
BookmarksStore.prototype.__proto__ = new Store();
|
||||
|
||||
function HistoryStore() {
|
||||
this._init();
|
||||
}
|
||||
HistoryStore.prototype = {
|
||||
_logName: "HistStore",
|
||||
|
||||
__hsvc: null,
|
||||
get _hsvc() {
|
||||
if (!this.__hsvc) {
|
||||
this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
this.__hsvc.QueryInterface(Ci.nsIGlobalHistory2);
|
||||
this.__hsvc.QueryInterface(Ci.nsIBrowserHistory);
|
||||
}
|
||||
return this.__hsvc;
|
||||
},
|
||||
|
||||
_createCommand: function HistStore__createCommand(command) {
|
||||
this._log.debug(" -> creating history entry: " + command.GUID);
|
||||
try {
|
||||
let uri = Utils.makeURI(command.data.URI);
|
||||
this._hsvc.addVisit(uri, command.data.time, null,
|
||||
this._hsvc.TRANSITION_TYPED, false, null);
|
||||
this._hsvc.setPageTitle(uri, command.data.title);
|
||||
} catch (e) {
|
||||
this._log.error("Exception caught: " + (e.message? e.message : e));
|
||||
}
|
||||
},
|
||||
|
||||
_removeCommand: function HistStore__removeCommand(command) {
|
||||
this._log.trace(" -> NOT removing history entry: " + command.GUID);
|
||||
// we can't remove because we only sync the last 1000 items, not
|
||||
// the whole store. So we don't know if remove commands were
|
||||
// generated due to the user removing an entry or because it
|
||||
// dropped past the 1000 item mark.
|
||||
},
|
||||
|
||||
_editCommand: function HistStore__editCommand(command) {
|
||||
this._log.trace(" -> FIXME: NOT editing history entry: " + command.GUID);
|
||||
// FIXME: implement!
|
||||
},
|
||||
|
||||
_historyRoot: function HistStore__historyRoot() {
|
||||
let query = this._hsvc.getNewQuery(),
|
||||
options = this._hsvc.getNewQueryOptions();
|
||||
|
||||
query.minVisits = 1;
|
||||
options.maxResults = 1000;
|
||||
options.resultType = options.RESULTS_AS_VISIT; // FULL_VISIT does not work
|
||||
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
|
||||
options.queryType = options.QUERY_TYPE_HISTORY;
|
||||
|
||||
let root = this._hsvc.executeQuery(query, options).root;
|
||||
root.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
||||
return root;
|
||||
},
|
||||
|
||||
wrap: function HistStore_wrap() {
|
||||
let root = this._historyRoot();
|
||||
root.containerOpen = true;
|
||||
let items = {};
|
||||
for (let i = 0; i < root.childCount; i++) {
|
||||
let item = root.getChild(i);
|
||||
let guid = item.time + ":" + item.uri
|
||||
items[guid] = {parentGUID: '',
|
||||
title: item.title,
|
||||
URI: item.uri,
|
||||
time: item.time
|
||||
};
|
||||
// FIXME: sync transition type - requires FULL_VISITs
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function HistStore_wipe() {
|
||||
this._hsvc.removeAllPages();
|
||||
}
|
||||
};
|
||||
HistoryStore.prototype.__proto__ = new Store();
|
||||
|
||||
|
||||
function CookieStore( cookieManagerStub ) {
|
||||
/* If no argument is passed in, this store will query/write to the real
|
||||
Mozilla cookie manager component. This is the normal way to use this
|
||||
class in production code. But for unit-testing purposes, you can pass
|
||||
in a stub object that will be used in place of the cookieManager. */
|
||||
this._init();
|
||||
this._cookieManagerStub = cookieManagerStub;
|
||||
}
|
||||
CookieStore.prototype = {
|
||||
_logName: "CookieStore",
|
||||
|
||||
|
||||
// Documentation of the nsICookie interface says:
|
||||
// name ACString The name of the cookie. Read only.
|
||||
// value ACString The cookie value. Read only.
|
||||
// isDomain boolean True if the cookie is a domain cookie, false otherwise. Read only.
|
||||
// host AUTF8String The host (possibly fully qualified) of the cookie. Read only.
|
||||
// path AUTF8String The path pertaining to the cookie. Read only.
|
||||
// isSecure boolean True if the cookie was transmitted over ssl, false otherwise. Read only.
|
||||
// expires PRUint64 Expiration time (local timezone) expressed as number of seconds since Jan 1, 1970. Read only.
|
||||
// status nsCookieStatus Holds the P3P status of cookie. Read only.
|
||||
// policy nsCookiePolicy Holds the site's compact policy value. Read only.
|
||||
// nsICookie2 deprecates expires, status, and policy, and adds:
|
||||
//rawHost AUTF8String The host (possibly fully qualified) of the cookie without a leading dot to represent if it is a domain cookie. Read only.
|
||||
//isSession boolean True if the cookie is a session cookie. Read only.
|
||||
//expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only.
|
||||
//isHttpOnly boolean True if the cookie is an http only cookie. Read only.
|
||||
|
||||
__cookieManager: null,
|
||||
get _cookieManager() {
|
||||
if ( this._cookieManagerStub != undefined ) {
|
||||
return this._cookieManagerStub;
|
||||
}
|
||||
// otherwise, use the real one
|
||||
if (!this.__cookieManager)
|
||||
this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"].
|
||||
getService(Ci.nsICookieManager2);
|
||||
// need the 2nd revision of the ICookieManager interface
|
||||
// because it supports add() and the 1st one doesn't.
|
||||
return this.__cookieManager
|
||||
},
|
||||
|
||||
_createCommand: function CookieStore__createCommand(command) {
|
||||
/* we got a command to create a cookie in the local browser
|
||||
in order to sync with the server. */
|
||||
|
||||
this._log.info("CookieStore got createCommand: " + command );
|
||||
// this assumes command.data fits the nsICookie2 interface
|
||||
if ( !command.data.isSession ) {
|
||||
// Add only persistent cookies ( not session cookies )
|
||||
this._cookieManager.add( command.data.host,
|
||||
command.data.path,
|
||||
command.data.name,
|
||||
command.data.value,
|
||||
command.data.isSecure,
|
||||
command.data.isHttpOnly,
|
||||
command.data.isSession,
|
||||
command.data.expiry );
|
||||
}
|
||||
},
|
||||
|
||||
_removeCommand: function CookieStore__removeCommand(command) {
|
||||
/* we got a command to remove a cookie from the local browser
|
||||
in order to sync with the server.
|
||||
command.data appears to be equivalent to what wrap() puts in
|
||||
the JSON dictionary. */
|
||||
|
||||
this._log.info("CookieStore got removeCommand: " + command );
|
||||
|
||||
/* I think it goes like this, according to
|
||||
http://developer.mozilla.org/en/docs/nsICookieManager
|
||||
the last argument is "always block cookies from this domain?"
|
||||
and the answer is "no". */
|
||||
this._cookieManager.remove( command.data.host,
|
||||
command.data.name,
|
||||
command.data.path,
|
||||
false );
|
||||
},
|
||||
|
||||
_editCommand: function CookieStore__editCommand(command) {
|
||||
/* we got a command to change a cookie in the local browser
|
||||
in order to sync with the server. */
|
||||
this._log.info("CookieStore got editCommand: " + command );
|
||||
|
||||
/* Look up the cookie that matches the one in the command: */
|
||||
var iter = this._cookieManager.enumerator;
|
||||
var matchingCookie = null;
|
||||
while (iter.hasMoreElements()){
|
||||
let cookie = iter.getNext();
|
||||
if (cookie.QueryInterface( Ci.nsICookie ) ){
|
||||
// see if host:path:name of cookie matches GUID given in command
|
||||
let key = cookie.host + ":" + cookie.path + ":" + cookie.name;
|
||||
if (key == command.GUID) {
|
||||
matchingCookie = cookie;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update values in the cookie:
|
||||
for (var key in command.data) {
|
||||
// Whatever values command.data has, use them
|
||||
matchingCookie[ key ] = command.data[ key ]
|
||||
}
|
||||
// Remove the old incorrect cookie from the manager:
|
||||
this._cookieManager.remove( matchingCookie.host,
|
||||
matchingCookie.name,
|
||||
matchingCookie.path,
|
||||
false );
|
||||
|
||||
// Re-add the new updated cookie:
|
||||
if ( !command.data.isSession ) {
|
||||
/* ignore single-session cookies, add only persistent cookies. */
|
||||
this._cookieManager.add( matchingCookie.host,
|
||||
matchingCookie.path,
|
||||
matchingCookie.name,
|
||||
matchingCookie.value,
|
||||
matchingCookie.isSecure,
|
||||
matchingCookie.isHttpOnly,
|
||||
matchingCookie.isSession,
|
||||
matchingCookie.expiry );
|
||||
}
|
||||
|
||||
// Also, there's an exception raised because
|
||||
// this._data[comand.GUID] is undefined
|
||||
},
|
||||
|
||||
wrap: function CookieStore_wrap() {
|
||||
/* Return contents of this store, as JSON.
|
||||
A dictionary of cookies where the keys are GUIDs and the
|
||||
values are sub-dictionaries containing all cookie fields. */
|
||||
|
||||
let items = {};
|
||||
var iter = this._cookieManager.enumerator;
|
||||
while (iter.hasMoreElements()){
|
||||
var cookie = iter.getNext();
|
||||
if (cookie.QueryInterface( Ci.nsICookie )){
|
||||
// String used to identify cookies is
|
||||
// host:path:name
|
||||
if ( cookie.isSession ) {
|
||||
/* Skip session-only cookies, sync only persistent cookies. */
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = cookie.host + ":" + cookie.path + ":" + cookie.name;
|
||||
items[ key ] = { parentGUID: '',
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
isDomain: cookie.isDomain,
|
||||
host: cookie.host,
|
||||
path: cookie.path,
|
||||
isSecure: cookie.isSecure,
|
||||
// nsICookie2 values:
|
||||
rawHost: cookie.rawHost,
|
||||
isSession: cookie.isSession,
|
||||
expiry: cookie.expiry,
|
||||
isHttpOnly: cookie.isHttpOnly }
|
||||
|
||||
/* See http://developer.mozilla.org/en/docs/nsICookie
|
||||
Note: not syncing "expires", "status", or "policy"
|
||||
since they're deprecated. */
|
||||
|
||||
}
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function CookieStore_wipe() {
|
||||
/* Remove everything from the store. Return nothing.
|
||||
TODO are the semantics of this just wiping out an internal
|
||||
buffer, or am I supposed to wipe out all cookies from
|
||||
the browser itself for reals? */
|
||||
this._cookieManager.removeAll()
|
||||
},
|
||||
|
||||
resetGUIDs: function CookieStore_resetGUIDs() {
|
||||
/* called in the case where remote/local sync GUIDs do not
|
||||
match. We do need to override this, but since we're deriving
|
||||
GUIDs from the cookie data itself and not generating them,
|
||||
there's basically no way they can get "out of sync" so there's
|
||||
nothing to do here. */
|
||||
}
|
||||
};
|
||||
CookieStore.prototype.__proto__ = new Store();
|
||||
|
||||
function PasswordStore() {
|
||||
this._init();
|
||||
}
|
||||
PasswordStore.prototype = {
|
||||
_logName: "PasswordStore",
|
||||
|
||||
__loginManager : null,
|
||||
get _loginManager() {
|
||||
if (!this.__loginManager)
|
||||
this.__loginManager = Cc["@mozilla.org/login-manager;1"].
|
||||
getService(Ci.nsILoginManager);
|
||||
return this.__loginManager;
|
||||
},
|
||||
|
||||
__nsLoginInfo : null,
|
||||
get _nsLoginInfo() {
|
||||
if (!this.__nsLoginInfo)
|
||||
this.__nsLoginInfo = new Components.Constructor(
|
||||
"@mozilla.org/login-manager/loginInfo;1",
|
||||
Ci.nsILoginInfo, "init");
|
||||
return this.__nsLoginInfo;
|
||||
},
|
||||
|
||||
|
||||
_createCommand: function PasswordStore__createCommand(command) {
|
||||
this._log.info("PasswordStore got createCommand: " + command );
|
||||
|
||||
var login = new this._nsLoginInfo(command.data.hostname,
|
||||
command.data.formSubmitURL,
|
||||
command.data.httpRealm,
|
||||
command.data.username,
|
||||
command.data.password,
|
||||
command.data.usernameField,
|
||||
command.data.passwordField);
|
||||
|
||||
this._loginManager.addLogin(login);
|
||||
},
|
||||
|
||||
_removeCommand: function PasswordStore__removeCommand(command) {
|
||||
this._log.info("PasswordStore got removeCommand: " + command );
|
||||
|
||||
var login = new this._nsLoginInfo(command.data.hostname,
|
||||
command.data.formSubmitURL,
|
||||
command.data.httpRealm,
|
||||
command.data.username,
|
||||
command.data.password,
|
||||
command.data.usernameField,
|
||||
command.data.passwordField);
|
||||
|
||||
this._loginManager.removeLogin(login);
|
||||
},
|
||||
|
||||
_editCommand: function PasswordStore__editCommand(command) {
|
||||
this._log.info("PasswordStore got editCommand: " + command );
|
||||
throw "Password syncs are expected to only be create/remove!";
|
||||
},
|
||||
|
||||
wrap: function PasswordStore_wrap() {
|
||||
/* Return contents of this store, as JSON. */
|
||||
var items = [];
|
||||
|
||||
var logins = this._loginManager.getAllLogins({});
|
||||
|
||||
for (var i = 0; i < logins.length; i++) {
|
||||
var login = logins[i];
|
||||
|
||||
var key = this._hashLoginInfo(login);
|
||||
|
||||
items[key] = { hostname : login.hostname,
|
||||
formSubmitURL : login.formSubmitURL,
|
||||
httpRealm : login.httpRealm,
|
||||
username : login.username,
|
||||
password : login.password,
|
||||
usernameField : login.usernameField,
|
||||
passwordField : login.passwordField };
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function PasswordStore_wipe() {
|
||||
this._loginManager.removeAllLogins();
|
||||
},
|
||||
|
||||
resetGUIDs: function PasswordStore_resetGUIDs() {
|
||||
// Not needed.
|
||||
}
|
||||
};
|
||||
PasswordStore.prototype.__proto__ = new Store();
|
||||
|
||||
function FormStore() {
|
||||
this._init();
|
||||
}
|
||||
FormStore.prototype = {
|
||||
_logName: "FormStore",
|
||||
|
||||
__formDB: null,
|
||||
get _formDB() {
|
||||
if (!this.__formDB) {
|
||||
var file = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties).
|
||||
get("ProfD", Ci.nsIFile);
|
||||
file.append("formhistory.sqlite");
|
||||
var stor = Cc["@mozilla.org/storage/service;1"].
|
||||
getService(Ci.mozIStorageService);
|
||||
this.__formDB = stor.openDatabase(file);
|
||||
}
|
||||
return this.__formDB;
|
||||
},
|
||||
|
||||
__formHistory: null,
|
||||
get _formHistory() {
|
||||
if (!this.__formHistory)
|
||||
this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"].
|
||||
getService(Ci.nsIFormHistory2);
|
||||
return this.__formHistory;
|
||||
},
|
||||
|
||||
_createCommand: function FormStore__createCommand(command) {
|
||||
this._log.info("FormStore got createCommand: " + command );
|
||||
this._formHistory.addEntry(command.data.name, command.data.value);
|
||||
},
|
||||
|
||||
_removeCommand: function FormStore__removeCommand(command) {
|
||||
this._log.info("FormStore got removeCommand: " + command );
|
||||
this._formHistory.removeEntry(command.data.name, command.data.value);
|
||||
},
|
||||
|
||||
_editCommand: function FormStore__editCommand(command) {
|
||||
this._log.info("FormStore got editCommand: " + command );
|
||||
this._log.warn("Form syncs are expected to only be create/remove!");
|
||||
},
|
||||
|
||||
wrap: function FormStore_wrap() {
|
||||
var items = [];
|
||||
var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory");
|
||||
|
||||
while (stmnt.executeStep()) {
|
||||
var nam = stmnt.getUTF8String(1);
|
||||
var val = stmnt.getUTF8String(2);
|
||||
var key = Utils.sha1(nam + val);
|
||||
|
||||
items[key] = { name: nam, value: val };
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
wipe: function FormStore_wipe() {
|
||||
this._formHistory.removeAllEntries();
|
||||
},
|
||||
|
||||
resetGUIDs: function FormStore_resetGUIDs() {
|
||||
// Not needed.
|
||||
}
|
||||
};
|
||||
FormStore.prototype.__proto__ = new Store();
|
||||
|
|
|
@ -34,8 +34,7 @@
|
|||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore',
|
||||
'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore'];
|
||||
const EXPORTED_SYMBOLS = ['SyncCore'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
@ -227,7 +226,7 @@ SyncCore.prototype = {
|
|||
let conflicts = [[], []];
|
||||
let ret = {propagations: propagations, conflicts: conflicts};
|
||||
this._log.debug("Reconciling " + listA.length +
|
||||
" against " + listB.length + "commands");
|
||||
" against " + listB.length + " commands");
|
||||
|
||||
let guidChanges = [];
|
||||
for (let i = 0; i < listA.length; i++) {
|
||||
|
@ -311,274 +310,3 @@ SyncCore.prototype = {
|
|||
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;
|
||||
},
|
||||
|
||||
_itemExists: function BSC__itemExists(GUID) {
|
||||
return this._bms.getItemIdForGUID(GUID) >= 0;
|
||||
},
|
||||
|
||||
_getEdits: function BSC__getEdits(a, b) {
|
||||
// NOTE: we do not increment ret.numProps, as that would cause
|
||||
// edit commands to always get generated
|
||||
let ret = SyncCore.prototype._getEdits.call(this, a, b);
|
||||
ret.props.type = a.type;
|
||||
return ret;
|
||||
},
|
||||
|
||||
// compares properties
|
||||
// returns true if the property is not set in either object
|
||||
// returns true if the property is set and equal in both objects
|
||||
// returns false otherwise
|
||||
_comp: function BSC__comp(a, b, prop) {
|
||||
return (!a.data[prop] && !b.data[prop]) ||
|
||||
(a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop]));
|
||||
},
|
||||
|
||||
_commandLike: function BSC__commandLike(a, b) {
|
||||
// Check that neither command is null, that their actions, types,
|
||||
// and parents are the same, and that they don't have the same
|
||||
// GUID.
|
||||
// * Items with the same GUID do not qualify for 'likeness' because
|
||||
// we already consider them to be the same object, and therefore
|
||||
// we need to process any edits.
|
||||
// * Remove or edit commands don't qualify for likeness either,
|
||||
// since remove or edit commands with different GUIDs are
|
||||
// guaranteed to refer to two different items
|
||||
// * The parent GUID check works because reconcile() fixes up the
|
||||
// parent GUIDs as it runs, and the command list is sorted by
|
||||
// depth
|
||||
if (!a || !b ||
|
||||
a.action != b.action ||
|
||||
a.action != "create" ||
|
||||
a.data.type != b.data.type ||
|
||||
a.data.parentGUID != b.data.parentGUID ||
|
||||
a.GUID == b.GUID)
|
||||
return false;
|
||||
|
||||
// Bookmarks and folders are allowed to be in a different index as long as
|
||||
// they are in the same folder. Separators must be at
|
||||
// the same index to qualify for 'likeness'.
|
||||
switch (a.data.type) {
|
||||
case "bookmark":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "query":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "microsummary":
|
||||
if (this._comp(a, b, 'URI') &&
|
||||
this._comp(a, b, 'generatorURI'))
|
||||
return true;
|
||||
return false;
|
||||
case "folder":
|
||||
if (this._comp(a, b, 'title'))
|
||||
return true;
|
||||
return false;
|
||||
case "livemark":
|
||||
if (this._comp(a, b, 'title') &&
|
||||
this._comp(a, b, 'siteURI') &&
|
||||
this._comp(a, b, 'feedURI'))
|
||||
return true;
|
||||
return false;
|
||||
case "separator":
|
||||
if (this._comp(a, b, 'index'))
|
||||
return true;
|
||||
return false;
|
||||
default:
|
||||
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||
this._log.error("commandLike: Unknown item type: " + json.encode(a));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
BookmarksSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
function HistorySyncCore() {
|
||||
this._init();
|
||||
}
|
||||
HistorySyncCore.prototype = {
|
||||
_logName: "HistSync",
|
||||
|
||||
_itemExists: function BSC__itemExists(GUID) {
|
||||
// we don't care about already-existing items; just try to re-add them
|
||||
return false;
|
||||
},
|
||||
|
||||
_commandLike: function BSC_commandLike(a, b) {
|
||||
// History commands never qualify for likeness. We will always
|
||||
// take the union of all client/server items. We use the URL as
|
||||
// the GUID, so the same sites will map to the same item (same
|
||||
// GUID), without our intervention.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
HistorySyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
|
||||
function CookieSyncCore() {
|
||||
this._init();
|
||||
}
|
||||
CookieSyncCore.prototype = {
|
||||
_logName: "CookieSync",
|
||||
|
||||
__cookieManager: null,
|
||||
get _cookieManager() {
|
||||
if (!this.__cookieManager)
|
||||
this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"].
|
||||
getService(Ci.nsICookieManager2);
|
||||
/* need the 2nd revision of the ICookieManager interface
|
||||
because it supports add() and the 1st one doesn't. */
|
||||
return this.__cookieManager;
|
||||
},
|
||||
|
||||
|
||||
_itemExists: function CSC__itemExists(GUID) {
|
||||
/* true if a cookie with the given GUID exists.
|
||||
The GUID that we are passed should correspond to the keys
|
||||
that we define in the JSON returned by CookieStore.wrap()
|
||||
That is, it will be a string of the form
|
||||
"host:path:name". */
|
||||
|
||||
/* TODO verify that colons can't normally appear in any of
|
||||
the fields -- if they did it then we can't rely on .split(":")
|
||||
to parse correctly.*/
|
||||
|
||||
let cookieArray = GUID.split( ":" );
|
||||
let cookieHost = cookieArray[0];
|
||||
let cookiePath = cookieArray[1];
|
||||
let cookieName = cookieArray[2];
|
||||
|
||||
/* alternate implementation would be to instantiate a cookie from
|
||||
cookieHost, cookiePath, and cookieName, then call
|
||||
cookieManager.cookieExists(). Maybe that would have better
|
||||
performance? This implementation seems pretty slow.*/
|
||||
let enumerator = this._cookieManager.enumerator;
|
||||
while (enumerator.hasMoreElements())
|
||||
{
|
||||
let aCookie = enumerator.getNext();
|
||||
if (aCookie.host == cookieHost &&
|
||||
aCookie.path == cookiePath &&
|
||||
aCookie.name == cookieName ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
/* Note: We can't just call cookieManager.cookieExists() with a generic
|
||||
javascript object with .host, .path, and .name attributes attatched.
|
||||
cookieExists is implemented in C and does a hard static_cast to an
|
||||
nsCookie object, so duck typing doesn't work (and in fact makes
|
||||
Firefox hard-crash as the static_cast returns null and is not checked.)
|
||||
*/
|
||||
},
|
||||
|
||||
_commandLike: function CSC_commandLike(a, b) {
|
||||
/* Method required to be overridden.
|
||||
a and b each have a .data and a .GUID
|
||||
If this function returns true, an editCommand will be
|
||||
generated to try to resolve the thing.
|
||||
but are a and b objects of the type in the Store or
|
||||
are they "commands"?? */
|
||||
return false;
|
||||
}
|
||||
};
|
||||
CookieSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
|
||||
function PasswordSyncCore() {
|
||||
this._init();
|
||||
}
|
||||
PasswordSyncCore.prototype = {
|
||||
_logName: "PasswordSync",
|
||||
|
||||
__loginManager : null,
|
||||
get _loginManager() {
|
||||
if (!this.__loginManager)
|
||||
this.__loginManager = Cc["@mozilla.org/login-manager;1"].
|
||||
getService(Ci.nsILoginManager);
|
||||
return this.__loginManager;
|
||||
},
|
||||
|
||||
_itemExists: function PSC__itemExists(GUID) {
|
||||
var found = false;
|
||||
var logins = this._loginManager.getAllLogins({});
|
||||
|
||||
// XXX It would be more efficient to compute all the hashes in one shot,
|
||||
// cache the results, and check the cache here. That would need to happen
|
||||
// once per sync -- not sure how to invalidate cache after current sync?
|
||||
for (var i = 0; i < logins.length && !found; i++) {
|
||||
var hash = this._hashLoginInfo(logins[i]);
|
||||
if (hash == GUID)
|
||||
found = true;;
|
||||
}
|
||||
|
||||
return found;
|
||||
},
|
||||
|
||||
_commandLike: function PSC_commandLike(a, b) {
|
||||
// Not used.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
PasswordSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
||||
function FormSyncCore() {
|
||||
this._init();
|
||||
}
|
||||
FormSyncCore.prototype = {
|
||||
_logName: "FormSync",
|
||||
|
||||
__formDB: null,
|
||||
get _formDB() {
|
||||
if (!this.__formDB) {
|
||||
var file = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties).
|
||||
get("ProfD", Ci.nsIFile);
|
||||
file.append("formhistory.sqlite");
|
||||
var stor = Cc["@mozilla.org/storage/service;1"].
|
||||
getService(Ci.mozIStorageService);
|
||||
this.__formDB = stor.openDatabase(file);
|
||||
}
|
||||
return this.__formDB;
|
||||
},
|
||||
|
||||
_itemExists: function FSC__itemExists(GUID) {
|
||||
var found = false;
|
||||
var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory");
|
||||
|
||||
/* Same performance restrictions as PasswordSyncCore apply here:
|
||||
caching required */
|
||||
while (stmnt.executeStep()) {
|
||||
var nam = stmnt.getUTF8String(1);
|
||||
var val = stmnt.getUTF8String(2);
|
||||
var key = Utils.sha1(nam + val);
|
||||
|
||||
if (key == GUID)
|
||||
found = true;
|
||||
}
|
||||
|
||||
return found;
|
||||
},
|
||||
|
||||
_commandLike: function FSC_commandLike(a, b) {
|
||||
/* Not required as GUIDs for similar data sets will be the same */
|
||||
return false;
|
||||
}
|
||||
};
|
||||
FormSyncCore.prototype.__proto__ = new SyncCore();
|
||||
|
|
|
@ -34,8 +34,7 @@
|
|||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker',
|
||||
'FormsTracker', 'CookieTracker'];
|
||||
const EXPORTED_SYMBOLS = ['Tracker'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
@ -55,7 +54,7 @@ Function.prototype.async = Async.sugar;
|
|||
* listening for changes to their particular data type
|
||||
* and updating their 'score', indicating how urgently they
|
||||
* want to sync.
|
||||
*
|
||||
*
|
||||
* 'score's range from 0 (Nothing's changed)
|
||||
* to 100 (I need to sync now!)
|
||||
* -1 is also a valid score
|
||||
|
@ -93,210 +92,3 @@ Tracker.prototype = {
|
|||
this._score = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Tracker objects for each engine may need to subclass the
|
||||
* getScore routine, which returns the current 'score' for that
|
||||
* engine. How the engine decides to set the score is upto it,
|
||||
* as long as the value between 0 and 100 actually corresponds
|
||||
* to its urgency to sync.
|
||||
*
|
||||
* Here's an example BookmarksTracker. We don't subclass getScore
|
||||
* because the observer methods take care of updating _score which
|
||||
* getScore returns by default.
|
||||
*/
|
||||
function BookmarksTracker() {
|
||||
this._init();
|
||||
}
|
||||
BookmarksTracker.prototype = {
|
||||
_logName: "BMTracker",
|
||||
|
||||
/* We don't care about the first three */
|
||||
onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {
|
||||
|
||||
},
|
||||
onEndUpdateBatch: function BMT_onEndUpdateBatch() {
|
||||
|
||||
},
|
||||
onItemVisited: function BMT_onItemVisited() {
|
||||
|
||||
},
|
||||
|
||||
/* Every add or remove is worth 4 points,
|
||||
* on the basis that adding or removing 20 bookmarks
|
||||
* means its time to sync?
|
||||
*/
|
||||
onItemAdded: function BMT_onEndUpdateBatch() {
|
||||
this._score += 4;
|
||||
},
|
||||
onItemRemoved: function BMT_onItemRemoved() {
|
||||
this._score += 4;
|
||||
},
|
||||
/* Changes are worth 2 points? */
|
||||
onItemChanged: function BMT_onItemChanged() {
|
||||
this._score += 2;
|
||||
},
|
||||
|
||||
_init: function BMT__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
|
||||
Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService).
|
||||
addObserver(this, false);
|
||||
}
|
||||
}
|
||||
BookmarksTracker.prototype.__proto__ = new Tracker();
|
||||
|
||||
function HistoryTracker() {
|
||||
this._init();
|
||||
}
|
||||
HistoryTracker.prototype = {
|
||||
_logName: "HistoryTracker",
|
||||
|
||||
/* We don't care about the first four */
|
||||
onBeginUpdateBatch: function HT_onBeginUpdateBatch() {
|
||||
|
||||
},
|
||||
onEndUpdateBatch: function HT_onEndUpdateBatch() {
|
||||
|
||||
},
|
||||
onPageChanged: function HT_onPageChanged() {
|
||||
|
||||
},
|
||||
onTitleChanged: function HT_onTitleChanged() {
|
||||
|
||||
},
|
||||
|
||||
/* Every add or remove is worth 1 point.
|
||||
* Clearing the whole history is worth 50 points,
|
||||
* to ensure we're above the cutoff for syncing
|
||||
* ASAP.
|
||||
*/
|
||||
onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) {
|
||||
this._score += 1;
|
||||
},
|
||||
onPageExpired: function HT_onPageExpired(uri, time, entry) {
|
||||
this._score += 1;
|
||||
},
|
||||
onDeleteURI: function HT_onDeleteURI(uri) {
|
||||
this._score += 1;
|
||||
},
|
||||
onClearHistory: function HT_onClearHistory() {
|
||||
this._score += 50;
|
||||
},
|
||||
|
||||
_init: function HT__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
|
||||
Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService).
|
||||
addObserver(this, false);
|
||||
}
|
||||
}
|
||||
HistoryTracker.prototype.__proto__ = new Tracker();
|
||||
|
||||
function CookieTracker() {
|
||||
this._init();
|
||||
}
|
||||
CookieTracker.prototype = {
|
||||
_logName: "CookieTracker",
|
||||
|
||||
_init: function CT__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
/* cookieService can't register observers, but what we CAN do is
|
||||
register a general observer with the global observerService
|
||||
to watch for the 'cookie-changed' message. */
|
||||
let observerService = Cc["@mozilla.org/observer-service;1"].
|
||||
getService(Ci.nsIObserverService);
|
||||
observerService.addObserver( this, 'cookie-changed', false );
|
||||
},
|
||||
|
||||
// implement observe method to satisfy nsIObserver interface
|
||||
observe: function ( aSubject, aTopic, aData ) {
|
||||
/* This gets called when any cookie is added, changed, or removed.
|
||||
aData will contain a string "added", "changed", etc. to tell us which,
|
||||
but for now we can treat them all the same. aSubject is the new
|
||||
cookie object itself. */
|
||||
var newCookie = aSubject.QueryInterface( Ci.nsICookie2 );
|
||||
if ( newCookie ) {
|
||||
if ( !newCookie.isSession ) {
|
||||
/* Any modification to a persistent cookie is worth
|
||||
10 points out of 100. Ignore session cookies. */
|
||||
this._score += 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CookieTracker.prototype.__proto__ = new Tracker();
|
||||
|
||||
function FormsTracker() {
|
||||
this._init();
|
||||
}
|
||||
FormsTracker.prototype = {
|
||||
_logName: "FormsTracker",
|
||||
|
||||
__formDB: null,
|
||||
get _formDB() {
|
||||
if (!this.__formDB) {
|
||||
var file = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties).
|
||||
get("ProfD", Ci.nsIFile);
|
||||
file.append("formhistory.sqlite");
|
||||
var stor = Cc["@mozilla.org/storage/service;1"].
|
||||
getService(Ci.mozIStorageService);
|
||||
this.__formDB = stor.openDatabase(file);
|
||||
}
|
||||
|
||||
return this.__formDB;
|
||||
},
|
||||
|
||||
/* nsIFormSubmitObserver is not available in JS.
|
||||
* To calculate scores, we instead just count the changes in
|
||||
* the database since the last time we were asked.
|
||||
*
|
||||
* FIXME!: Buggy, because changes in a row doesn't result in
|
||||
* an increment of our score. A possible fix is to do a
|
||||
* SELECT for each fieldname and compare those instead of the
|
||||
* whole row count.
|
||||
*
|
||||
* Each change is worth 2 points. At some point, we may
|
||||
* want to differentiate between search-history rows and other
|
||||
* form items, and assign different scores.
|
||||
*/
|
||||
_rowCount: 0,
|
||||
get score() {
|
||||
var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory");
|
||||
stmnt.executeStep();
|
||||
var count = stmnt.getInt32(0);
|
||||
stmnt.reset();
|
||||
|
||||
this._score = Math.abs(this._rowCount - count) * 2;
|
||||
|
||||
if (this._score >= 100)
|
||||
return 100;
|
||||
else
|
||||
return this._score;
|
||||
},
|
||||
|
||||
resetScore: function FormsTracker_resetScore() {
|
||||
var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory");
|
||||
stmnt.executeStep();
|
||||
this._rowCount = stmnt.getInt32(0);
|
||||
stmnt.reset();
|
||||
this._score = 0;
|
||||
},
|
||||
|
||||
_init: function FormsTracker__init() {
|
||||
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
|
||||
this._score = 0;
|
||||
|
||||
var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory");
|
||||
stmnt.executeStep();
|
||||
this._rowCount = stmnt.getInt32(0);
|
||||
stmnt.reset();
|
||||
}
|
||||
}
|
||||
FormsTracker.prototype.__proto__ = new Tracker();
|
||||
|
|
|
@ -1,72 +1,76 @@
|
|||
const EXPORTED_SYMBOLS = [ "PlainAuthenticator", "Md5DigestAuthenticator" ];
|
||||
|
||||
if(typeof(atob) == 'undefined') {
|
||||
// This code was written by Tyler Akins and has been placed in the
|
||||
// public domain. It would be nice if you left this header intact.
|
||||
// Base64 code from Tyler Akins -- http://rumkin.com
|
||||
|
||||
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
|
||||
function btoa(input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
chr1 = input.charCodeAt(i++);
|
||||
chr2 = input.charCodeAt(i++);
|
||||
chr3 = input.charCodeAt(i++);
|
||||
|
||||
enc1 = chr1 >> 2;
|
||||
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||||
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||||
enc4 = chr3 & 63;
|
||||
|
||||
if (isNaN(chr2)) {
|
||||
enc3 = enc4 = 64;
|
||||
} else if (isNaN(chr3)) {
|
||||
enc4 = 64;
|
||||
}
|
||||
|
||||
output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
|
||||
keyStr.charAt(enc3) + keyStr.charAt(enc4);
|
||||
} while (i < input.length);
|
||||
|
||||
return output;
|
||||
function LOG(aMsg) {
|
||||
dump("Weave::AuthenticationLayer: " + aMsg + "\n");
|
||||
}
|
||||
|
||||
function atob(input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
if (typeof(atob) == 'undefined') {
|
||||
// This code was written by Tyler Akins and has been placed in the
|
||||
// public domain. It would be nice if you left this header intact.
|
||||
// Base64 code from Tyler Akins -- http://rumkin.com
|
||||
|
||||
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
|
||||
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
|
||||
do {
|
||||
enc1 = keyStr.indexOf(input.charAt(i++));
|
||||
enc2 = keyStr.indexOf(input.charAt(i++));
|
||||
enc3 = keyStr.indexOf(input.charAt(i++));
|
||||
enc4 = keyStr.indexOf(input.charAt(i++));
|
||||
function btoa(input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
|
||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||
do {
|
||||
chr1 = input.charCodeAt(i++);
|
||||
chr2 = input.charCodeAt(i++);
|
||||
chr3 = input.charCodeAt(i++);
|
||||
|
||||
output = output + String.fromCharCode(chr1);
|
||||
enc1 = chr1 >> 2;
|
||||
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||||
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||||
enc4 = chr3 & 63;
|
||||
|
||||
if (enc3 != 64) {
|
||||
output = output + String.fromCharCode(chr2);
|
||||
}
|
||||
if (enc4 != 64) {
|
||||
output = output + String.fromCharCode(chr3);
|
||||
}
|
||||
} while (i < input.length);
|
||||
if (isNaN(chr2)) {
|
||||
enc3 = enc4 = 64;
|
||||
} else if (isNaN(chr3)) {
|
||||
enc4 = 64;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
|
||||
keyStr.charAt(enc3) + keyStr.charAt(enc4);
|
||||
} while (i < input.length);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function atob(input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
|
||||
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
|
||||
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||
|
||||
do {
|
||||
enc1 = keyStr.indexOf(input.charAt(i++));
|
||||
enc2 = keyStr.indexOf(input.charAt(i++));
|
||||
enc3 = keyStr.indexOf(input.charAt(i++));
|
||||
enc4 = keyStr.indexOf(input.charAt(i++));
|
||||
|
||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||
|
||||
output = output + String.fromCharCode(chr1);
|
||||
|
||||
if (enc3 != 64) {
|
||||
output = output + String.fromCharCode(chr2);
|
||||
}
|
||||
if (enc4 != 64) {
|
||||
output = output + String.fromCharCode(chr3);
|
||||
}
|
||||
} while (i < input.length);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -75,12 +79,16 @@ function atob(input) {
|
|||
|
||||
|
||||
Here's the interface that each implementation must obey:
|
||||
initialize( clientName, clientRealm, clientPassword );
|
||||
|
||||
generateResponse( rootElem );
|
||||
{
|
||||
initialize( clientName, clientRealm, clientPassword );
|
||||
|
||||
generateResponse( rootElem );
|
||||
|
||||
// returns text of error message
|
||||
getError();
|
||||
}
|
||||
|
||||
getError();
|
||||
returns text of error message
|
||||
*/
|
||||
|
||||
function BaseAuthenticator() {
|
||||
|
@ -94,14 +102,14 @@ BaseAuthenticator.prototype = {
|
|||
this._password = password;
|
||||
this._stepNumber = 0;
|
||||
this._errorMsg = "";
|
||||
},
|
||||
},
|
||||
|
||||
getError: function () {
|
||||
/* Returns text of most recent error message.
|
||||
Client code should call this if generateResponse() returns false
|
||||
to see what the problem was. */
|
||||
return this._errorMsg;
|
||||
},
|
||||
},
|
||||
|
||||
generateResponse: function( rootElem ) {
|
||||
/* Subclasses must override this. rootElem is a DOM node which is
|
||||
|
@ -113,7 +121,7 @@ BaseAuthenticator.prototype = {
|
|||
|
||||
this._errorMsg = "generateResponse() should be overridden by subclass.";
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
verifyProtocolSupport: function( rootElem, protocolName ) {
|
||||
/* Parses the incoming stream from the server to check whether the
|
||||
|
@ -152,7 +160,7 @@ BaseAuthenticator.prototype = {
|
|||
var mechanisms = child.getElementsByTagName( "mechanism" );
|
||||
for ( var x = 0; x < mechanisms.length; x++ ) {
|
||||
if ( mechanisms[x].firstChild.nodeValue == protocolName ) {
|
||||
protocolSupported = true;
|
||||
protocolSupported = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,7 +169,7 @@ BaseAuthenticator.prototype = {
|
|||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
@ -180,15 +188,15 @@ function Md5DigestAuthenticator( ) {
|
|||
}
|
||||
Md5DigestAuthenticator.prototype = {
|
||||
|
||||
_makeCNonce: function( ) {
|
||||
_makeCNonce: function( ) {
|
||||
return "\"" + Math.floor( 10000000000 * Math.random() ) + "\"";
|
||||
},
|
||||
|
||||
generateResponse: function Md5__generateResponse( rootElem ) {
|
||||
|
||||
generateResponse: function Md5__generateResponse( rootElem ) {
|
||||
if ( this._stepNumber == 0 ) {
|
||||
|
||||
if ( this.verifyProtocolSupport( rootElem, "DIGEST-MD5" ) == false ) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
// SASL step 1: request that we use DIGEST-MD5 authentication.
|
||||
this._stepNumber = 1;
|
||||
|
@ -204,21 +212,21 @@ Md5DigestAuthenticator.prototype = {
|
|||
|
||||
// Now i have the nonce: make a digest-response out of
|
||||
/* username: required
|
||||
realm: only needed if realm is in challenge
|
||||
nonce: required, just as recieved
|
||||
cnonce: required, opaque quoted string, 64 bits entropy
|
||||
nonce-count: optional
|
||||
qop: (quality of protection) optional
|
||||
serv-type: optional?
|
||||
host: optional?
|
||||
serv-name: optional?
|
||||
digest-uri: "service/host/serv-name" (replaces those three?)
|
||||
response: required (32 lowercase hex),
|
||||
maxbuf: optional,
|
||||
charset,
|
||||
LHEX (32 hex digits = ??),
|
||||
cipher: required if auth-conf is negotiatedd??
|
||||
authzid: optional
|
||||
realm: only needed if realm is in challenge
|
||||
nonce: required, just as recieved
|
||||
cnonce: required, opaque quoted string, 64 bits entropy
|
||||
nonce-count: optional
|
||||
qop: (quality of protection) optional
|
||||
serv-type: optional?
|
||||
host: optional?
|
||||
serv-name: optional?
|
||||
digest-uri: "service/host/serv-name" (replaces those three?)
|
||||
response: required (32 lowercase hex),
|
||||
maxbuf: optional,
|
||||
charset,
|
||||
LHEX (32 hex digits = ??),
|
||||
cipher: required if auth-conf is negotiatedd??
|
||||
authzid: optional
|
||||
*/
|
||||
|
||||
|
||||
|
@ -252,8 +260,8 @@ Md5DigestAuthenticator.prototype = {
|
|||
// At this point the server might reject us with a
|
||||
// <failure><not-authorized/></failure>
|
||||
if ( rootElem.nodeName == "failure" ) {
|
||||
this._errorMsg = rootElem.firstChild.nodeName;
|
||||
return false;
|
||||
this._errorMsg = rootElem.firstChild.nodeName;
|
||||
return false;
|
||||
}
|
||||
//this._connectionStatus = this.REQUESTED_SASL_3;
|
||||
}
|
||||
|
@ -261,7 +269,7 @@ Md5DigestAuthenticator.prototype = {
|
|||
return false;
|
||||
},
|
||||
|
||||
_unpackChallenge: function( challengeString ) {
|
||||
_unpackChallenge: function( challengeString ) {
|
||||
var challenge = atob( challengeString );
|
||||
dump( "After b64 decoding: " + challenge + "\n" );
|
||||
var challengeItemStrings = challenge.split( "," );
|
||||
|
@ -273,7 +281,7 @@ Md5DigestAuthenticator.prototype = {
|
|||
return challengeItems;
|
||||
},
|
||||
|
||||
_packChallengeResponse: function( responseDict ) {
|
||||
_packChallengeResponse: function( responseDict ) {
|
||||
var responseArray = []
|
||||
for( var x in responseDict ) {
|
||||
responseArray.push( x + "=" + responseDict[x] );
|
||||
|
@ -292,53 +300,59 @@ function PlainAuthenticator( ) {
|
|||
}
|
||||
PlainAuthenticator.prototype = {
|
||||
|
||||
generateResponse: function( rootElem ) {
|
||||
generateResponse: function( rootElem ) {
|
||||
if ( this._stepNumber == 0 ) {
|
||||
if ( this.verifyProtocolSupport( rootElem, "PLAIN" ) == false ) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
var authString = btoa( this._realm + '\0' + this._name + '\0' + this._password );
|
||||
this._stepNumber = 1;
|
||||
|
||||
// XXX why does this not match the stanzas in XEP-025?
|
||||
return "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" + authString + "</auth>";
|
||||
|
||||
} else if ( this._stepNumber == 1 ) {
|
||||
if ( rootElem.nodeName == "failure" ) {
|
||||
// Authentication rejected: username or password may be wrong.
|
||||
this._errorMsg = rootElem.firstChild.nodeName;
|
||||
return false;
|
||||
// Authentication rejected: username or password may be wrong.
|
||||
this._errorMsg = rootElem.firstChild.nodeName;
|
||||
return false;
|
||||
} else if ( rootElem.nodeName == "success" ) {
|
||||
// Authentication accepted: now we start a new stream for
|
||||
// resource binding.
|
||||
/* RFC3920 part 7 says: upon receiving a success indication within the
|
||||
SASL negotiation, the client MUST send a new stream header to the
|
||||
server, to which the serer MUST respond with a stream header
|
||||
as well as a list of available stream features. */
|
||||
// TODO: resource binding happens in any authentication mechanism
|
||||
// so should be moved to base class.
|
||||
this._stepNumber = 2;
|
||||
return "<?xml version='1.0'?><stream:stream to='jonathan-dicarlos-macbook-pro.local' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
|
||||
// Authentication accepted: now we start a new stream for
|
||||
// resource binding.
|
||||
/* RFC3920 part 7 says: upon receiving a success indication within the
|
||||
SASL negotiation, the client MUST send a new stream header to the
|
||||
server, to which the serer MUST respond with a stream header
|
||||
as well as a list of available stream features. */
|
||||
// TODO: resource binding happens in any authentication mechanism
|
||||
// so should be moved to base class.
|
||||
this._stepNumber = 2;
|
||||
return "<?xml version='1.0'?><stream:stream to='" +
|
||||
this._realm +
|
||||
"' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
|
||||
}
|
||||
} else if ( this._stepNumber == 2 ) {
|
||||
// See if the server is asking us to bind a resource, and if it's
|
||||
// asking us to start a session:
|
||||
var bindNodes = rootElem.getElementsByTagName( "bind" );
|
||||
if ( bindNodes.length > 0 ) {
|
||||
this._needBinding = true;
|
||||
this._needBinding = true;
|
||||
}
|
||||
|
||||
var sessionNodes = rootElem.getElementsByTagName( "session" );
|
||||
if ( sessionNodes.length > 0 ) {
|
||||
this._needSession = true;
|
||||
this._needSession = true;
|
||||
}
|
||||
|
||||
if ( !this._needBinding && !this._needSession ) {
|
||||
// Server hasn't requested either: we're done.
|
||||
return this.COMPLETION_CODE;
|
||||
// Server hasn't requested either: we're done.
|
||||
return this.COMPLETION_CODE;
|
||||
}
|
||||
|
||||
if ( this._needBinding ) {
|
||||
// Do resource binding:
|
||||
// Tell the server to generate the resource ID for us.
|
||||
this._stepNumber = 3;
|
||||
return "<iq type='set' id='bind_1'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>";
|
||||
// Do resource binding:
|
||||
// Tell the server to generate the resource ID for us.
|
||||
this._stepNumber = 3;
|
||||
return "<iq type='set' id='bind_1'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>";
|
||||
}
|
||||
|
||||
this._errorMsg = "Server requested session not binding: can't happen?";
|
||||
|
@ -347,20 +361,20 @@ PlainAuthenticator.prototype = {
|
|||
// Pull the JID out of the stuff the server sends us.
|
||||
var jidNodes = rootElem.getElementsByTagName( "jid" );
|
||||
if ( jidNodes.length == 0 ) {
|
||||
this._errorMsg = "Expected JID node from server, got none.";
|
||||
return false;
|
||||
this._errorMsg = "Expected JID node from server, got none.";
|
||||
return false;
|
||||
}
|
||||
this._jid = jidNodes[0].firstChild.nodeValue;
|
||||
// TODO: Does the client need to do anything special with its new
|
||||
// "client@host.com/resourceID" full JID?
|
||||
dump( "JID set to " + this._jid );
|
||||
LOG( "JID set to " + this._jid );
|
||||
|
||||
// If we still need to do session, then we're not done yet:
|
||||
if ( this._needSession ) {
|
||||
this._stepNumber = 4;
|
||||
return "<iq to='" + this._realm + "' type='set' id='sess_1'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>";
|
||||
this._stepNumber = 4;
|
||||
return "<iq to='" + this._realm + "' type='set' id='sess_1'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>";
|
||||
} else {
|
||||
return this.COMPLETION_CODE;
|
||||
return this.COMPLETION_CODE;
|
||||
}
|
||||
} else if ( this._stepNumber == 4 ) {
|
||||
// OK, now we're done.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
About the XMPP module
|
||||
|
||||
Here is sample code demonstrating how client code can use the XMPP module. It assumes that a Jabber server is running on localhost on port 5280.
|
||||
Here is sample code demonstrating how client code can use the XMPP module.
|
||||
It assumes that a Jabber server capable of HTTP-Polling is running on localhost
|
||||
on port 5280.
|
||||
|
||||
Components.utils.import( "resource://weave/xmpp/xmppClient.js" );
|
||||
|
||||
|
@ -66,7 +68,6 @@ The ejabberd process is started simply by running:
|
|||
ejabberd/bin/ejabberdctl start
|
||||
|
||||
|
||||
|
||||
Outstanding Issues -- bugs and things to do.
|
||||
|
||||
* The test above is failing with a timeout. How to debug this? Let's start
|
||||
|
@ -122,4 +123,3 @@ Outstanding Issues -- bugs and things to do.
|
|||
(Everything seems to be working OK with useKeys turned off, but that's less
|
||||
secure.)
|
||||
|
||||
|
||||
|
|
|
@ -3,26 +3,41 @@ const EXPORTED_SYMBOLS = ['HTTPPollingTransport'];
|
|||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
|
||||
function LOG(aMsg) {
|
||||
dump("Weave::Transport-HTTP-Poll: " + aMsg + "\n");
|
||||
}
|
||||
|
||||
/*
|
||||
The interface that should be implemented by any Transport object:
|
||||
|
||||
send( messageXml );
|
||||
setCallbackObject(object with .onIncomingData(aStringData) and .onTransportError(aErrorText) );
|
||||
connect();
|
||||
disconnect();
|
||||
*/
|
||||
|
||||
function InputStreamBuffer() {
|
||||
}
|
||||
InputStreamBuffer.prototype = {
|
||||
_data: "",
|
||||
append: function( stuff ) {
|
||||
_data: "",
|
||||
append: function( stuff ) {
|
||||
this._data = this._data + stuff;
|
||||
},
|
||||
clear: function() {
|
||||
clear: function() {
|
||||
this._data = "";
|
||||
},
|
||||
getData: function() {
|
||||
getData: function() {
|
||||
return this._data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A transport layer that uses raw sockets.
|
||||
* Not recommended for use; currently fails when trying to negotiate
|
||||
* TLS.
|
||||
* Use HTTPPollingTransport instead.
|
||||
*/
|
||||
function SocketClient( host, port ) {
|
||||
/* A transport layer that uses raw sockets.
|
||||
Not recommended for use; currently fails when trying to negotiate
|
||||
TLS.
|
||||
Use HTTPPollingTransport instead. */
|
||||
this._init( host, port );
|
||||
}
|
||||
SocketClient.prototype = {
|
||||
|
@ -32,6 +47,7 @@ SocketClient.prototype = {
|
|||
this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
return this.__threadManager;
|
||||
},
|
||||
|
||||
__transport: null,
|
||||
get _transport() {
|
||||
if (!this.__transport) {
|
||||
|
@ -45,7 +61,7 @@ SocketClient.prototype = {
|
|||
return this.__transport;
|
||||
},
|
||||
|
||||
_init: function( host, port ) {
|
||||
_init: function( host, port ) {
|
||||
this._host = host;
|
||||
this._port = port;
|
||||
this._contentRead = "";
|
||||
|
@ -53,7 +69,7 @@ SocketClient.prototype = {
|
|||
this.connect();
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
connect: function() {
|
||||
var outstream = this._transport.openOutputStream( 0, // flags
|
||||
0, // buffer size
|
||||
0 ); // number of buffers
|
||||
|
@ -75,18 +91,18 @@ SocketClient.prototype = {
|
|||
|
||||
// create dataListener class for callback:
|
||||
var dataListener = {
|
||||
data : "",
|
||||
onStartRequest: function(request, context){
|
||||
data : "",
|
||||
onStartRequest: function(request, context){
|
||||
},
|
||||
onStopRequest: function(request, context, status){
|
||||
rawInputStream.close();
|
||||
outstream.close();
|
||||
onStopRequest: function(request, context, status){
|
||||
rawInputStream.close();
|
||||
outstream.close();
|
||||
},
|
||||
onDataAvailable: function(request, context, inputStream, offset, count){
|
||||
// use scriptable stream wrapper, not "real" stream.
|
||||
// count is number of bytes available, offset is position in stream.
|
||||
// Do stuff with data here!
|
||||
buffer.append( scriptablestream.read( count ));
|
||||
onDataAvailable: function(request, context, inputStream, offset, count){
|
||||
// use scriptable stream wrapper, not "real" stream.
|
||||
// count is number of bytes available, offset is position in stream.
|
||||
// Do stuff with data here!
|
||||
buffer.append( scriptablestream.read( count ));
|
||||
}
|
||||
};
|
||||
// register it:
|
||||
|
@ -108,22 +124,20 @@ SocketClient.prototype = {
|
|||
|
||||
disconnect: function() {
|
||||
var thread = this._threadManager.currentThread;
|
||||
while( thread.hasPendingEvents() )
|
||||
{
|
||||
thread.processNextEvent( true );
|
||||
}
|
||||
while ( thread.hasPendingEvents() ) {
|
||||
thread.processNextEvent( true );
|
||||
}
|
||||
},
|
||||
|
||||
checkResponse: function() {
|
||||
checkResponse: function() {
|
||||
return this._getData();
|
||||
},
|
||||
|
||||
waitForResponse: function() {
|
||||
waitForResponse: function() {
|
||||
var thread = this._threadManager.currentThread;
|
||||
while( this._buffer.getData().length == 0 )
|
||||
{
|
||||
thread.processNextEvent( true );
|
||||
}
|
||||
while( this._buffer.getData().length == 0 ) {
|
||||
thread.processNextEvent( true );
|
||||
}
|
||||
var output = this._buffer.getData();
|
||||
this._buffer.clear();
|
||||
return output;
|
||||
|
@ -132,30 +146,24 @@ SocketClient.prototype = {
|
|||
startTLS: function() {
|
||||
this._transport.securityInfo.QueryInterface(Ci.nsISSLSocketControl);
|
||||
this._transport.securityInfo.StartTLS();
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* The interface that should be implemented by any Transport object:
|
||||
send( messageXml );
|
||||
setCallbackObject( object with .onIncomingData and .onTransportError );
|
||||
connect();
|
||||
disconnect();
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send HTTP requests periodically to the server using a timer.
|
||||
* HTTP POST requests with content-type application/x-www-form-urlencoded.
|
||||
* responses from the server have content-type text/xml
|
||||
* request and response are UTF-8 encoded (ignore what HTTP header says)
|
||||
* identify session by always using set-cookie header with cookie named ID
|
||||
* first request sets this to 0 to indicate new session.
|
||||
*/
|
||||
function HTTPPollingTransport( serverUrl, useKeys, interval ) {
|
||||
/* Send HTTP requests periodically to the server using a timer.
|
||||
HTTP POST requests with content-type application/x-www-form-urlencoded.
|
||||
responses from the server have content-type text/xml
|
||||
request and response are UTF-8 encoded (ignore what HTTP header says)
|
||||
identify session by always using set-cookie header with cookie named ID
|
||||
first request sets this to 0 to indicate new session. */
|
||||
|
||||
this._init( serverUrl, useKeys, interval );
|
||||
}
|
||||
HTTPPollingTransport.prototype = {
|
||||
_init: function( serverUrl, useKeys, interval ) {
|
||||
_init: function( serverUrl, useKeys, interval ) {
|
||||
LOG("Initializing transport: serverUrl=" + serverUrl + ", useKeys=" + useKeys + ", interval=" + interval);
|
||||
this._serverUrl = serverUrl
|
||||
this._n = 0;
|
||||
this._key = this._makeSeed();
|
||||
|
@ -165,43 +173,47 @@ HTTPPollingTransport.prototype = {
|
|||
this._useKeys = useKeys;
|
||||
this._interval = interval;
|
||||
this._outgoingRetryBuffer = "";
|
||||
this._retryCount = 0;
|
||||
this._retryCap = 0;
|
||||
},
|
||||
|
||||
__request: null,
|
||||
get _request() {
|
||||
__request: null,
|
||||
get _request() {
|
||||
if (!this.__request)
|
||||
this.__request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance( Ci.nsIXMLHttpRequest );
|
||||
return this.__request;
|
||||
},
|
||||
|
||||
__hasher: null,
|
||||
get _hasher() {
|
||||
__hasher: null,
|
||||
get _hasher() {
|
||||
if (!this.__hasher)
|
||||
this.__hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash );
|
||||
return this.__hasher;
|
||||
},
|
||||
|
||||
__timer: null,
|
||||
get _timer() {
|
||||
__timer: null,
|
||||
get _timer() {
|
||||
if (!this.__timer)
|
||||
this.__timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer );
|
||||
return this.__timer;
|
||||
},
|
||||
|
||||
_makeSeed: function() {
|
||||
_makeSeed: function() {
|
||||
return "foo";//"MyKeyOfHorrors";
|
||||
},
|
||||
|
||||
_advanceKey: function() {
|
||||
var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
_advanceKey: function() {
|
||||
var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
|
||||
createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
|
||||
// we use UTF-8 here, you can choose other encodings.
|
||||
// TODO make configurable
|
||||
converter.charset = "UTF-8";
|
||||
// result is an out parameter,
|
||||
// result.value will contain the array length
|
||||
var result = {};
|
||||
// data is an array of bytes
|
||||
var data = converter.convertToByteArray( this._key, result);
|
||||
var data = converter.convertToByteArray(this._key, result);
|
||||
|
||||
this._n += 1;
|
||||
this._hasher.initWithString( "SHA1" );
|
||||
|
@ -209,8 +221,8 @@ HTTPPollingTransport.prototype = {
|
|||
this._key = this._hasher.finish( true ); // true means B64encode
|
||||
},
|
||||
|
||||
_setIdFromCookie: function( self, cookie ) {
|
||||
// parse connection ID out of the cookie:
|
||||
_setIdFromCookie: function( self, cookie ) {
|
||||
// parse connection ID out of the cookie:
|
||||
// dump( "Cookie is " + cookie + "\n" );
|
||||
var cookieSegments = cookie.split( ";" );
|
||||
cookieSegments = cookieSegments[0].split( "=" );
|
||||
|
@ -235,7 +247,7 @@ HTTPPollingTransport.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_onError: function( errorText ) {
|
||||
_onError: function( errorText ) {
|
||||
dump( "Transport error: " + errorText + "\n" );
|
||||
if ( this._callbackObject != null ) {
|
||||
this._callbackObject.onTransportError( errorText );
|
||||
|
@ -243,72 +255,88 @@ HTTPPollingTransport.prototype = {
|
|||
this.disconnect();
|
||||
},
|
||||
|
||||
_doPost: function( requestXml ) {
|
||||
var request = this._request;
|
||||
_doPost: function( requestXml ) {
|
||||
var request = this._request;
|
||||
var callbackObj = this._callbackObject;
|
||||
var self = this;
|
||||
var contents = "";
|
||||
|
||||
|
||||
if ( this._useKey ) {
|
||||
this._advanceKey();
|
||||
contents = this._connectionId + ";" + this._key + "," + requestXml;
|
||||
} else {
|
||||
contents = this._connectionId + "," + requestXml;
|
||||
/* TODO:
|
||||
Currently I get a "-3:0" error (key sequence error) from the 2nd
|
||||
exchange if using the keys is enabled. */
|
||||
Currently I get a "-3:0" error (key sequence error) from the 2nd
|
||||
exchange if using the keys is enabled. */
|
||||
}
|
||||
|
||||
_processReqChange = function( ) {
|
||||
//Callback for XMLHTTPRequest object state change messages
|
||||
var _processReqChange = function() {
|
||||
// Callback for XMLHTTPRequest object state change messages
|
||||
if ( request.readyState == 4 ) {
|
||||
if ( request.status == 200) {
|
||||
// 200 means success.
|
||||
|
||||
dump( "Server says: " + request.responseText + "\n" );
|
||||
// Look for a set-cookie header:
|
||||
var latestCookie = request.getResponseHeader( "Set-Cookie" );
|
||||
if ( latestCookie.length > 0 ) {
|
||||
self._setIdFromCookie( self, latestCookie );
|
||||
}
|
||||
// Respond to any text we get back from the server in response
|
||||
if ( callbackObj != null && request.responseText.length > 0 ) {
|
||||
callbackObj.onIncomingData( request.responseText );
|
||||
}
|
||||
} else {
|
||||
dump ( "Error! Got HTTP status code " + request.status + "\n" );
|
||||
if ( request.status == 0 ) {
|
||||
/* Sometimes the server gives us HTTP status code 0 in response
|
||||
to an attempt to POST. I'm not sure why this happens, but
|
||||
if we re-send the POST it seems to usually work the second
|
||||
time. So put the message into a buffer and try again later:
|
||||
*/
|
||||
self._outgoingRetryBuffer = requestXml;
|
||||
}
|
||||
}
|
||||
if ( request.status == 200) {
|
||||
// 200 means success.
|
||||
|
||||
LOG("Server says: " + request.responseText);
|
||||
// Look for a set-cookie header:
|
||||
var latestCookie = request.getResponseHeader( "Set-Cookie" );
|
||||
if ( latestCookie.length > 0 ) {
|
||||
self._setIdFromCookie( self, latestCookie );
|
||||
}
|
||||
|
||||
// Respond to any text we get back from the server in response
|
||||
if ( callbackObj != null && request.responseText.length > 0 ) {
|
||||
callbackObj.onIncomingData( request.responseText );
|
||||
}
|
||||
} else {
|
||||
LOG( "Error! Got HTTP status code " + request.status );
|
||||
if ( request.status == 0 ) {
|
||||
/* Sometimes the server gives us HTTP status code 0 in response
|
||||
to an attempt to POST. I'm not sure why this happens, but
|
||||
if we re-send the POST it seems to usually work the second
|
||||
time. So put the message into a buffer and try again later:
|
||||
*/
|
||||
if (self._retryCount >= self._retryCap) {
|
||||
self._onError("Maximum number of retries reached. Unable to communicate with the server.");
|
||||
}
|
||||
else {
|
||||
self._outgoingRetryBuffer = requestXml;
|
||||
self._retryCount++;
|
||||
}
|
||||
}
|
||||
else if (request.status == 404) {
|
||||
self._onError("Provided URL is not valid.");
|
||||
}
|
||||
else {
|
||||
self._onError("Unable to communicate with the server.");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.open( "POST", this._serverUrl, true ); //async = true
|
||||
request.setRequestHeader( "Content-type",
|
||||
"application/x-www-form-urlencoded;charset=UTF-8" );
|
||||
request.setRequestHeader( "Content-length", contents.length );
|
||||
request.setRequestHeader( "Connection", "close" );
|
||||
request.onreadystatechange = _processReqChange;
|
||||
dump( "Sending: " + contents + "\n" );
|
||||
request.send( contents );
|
||||
try {
|
||||
request.open( "POST", this._serverUrl, true ); //async = true
|
||||
request.setRequestHeader( "Content-type", "application/x-www-form-urlencoded;charset=UTF-8" );
|
||||
request.setRequestHeader( "Content-length", contents.length );
|
||||
request.setRequestHeader( "Connection", "close" );
|
||||
request.onreadystatechange = _processReqChange;
|
||||
LOG("Sending: " + contents);
|
||||
request.send( contents );
|
||||
} catch(ex) {
|
||||
this._onError("Unable to send message to server: " + this._serverUrl);
|
||||
LOG("Connection failure: " + ex);
|
||||
}
|
||||
},
|
||||
|
||||
send: function( messageXml ) {
|
||||
send: function( messageXml ) {
|
||||
this._doPost( messageXml );
|
||||
},
|
||||
|
||||
setCallbackObject: function( callbackObject ) {
|
||||
this._callbackObject = callbackObject;
|
||||
setCallbackObject: function( callbackObject ) {
|
||||
this._callbackObject = callbackObject;
|
||||
},
|
||||
|
||||
notify: function( timer ) {
|
||||
notify: function( timer ) {
|
||||
/* having a notify method makes this object satisfy the nsITimerCallback
|
||||
interface, so the object can be passed to timer.initWithCallback. */
|
||||
|
||||
|
@ -322,29 +350,31 @@ HTTPPollingTransport.prototype = {
|
|||
this._doPost( outgoingMsg );
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
/* Set up a timer to poll the server periodically. */
|
||||
connect: function() {
|
||||
// In case this is a reconnect, make sure to re-initialize.
|
||||
this._init(this._serverUrl, this._useKeys, this._interval);
|
||||
|
||||
// TODO doPost isn't reentrant; don't try to doPost if there's
|
||||
//already a post in progress... or can that never happen?
|
||||
/* Set up a timer to poll the server periodically. */
|
||||
|
||||
this._timer.initWithCallback( this,
|
||||
this._interval,
|
||||
this._timer.TYPE_REPEATING_SLACK );
|
||||
// TODO doPost isn't reentrant; don't try to doPost if there's
|
||||
//already a post in progress... or can that never happen?
|
||||
|
||||
this._timer.initWithCallback( this,
|
||||
this._interval,
|
||||
this._timer.TYPE_REPEATING_SLACK );
|
||||
},
|
||||
|
||||
disconnect: function () {
|
||||
disconnect: function () {
|
||||
this._request.abort();
|
||||
this._timer.cancel();
|
||||
},
|
||||
|
||||
testKeys: function () {
|
||||
|
||||
testKeys: function () {
|
||||
this._key = "foo";
|
||||
dump( this._key + "\n" );
|
||||
LOG(this._key);
|
||||
for ( var x = 1; x < 7; x++ ) {
|
||||
this._advanceKey();
|
||||
dump( this._key + "\n" );
|
||||
LOG(this._key);
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,10 +7,17 @@ const EXPORTED_SYMBOLS = ['XmppClient', 'HTTPPollingTransport', 'PlainAuthentica
|
|||
// http://developer.mozilla.org/en/docs/xpcshell
|
||||
// http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests
|
||||
|
||||
// IM level protocol stuff: presence announcements, conversations, etc.
|
||||
// ftp://ftp.isi.edu/in-notes/rfc3921.txt
|
||||
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
var Cu = Components.utils;
|
||||
|
||||
function LOG(aMsg) {
|
||||
dump("Weave::XMPPClient: " + aMsg + "\n");
|
||||
}
|
||||
|
||||
Cu.import("resource://weave/xmpp/transportLayer.js");
|
||||
Cu.import("resource://weave/xmpp/authenticationLayer.js");
|
||||
|
||||
|
@ -18,19 +25,19 @@ function XmppClient( clientName, realm, clientPassword, transport, authenticator
|
|||
this._init( clientName, realm, clientPassword, transport, authenticator );
|
||||
}
|
||||
XmppClient.prototype = {
|
||||
//connection status codes:
|
||||
NOT_CONNECTED: 0,
|
||||
CALLED_SERVER: 1,
|
||||
AUTHENTICATING: 2,
|
||||
CONNECTED: 3,
|
||||
FAILED: -1,
|
||||
//connection status codes:
|
||||
NOT_CONNECTED: 0,
|
||||
CALLED_SERVER: 1,
|
||||
AUTHENTICATING: 2,
|
||||
CONNECTED: 3,
|
||||
FAILED: -1,
|
||||
|
||||
// IQ stanza status codes:
|
||||
IQ_WAIT: 0,
|
||||
IQ_OK: 1,
|
||||
IQ_ERROR: -1,
|
||||
// IQ stanza status codes:
|
||||
IQ_WAIT: 0,
|
||||
IQ_OK: 1,
|
||||
IQ_ERROR: -1,
|
||||
|
||||
_init: function( clientName, realm, clientPassword, transport, authenticator ) {
|
||||
_init: function( clientName, realm, clientPassword, transport, authenticator ) {
|
||||
this._myName = clientName;
|
||||
this._realm = realm;
|
||||
this._fullName = clientName + "@" + realm;
|
||||
|
@ -40,6 +47,7 @@ XmppClient.prototype = {
|
|||
this._streamOpen = false;
|
||||
this._transportLayer = transport;
|
||||
this._authenticationLayer = authenticator;
|
||||
LOG("initialized auth with clientName=" + clientName + ", realm=" + realm + ", pw=" + clientPassword);
|
||||
this._authenticationLayer.initialize( clientName, realm, clientPassword );
|
||||
this._messageHandlers = [];
|
||||
this._iqResponders = [];
|
||||
|
@ -47,9 +55,9 @@ XmppClient.prototype = {
|
|||
this._pendingIqs = {};
|
||||
},
|
||||
|
||||
__parser: null,
|
||||
__parser: null,
|
||||
get _parser() {
|
||||
if (!this.__parser)
|
||||
if (!this.__parser)
|
||||
this.__parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance( Ci.nsIDOMParser );
|
||||
return this.__parser;
|
||||
},
|
||||
|
@ -61,11 +69,10 @@ XmppClient.prototype = {
|
|||
return this.__threadManager;
|
||||
},
|
||||
|
||||
|
||||
parseError: function( streamErrorNode ) {
|
||||
dump( "Uh-oh, there was an error!\n" );
|
||||
parseError: function( streamErrorNode ) {
|
||||
LOG( "Uh-oh, there was an error!" );
|
||||
var error = streamErrorNode.childNodes[0];
|
||||
dump( "Name: " + error.nodeName + " Value: " + error.nodeValue + "\n" );
|
||||
LOG( "Name: " + error.nodeName + " Value: " + error.nodeValue );
|
||||
this._error = error.nodeName;
|
||||
this.disconnect();
|
||||
/* Note there can be an optional <text>bla bla </text> node inside
|
||||
|
@ -74,39 +81,63 @@ XmppClient.prototype = {
|
|||
namespace */
|
||||
},
|
||||
|
||||
setError: function( errorText ) {
|
||||
dump( "Error: " + errorText + "\n" );
|
||||
setError: function( errorText ) {
|
||||
LOG( "Error: " + errorText );
|
||||
this._error = errorText;
|
||||
this._connectionStatus = this.FAILED;
|
||||
},
|
||||
|
||||
onIncomingData: function( messageText ) {
|
||||
onIncomingData: function( messageText ) {
|
||||
LOG("onIncomingData(): rcvd: " + messageText);
|
||||
var responseDOM = this._parser.parseFromString( messageText, "text/xml" );
|
||||
|
||||
// Handle server disconnection
|
||||
if (messageText.match("^</stream:stream>$")) {
|
||||
this._handleServerDisconnection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect parse errors, and attempt to handle them in the valid cases.
|
||||
|
||||
if (responseDOM.documentElement.nodeName == "parsererror" ) {
|
||||
/* Before giving up, remember that XMPP doesn't close the top-level
|
||||
<stream:stream> element until the communication is done; this means
|
||||
that what we get from the server is often technically only an
|
||||
xml fragment. Try manually appending the closing tag to simulate
|
||||
a complete xml document and then parsing that. */
|
||||
/*
|
||||
Before giving up, remember that XMPP doesn't close the top-level
|
||||
<stream:stream> element until the communication is done; this means
|
||||
that what we get from the server is often technically only an
|
||||
xml fragment. Try manually appending the closing tag to simulate
|
||||
a complete xml document and then parsing that. */
|
||||
|
||||
var response = messageText + this._makeClosingXml();
|
||||
responseDOM = this._parser.parseFromString( response, "text/xml" );
|
||||
}
|
||||
|
||||
if ( responseDOM.documentElement.nodeName == "parsererror" ) {
|
||||
/* If that still doesn't work, it might be that we're getting a fragment
|
||||
with multiple top-level tags, which is a no-no. Try wrapping it
|
||||
all inside one proper top-level stream element and parsing. */
|
||||
with multiple top-level tags, which is a no-no. Try wrapping it
|
||||
all inside one proper top-level stream element and parsing. */
|
||||
response = this._makeHeaderXml( this._fullName ) + messageText + this._makeClosingXml();
|
||||
responseDOM = this._parser.parseFromString( response, "text/xml" );
|
||||
}
|
||||
|
||||
if ( responseDOM.documentElement.nodeName == "parsererror" ) {
|
||||
/* Still can't parse it, give up. */
|
||||
this.setError( "Can't parse incoming XML." );
|
||||
return;
|
||||
}
|
||||
|
||||
// Message is parseable, now look for message-level errors.
|
||||
|
||||
var rootElem = responseDOM.documentElement;
|
||||
|
||||
var errors = rootElem.getElementsByTagName( "stream:error" );
|
||||
if ( errors.length > 0 ) {
|
||||
this.setError( errors[0].firstChild.nodeName );
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream is valid.
|
||||
|
||||
// Detect and handle mid-authentication steps.
|
||||
if ( this._connectionStatus == this.CALLED_SERVER ) {
|
||||
// skip TLS, go straight to SALS. (encryption should be negotiated
|
||||
// at the HTTP layer, i.e. use HTTPS)
|
||||
|
@ -114,64 +145,59 @@ XmppClient.prototype = {
|
|||
//dispatch whatever the next stage of the connection protocol is.
|
||||
response = this._authenticationLayer.generateResponse( rootElem );
|
||||
if ( response == false ) {
|
||||
this.setError( this._authenticationLayer.getError() );
|
||||
this.setError( this._authenticationLayer.getError() );
|
||||
} else if ( response == this._authenticationLayer.COMPLETION_CODE ){
|
||||
this._connectionStatus = this.CONNECTED;
|
||||
dump( "We be connected!!\n" );
|
||||
this._connectionStatus = this.CONNECTED;
|
||||
LOG( "We be connected!!" );
|
||||
} else {
|
||||
this._transportLayer.send( response );
|
||||
this._transportLayer.send( response );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect and handle regular communication.
|
||||
if ( this._connectionStatus == this.CONNECTED ) {
|
||||
/* Check incoming xml to see if it contains errors, presence info,
|
||||
or a message: */
|
||||
var errors = rootElem.getElementsByTagName( "stream:error" );
|
||||
if ( errors.length > 0 ) {
|
||||
this.setError( errors[0].firstChild.nodeName );
|
||||
return;
|
||||
}
|
||||
var presences = rootElem.getElementsByTagName( "presence" );
|
||||
if (presences.length > 0 ) {
|
||||
var from = presences[0].getAttribute( "from" );
|
||||
if ( from != undefined ) {
|
||||
dump( "I see that " + from + " is online.\n" );
|
||||
}
|
||||
var from = presences[0].getAttribute( "from" );
|
||||
if ( from != undefined ) {
|
||||
LOG( "I see that " + from + " is online." );
|
||||
}
|
||||
}
|
||||
|
||||
if ( rootElem.nodeName == "message" ) {
|
||||
this.processIncomingMessage( rootElem );
|
||||
this.processIncomingMessage( rootElem );
|
||||
} else {
|
||||
var messages = rootElem.getElementsByTagName( "message" );
|
||||
if (messages.length > 0 ) {
|
||||
for ( var message in messages ) {
|
||||
this.processIncomingMessage( messages[ message ] );
|
||||
}
|
||||
}
|
||||
var messages = rootElem.getElementsByTagName( "message" );
|
||||
if (messages.length > 0 ) {
|
||||
for ( var message in messages ) {
|
||||
this.processIncomingMessage( messages[ message ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( rootElem.nodeName == "iq" ) {
|
||||
this.processIncomingIq( rootElem );
|
||||
this.processIncomingIq( rootElem );
|
||||
} else {
|
||||
var iqs = rootElem.getElementsByTagName( "iq" );
|
||||
if ( iqs.length > 0 ) {
|
||||
for ( var iq in iqs ) {
|
||||
this.processIncomingIq( iqs[ iq ] );
|
||||
}
|
||||
}
|
||||
var iqs = rootElem.getElementsByTagName( "iq" );
|
||||
if ( iqs.length > 0 ) {
|
||||
for ( var iq in iqs ) {
|
||||
this.processIncomingIq( iqs[ iq ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
processIncomingMessage: function( messageElem ) {
|
||||
dump( "in processIncomingMessage: messageElem is a " + messageElem + "\n" );
|
||||
processIncomingMessage: function( messageElem ) {
|
||||
LOG( "in processIncomingMessage: messageElem is a " + messageElem );
|
||||
var from = messageElem.getAttribute( "from" );
|
||||
var contentElem = messageElem.firstChild;
|
||||
// Go down till we find the element with nodeType = 3 (TEXT_NODE)
|
||||
while ( contentElem.nodeType != 3 ) {
|
||||
contentElem = contentElem.firstChild;
|
||||
}
|
||||
dump( "Incoming message to you from " + from + ":\n" );
|
||||
dump( contentElem.nodeValue );
|
||||
LOG( "Incoming message to you from " + from + ":" + contentElem.nodeValue );
|
||||
for ( var x in this._messageHandlers ) {
|
||||
// TODO do messages have standard place for metadata?
|
||||
// will want to have handlers that trigger only on certain metadata.
|
||||
|
@ -179,7 +205,7 @@ XmppClient.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
processIncomingIq: function( iqElem ) {
|
||||
processIncomingIq: function( iqElem ) {
|
||||
/* This processes both kinds of incoming IQ stanzas --
|
||||
ones that are new (initated by another jabber client) and those that
|
||||
are responses to ones we sent out previously. We can tell the
|
||||
|
@ -202,9 +228,9 @@ XmppClient.prototype = {
|
|||
break;
|
||||
case "set":
|
||||
/* Someone is telling us to set the value of a variable.
|
||||
Delegate this to the registered iqResponder; we can reply
|
||||
either with an empty iq type="result" stanza, or else an
|
||||
iq type="error" stanza */
|
||||
Delegate this to the registered iqResponder; we can reply
|
||||
either with an empty iq type="result" stanza, or else an
|
||||
iq type="error" stanza */
|
||||
var variable = iqElem.firstChild.firstChild.getAttribute( "var" );
|
||||
var newValue = iqElem.firstChild.firstChildgetAttribute( "value" );
|
||||
// TODO what happens when there's more than one reigistered
|
||||
|
@ -216,61 +242,61 @@ XmppClient.prototype = {
|
|||
break;
|
||||
case "result":
|
||||
/* If all is right with the universe, then the id of this iq stanza
|
||||
corresponds to a set or get stanza that we sent out, so it should
|
||||
be in our pending dictionary.
|
||||
corresponds to a set or get stanza that we sent out, so it should
|
||||
be in our pending dictionary.
|
||||
*/
|
||||
if ( this._pendingIqs[ id ] == undefined ) {
|
||||
this.setError( "Unexpected IQ reply id" + id );
|
||||
return;
|
||||
this.setError( "Unexpected IQ reply id" + id );
|
||||
return;
|
||||
}
|
||||
/* The result stanza may have a query with a value in it, in
|
||||
which case this is the value of the variable we requested.
|
||||
If there's no value, it was probably a set query, and should
|
||||
just be considred a success. */
|
||||
which case this is the value of the variable we requested.
|
||||
If there's no value, it was probably a set query, and should
|
||||
just be considred a success. */
|
||||
var newValue = iqElem.firstChild.firstChild.getAttribute( "value" );
|
||||
if ( newValue != undefined ) {
|
||||
this._pendingIqs[ id ].value = newValue;
|
||||
this._pendingIqs[ id ].value = newValue;
|
||||
} else {
|
||||
this._pendingIqs[ id ].value = true;
|
||||
this._pendingIqs[ id ].value = true;
|
||||
}
|
||||
this._pendingIqs[ id ].status = this.IQ_OK;
|
||||
break;
|
||||
case "error":
|
||||
/* Dig down through the element tree till we find the one with
|
||||
the error text... */
|
||||
the error text... */
|
||||
var elems = iqElem.getElementsByTagName( "error" );
|
||||
var errorNode = elems[0].firstChild;
|
||||
if ( errorNode.nodeValue != null ) {
|
||||
this.setError( errorNode.nodeValue );
|
||||
this.setError( errorNode.nodeValue );
|
||||
} else {
|
||||
this.setError( errorNode.nodeName );
|
||||
this.setError( errorNode.nodeName );
|
||||
}
|
||||
if ( this._pendingIqs[ id ] != undefined ) {
|
||||
this._pendingIqs[ id ].status = this.IQ_ERROR;
|
||||
this._pendingIqs[ id ].status = this.IQ_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
registerMessageHandler: function( handlerObject ) {
|
||||
registerMessageHandler: function( handlerObject ) {
|
||||
/* messageHandler object must have
|
||||
handle( messageText, from ) method.
|
||||
*/
|
||||
this._messageHandlers.push( handlerObject );
|
||||
},
|
||||
|
||||
registerIQResponder: function( handlerObject ) {
|
||||
registerIQResponder: function( handlerObject ) {
|
||||
/* IQResponder object must have
|
||||
.get( variable ) and
|
||||
.set( variable, newvalue ) methods. */
|
||||
this._iqResponders.push( handlerObject );
|
||||
},
|
||||
|
||||
onTransportError: function( errorText ) {
|
||||
|
||||
onTransportError: function( errorText ) {
|
||||
this.setError( errorText );
|
||||
},
|
||||
|
||||
connect: function( host ) {
|
||||
|
||||
connect: function( host ) {
|
||||
// Do the handshake to connect with the server and authenticate.
|
||||
this._transportLayer.connect();
|
||||
this._transportLayer.setCallbackObject( this );
|
||||
|
@ -281,27 +307,31 @@ XmppClient.prototype = {
|
|||
// onIncomingData.
|
||||
},
|
||||
|
||||
_makeHeaderXml: function( recipient ) {
|
||||
return "<?xml version='1.0'?><stream:stream to='" + recipient + "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
|
||||
_makeHeaderXml: function( recipient ) {
|
||||
return "<?xml version='1.0'?><stream:stream to='" +
|
||||
recipient +
|
||||
"' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
|
||||
},
|
||||
|
||||
_makeMessageXml: function( messageText, fullName, recipient ) {
|
||||
_makeMessageXml: function( messageText, fullName, recipient ) {
|
||||
/* a "message stanza". Note the message element must have the
|
||||
full namespace info or it will be rejected. */
|
||||
var msgXml = "<message xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='" + fullName + "' to='" + recipient + "' xml:lang='en'><body>" + messageText + "</body></message>";
|
||||
dump( "Message xml: \n" );
|
||||
dump( msgXml );
|
||||
var msgXml = "<message xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='" +
|
||||
fullName + "' to='" + recipient + "' xml:lang='en'><body>" +
|
||||
messageText + "</body></message>";
|
||||
LOG( "Message xml: " );
|
||||
LOG( msgXml );
|
||||
return msgXml;
|
||||
},
|
||||
|
||||
_makePresenceXml: function( fullName ) {
|
||||
_makePresenceXml: function( fullName ) {
|
||||
// a "presence stanza", sent to announce my presence to the server;
|
||||
// the server is supposed to multiplex this to anyone subscribed to
|
||||
// presence notifications.
|
||||
return "<presence from ='" + fullName + "'><show/></presence>";
|
||||
},
|
||||
|
||||
_makeIqXml: function( fullName, recipient, type, id, query ) {
|
||||
|
||||
_makeIqXml: function( fullName, recipient, type, id, query ) {
|
||||
/* an "iq (info/query) stanza". This can be used for structured data
|
||||
exchange: I send an <iq type='get' id='1'> containing a query,
|
||||
and get back an <iq type='result' id='1'> containing the answer to my
|
||||
|
@ -313,11 +343,11 @@ XmppClient.prototype = {
|
|||
return "<iq xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='" + fullName + "' to='" + recipient + "' type='" + type + "' id='" + id + "'>" + query + "</iq>";
|
||||
},
|
||||
|
||||
_makeClosingXml: function () {
|
||||
_makeClosingXml: function () {
|
||||
return "</stream:stream>";
|
||||
},
|
||||
|
||||
_generateIqId: function() {
|
||||
_generateIqId: function() {
|
||||
// Each time this is called, it returns an ID that has not
|
||||
// previously been used this session.
|
||||
var id = "client_" + this._nextIqId;
|
||||
|
@ -325,14 +355,14 @@ XmppClient.prototype = {
|
|||
return id;
|
||||
},
|
||||
|
||||
_sendIq: function( recipient, query, type ) {
|
||||
_sendIq: function( recipient, query, type ) {
|
||||
var id = this._generateIqId();
|
||||
this._pendingIqs[ id ] = { status: this.IQ_WAIT };
|
||||
this._transportLayer.send( this._makeIqXml( this._fullName,
|
||||
recipient,
|
||||
type,
|
||||
id,
|
||||
query ) );
|
||||
recipient,
|
||||
type,
|
||||
id,
|
||||
query ) );
|
||||
/* And then wait for a response with the same ID to come back...
|
||||
When we get a reply, the pendingIq dictionary entry will have
|
||||
its status set to IQ_OK or IQ_ERROR and, if it's IQ_OK and
|
||||
|
@ -350,28 +380,28 @@ XmppClient.prototype = {
|
|||
// Can't happen?
|
||||
},
|
||||
|
||||
iqGet: function( recipient, variable ) {
|
||||
iqGet: function( recipient, variable ) {
|
||||
var query = "<query><getvar var='" + variable + "'/></query>";
|
||||
return this._sendIq( recipient, query, "get" );
|
||||
},
|
||||
|
||||
iqSet: function( recipient, variable, value ) {
|
||||
|
||||
iqSet: function( recipient, variable, value ) {
|
||||
var query = "<query><setvar var='" + variable + "' value='" + value + "'/></query>";
|
||||
return this._sendIq( recipient, query, "set" );
|
||||
},
|
||||
|
||||
sendMessage: function( recipient, messageText ) {
|
||||
sendMessage: function( recipient, messageText ) {
|
||||
// OK so now I'm doing that part, but what am I supposed to do with the
|
||||
// new JID that I'm bound to??
|
||||
var body = this._makeMessageXml( messageText, this._fullName, recipient );
|
||||
this._transportLayer.send( body );
|
||||
},
|
||||
|
||||
announcePresence: function() {
|
||||
this._transportLayer.send( "<presence/>" );
|
||||
announcePresence: function() {
|
||||
this._transportLayer.send( this._makePresenceXml(this._myName) );
|
||||
},
|
||||
|
||||
subscribeForPresence: function( buddyId ) {
|
||||
subscribeForPresence: function( buddyId ) {
|
||||
// OK, there are 'subscriptions' and also 'rosters'...?
|
||||
//this._transportLayer.send( "<presence to='" + buddyId + "' type='subscribe'/>" );
|
||||
// TODO
|
||||
|
@ -379,30 +409,31 @@ XmppClient.prototype = {
|
|||
// me with type ='subscribed'.
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
disconnect: function() {
|
||||
// todo: only send closing xml if the stream has not already been
|
||||
// closed (if there was an error, the server will have closed the stream.)
|
||||
this._transportLayer.send( this._makeClosingXml() );
|
||||
this._transportLayer.disconnect();
|
||||
|
||||
this.waitForDisconnect();
|
||||
},
|
||||
|
||||
waitForConnection: function( ) {
|
||||
_handleServerDisconnection: function() {
|
||||
this._transportLayer.disconnect();
|
||||
this._connectionStatus = this.NOT_CONNECTED;
|
||||
},
|
||||
|
||||
waitForConnection: function( ) {
|
||||
var thread = this._threadManager.currentThread;
|
||||
while ( this._connectionStatus != this.CONNECTED &&
|
||||
this._connectionStatus != this.FAILED ) {
|
||||
this._connectionStatus != this.FAILED ) {
|
||||
thread.processNextEvent( true );
|
||||
}
|
||||
},
|
||||
|
||||
waitForDisconnect: function() {
|
||||
waitForDisconnect: function() {
|
||||
var thread = this._threadManager.currentThread;
|
||||
while ( this._connectionStatus == this.CONNECTED ) {
|
||||
thread.processNextEvent( true );
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// IM level protocol stuff: presence announcements, conversations, etc.
|
||||
// ftp://ftp.isi.edu/in-notes/rfc3921.txt
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ pref("extensions.weave.engine.history", true);
|
|||
pref("extensions.weave.engine.cookies", false );
|
||||
pref("extensions.weave.engine.passwords", false );
|
||||
pref("extensions.weave.engine.forms", false );
|
||||
pref("extensions.weave.engine.tabs", false);
|
||||
|
||||
pref("extensions.weave.log.appender.console", "Warn");
|
||||
pref("extensions.weave.log.appender.dump", "Error");
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
function FakeCookie( host, path, name, value,
|
||||
Components.utils.import("resource://weave/engines/cookies.js");
|
||||
|
||||
function FakeCookie( host, path, name, value,
|
||||
isSecure, isHttpOnly, isSession, expiry ) {
|
||||
this._init( host, path, name, value,
|
||||
this._init( host, path, name, value,
|
||||
isSecure, isHttpOnly, isSession, expiry );
|
||||
}
|
||||
FakeCookie.prototype = {
|
||||
_init: function( host, path, name, value,
|
||||
_init: function( host, path, name, value,
|
||||
isSecure, isHttpOnly, isSession, expiry) {
|
||||
this.host = host;
|
||||
this.path = path;
|
||||
|
@ -49,7 +51,7 @@ FakeCookieManager.prototype = {
|
|||
this._cookieList = [];
|
||||
},
|
||||
|
||||
add: function( host, path, name, value,
|
||||
add: function( host, path, name, value,
|
||||
isSecure, isHttpOnly, isSession, expiry) {
|
||||
var newCookie = new FakeCookie( host,
|
||||
path,
|
||||
|
@ -84,8 +86,6 @@ FakeCookieManager.prototype = {
|
|||
};
|
||||
|
||||
function sub_test_cookie_tracker() {
|
||||
Components.utils.import("resource://weave/trackers.js");
|
||||
|
||||
var ct = new CookieTracker();
|
||||
|
||||
// gonna have to use the real cookie manager here...
|
||||
|
@ -127,8 +127,6 @@ function run_test() {
|
|||
then call cookieStore.wrap() and make sure it returns the persistent
|
||||
one and not the non-persistent one */
|
||||
|
||||
Components.utils.import("resource://weave/stores.js");
|
||||
|
||||
// My stub object to replace the real cookieManager:
|
||||
var fakeCookieManager = new FakeCookieManager();
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
function loadInSandbox(aUri) {
|
||||
var sandbox = Components.utils.Sandbox(this);
|
||||
var request = Components.
|
||||
classes["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
||||
createInstance();
|
||||
|
||||
request.open("GET", aUri, false);
|
||||
request.send(null);
|
||||
Components.utils.evalInSandbox(request.responseText, sandbox);
|
||||
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
// The JS module we're testing, with all members exposed.
|
||||
var passwords = loadInSandbox("resource://weave/engines/passwords.js");
|
||||
|
||||
// Fake nsILoginInfo object.
|
||||
var fakeUser = {
|
||||
hostname: "www.boogle.com",
|
||||
formSubmitURL: "http://www.boogle.com/search",
|
||||
httpRealm: "",
|
||||
username: "",
|
||||
password: "",
|
||||
usernameField: "test_person",
|
||||
passwordField: "test_password"
|
||||
};
|
||||
|
||||
// Fake nsILoginManager object.
|
||||
var fakeLoginManager = {
|
||||
getAllLogins: function() { return [fakeUser]; }
|
||||
};
|
||||
|
||||
// Ensure that _hashLoginInfo() works.
|
||||
var fakeUserHash = passwords._hashLoginInfo(fakeUser);
|
||||
do_check_eq(typeof fakeUserHash, 'string');
|
||||
do_check_eq(fakeUserHash.length, 40);
|
||||
|
||||
// Ensure that PasswordSyncCore._itemExists() works.
|
||||
var psc = new passwords.PasswordSyncCore();
|
||||
psc.__loginManager = fakeLoginManager;
|
||||
do_check_false(psc._itemExists("invalid guid"));
|
||||
do_check_true(psc._itemExists(fakeUserHash));
|
||||
}
|
|
@ -13,7 +13,7 @@ function run_test() {
|
|||
do_check_true(clearTxt == "my very secret message!");
|
||||
|
||||
// The following check with wrong password must cause decryption to fail
|
||||
// beuase of used padding-schema cipher, RFC 3852 Section 6.3
|
||||
// because of used padding-schema cipher, RFC 3852 Section 6.3
|
||||
let failure = false;
|
||||
try {
|
||||
pbe.decrypt("wrongpassphrase", cipherTxt);
|
||||
|
|
|
@ -2,36 +2,57 @@ var Cu = Components.utils;
|
|||
|
||||
Cu.import( "resource://weave/xmpp/xmppClient.js" );
|
||||
|
||||
var serverUrl = "http://127.0.0.1:5280/http-poll";
|
||||
var jabberDomain = "jonathan-dicarlos-macbook-pro.local";
|
||||
function LOG(aMsg) {
|
||||
dump("TEST_XMPP: " + aMsg + "\n");
|
||||
}
|
||||
|
||||
var serverUrl = "http://localhost:5280/http-poll";
|
||||
var jabberDomain = "localhost";
|
||||
|
||||
var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer );
|
||||
var threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
|
||||
function run_test() {
|
||||
|
||||
// FIXME: this test hangs when you don't have a server, disabling for now
|
||||
return;
|
||||
|
||||
/* First, just see if we can connect: */
|
||||
var transport = new HTTPPollingTransport( serverUrl,
|
||||
false,
|
||||
4000 );
|
||||
var transport = new HTTPPollingTransport(serverUrl, false, 4000);
|
||||
var auth = new PlainAuthenticator();
|
||||
var alice = new XmppClient( "alice", jabberDomain, "iamalice",
|
||||
transport, auth );
|
||||
var alice = new XmppClient("alice", jabberDomain, "iamalice",
|
||||
transport, auth);
|
||||
|
||||
/*
|
||||
// test connection
|
||||
LOG("connecting");
|
||||
alice.connect( jabberDomain );
|
||||
alice.waitForConnection();
|
||||
do_check_neq( alice._connectionStatus, alice.FAILED );
|
||||
if ( alice._connectionStatus != alice.FAILED ) {
|
||||
alice.disconnect();
|
||||
};
|
||||
// A flaw here: once alice disconnects, she can't connect again?
|
||||
// Make an explicit test out of that.
|
||||
*/
|
||||
do_check_eq( alice._connectionStatus, alice.CONNECTED);
|
||||
LOG("connected");
|
||||
|
||||
// test disconnection
|
||||
LOG("disconnecting");
|
||||
alice.disconnect();
|
||||
do_check_eq( alice._connectionStatus, alice.NOT_CONNECTED);
|
||||
LOG("disconnected");
|
||||
|
||||
// test re-connection
|
||||
LOG("reconnecting");
|
||||
alice.connect( jabberDomain );
|
||||
alice.waitForConnection();
|
||||
LOG("reconnected");
|
||||
do_check_eq( alice._connectionStatus, alice.CONNECTED);
|
||||
alice.disconnect();
|
||||
|
||||
// test connection failure - bad domain
|
||||
alice.connect( "bad domain" );
|
||||
alice.waitForConnection();
|
||||
do_check_eq( alice._connectionStatus, alice.FAILED );
|
||||
|
||||
/*
|
||||
// re-connect and move on
|
||||
alice.connect( jabberDomain );
|
||||
alice.waitForConnection();
|
||||
do_check_eq( alice._connectionStatus, alice.CONNECTED);
|
||||
|
||||
// The talking-to-myself test:
|
||||
var testIsOver = false;
|
||||
|
@ -81,4 +102,5 @@ function run_test() {
|
|||
|
||||
alice.disconnect();
|
||||
bob.disconnect();
|
||||
*/
|
||||
};
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
function LOG(aMsg) {
|
||||
dump("TEST_XMPP_SIMPLE: " + aMsg + "\n");
|
||||
}
|
||||
|
||||
Components.utils.import( "resource://weave/xmpp/xmppClient.js" );
|
||||
|
||||
var serverUrl = "http://localhost:5280/http-poll";
|
||||
var jabberDomain = "localhost";
|
||||
|
||||
function run_test() {
|
||||
// FIXME: this test hangs when you don't have a server, disabling for now
|
||||
// return;
|
||||
|
||||
// async test
|
||||
do_test_pending();
|
||||
|
||||
var testMessage = "Hello Bob.";
|
||||
|
||||
var aliceHandler = {
|
||||
handle: function(msgText, from) {
|
||||
LOG("ALICE RCVD from " + from + ": " + msgText);
|
||||
}
|
||||
};
|
||||
var aliceClient = getClientForUser("alice", "iamalice", aliceHandler);
|
||||
|
||||
var bobHandler = {
|
||||
handle: function(msgText, from) {
|
||||
LOG("BOB RCVD from " + from + ": " + msgText);
|
||||
do_check_eq(from.split("/")[0], "alice@" + jabberDomain);
|
||||
do_check_eq(msgText, testMessage);
|
||||
LOG("messages checked out");
|
||||
|
||||
aliceClient.disconnect();
|
||||
bobClient.disconnect();
|
||||
LOG("disconnected");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
};
|
||||
var bobClient = getClientForUser("bob", "iambob", bobHandler);
|
||||
bobClient.announcePresence();
|
||||
|
||||
|
||||
// Send a message
|
||||
aliceClient.sendMessage("bob@" + jabberDomain, testMessage);
|
||||
}
|
||||
|
||||
function getClientForUser(aName, aPassword, aHandler) {
|
||||
// "false" tells the transport not to use session keys. 4000 is the number of
|
||||
// milliseconds to wait between attempts to poll the server.
|
||||
var transport = new HTTPPollingTransport(serverUrl, false, 4000);
|
||||
|
||||
var auth = new PlainAuthenticator();
|
||||
|
||||
var client = new XmppClient(aName, jabberDomain, aPassword,
|
||||
transport, auth);
|
||||
|
||||
client.registerMessageHandler(aHandler);
|
||||
|
||||
// Connect
|
||||
client.connect(jabberDomain);
|
||||
client.waitForConnection();
|
||||
|
||||
// this will block until our connection attempt has either succeeded or failed.
|
||||
// Check if connection succeeded:
|
||||
if ( client._connectionStatus == client.FAILED ) {
|
||||
do_throw("connection failed");
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
function LOG(aMsg) {
|
||||
dump("TEST_XMPP_TRANSPORT_HTTP: " + aMsg + "\n");
|
||||
}
|
||||
|
||||
Components.utils.import( "resource://weave/xmpp/xmppClient.js" );
|
||||
|
||||
var tests = [];
|
||||
|
||||
// test connection failure - no server
|
||||
tests.push(function run_test_bad_server() {
|
||||
LOG("starting test: bad server");
|
||||
|
||||
var transport = new HTTPPollingTransport("this is not a server URL", false, 4000);
|
||||
transport.connect();
|
||||
transport.setCallbackObject({
|
||||
onIncomingData: function(aData) {
|
||||
do_throw("onIncomingData was called instead of onTransportError, for a bad URL");
|
||||
},
|
||||
onTransportError: function(aErrorMessage) {
|
||||
do_check_true(/^Unable to send message to server:/.test(aErrorMessage));
|
||||
// continue test suite
|
||||
tests.shift()();
|
||||
}
|
||||
});
|
||||
transport.send();
|
||||
});
|
||||
|
||||
tests.push(function run_test_bad_url() {
|
||||
LOG("starting test: bad url");
|
||||
// test connection failure - server up, bad URL
|
||||
var serverUrl = "http://localhost:5280/http-polly-want-a-cracker";
|
||||
var transport = new HTTPPollingTransport(serverUrl, false, 4000);
|
||||
transport.connect();
|
||||
transport.setCallbackObject({
|
||||
onIncomingData: function(aData) {
|
||||
do_throw("onIncomingData was called instead of onTransportError, for a bad URL");
|
||||
},
|
||||
onTransportError: function(aErrorMessage) {
|
||||
LOG("ERROR: " + aErrorMessage);
|
||||
do_check_true(/^Provided URL is not valid./.test(aErrorMessage));
|
||||
do_test_finished();
|
||||
}
|
||||
});
|
||||
transport.send();
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
// FIXME: this test hangs when you don't have a server, disabling for now
|
||||
return;
|
||||
|
||||
// async test
|
||||
do_test_pending();
|
||||
|
||||
tests.shift()();
|
||||
}
|
Загрузка…
Ссылка в новой задаче