This commit is contained in:
Dan Mills 2008-06-05 23:11:20 +09:00
Родитель 510669d414 13f2fbd215
Коммит a2659cdec7
25 изменённых файлов: 2798 добавлений и 1967 удалений

Просмотреть файл

@ -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()();
}