From 12f3dcc427c2e1efa827020bbe3c9aa6a01a3407 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Thu, 22 May 2008 11:41:05 -0700 Subject: [PATCH 01/22] Bug 419121 - Weave chokes on microsummaries (r=thunder) --- services/sync/modules/stores.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 51feef1348c..e12ddb41ff7 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -369,6 +369,8 @@ BookmarksStore.prototype = { 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); @@ -578,6 +580,7 @@ BookmarksStore.prototype = { 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; From 826fa1d7f348219069b366a93b17ba8902e1288e Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 2 Jun 2008 15:24:52 -0700 Subject: [PATCH 02/22] minor typo fixes --- services/sync/modules/engines.js | 2 +- services/sync/modules/stores.js | 2 +- services/sync/modules/syncCores.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 776842a2879..71ca41aae9d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -273,7 +273,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"); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 17988920391..51feef1348c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -1005,7 +1005,7 @@ PasswordStore.prototype = { wrap: function PasswordStore_wrap() { /* Return contents of this store, as JSON. */ - var items = []; + var items = {}; var logins = this._loginManager.getAllLogins({}); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index a7ea0b5f2db..39e46901651 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -227,7 +227,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++) { @@ -416,12 +416,12 @@ function HistorySyncCore() { HistorySyncCore.prototype = { _logName: "HistSync", - _itemExists: function BSC__itemExists(GUID) { + _itemExists: function HSC__itemExists(GUID) { // we don't care about already-existing items; just try to re-add them return false; }, - _commandLike: function BSC_commandLike(a, b) { + _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 From fd84f76ff66a97d9f2941da452f7e563280c53a9 Mon Sep 17 00:00:00 2001 From: "jonathandicarlo@jonathan-dicarlos-macbook-pro.local" Date: Mon, 2 Jun 2008 20:13:46 -0700 Subject: [PATCH 03/22] Bookmark share now leaves an annotation ('weave/share/sahred_outgoing' = true or false) on a bookmark folder to note whether it's being shared or not; when a folder is being shared, the menu item in the folder submenu changes to 'Stop sharing this folder'. --- services/sync/locales/en-US/sync.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index dc3e50e8989..90dab966f84 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -4,4 +4,5 @@ status.idle = Idle status.active = Working... status.offline = Offline status.error = Error -shareBookmark.menuItem = Share This Folder... \ No newline at end of file +shareBookmark.menuItem = Share This Folder... +unShareBookmark.menuItem = Stop Sharing This Folder \ No newline at end of file From 7a7a041ace0612cb2484fc1e5874a8082cb10c90 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 11:11:44 -0700 Subject: [PATCH 04/22] Moved all code related to the syncing of cookies--e.g. CookieStore, CookieTracker, CookieEngine, CookieSyncCore--into their own file at modules/engines/cookies.js. I'll be doing the same to the other engines shortly. This helps with code organization--all the logic for dealing with a particular data type is now in one place--and should also make it easier to write unit/regression tests. --- services/sync/modules/engines.js | 33 +- services/sync/modules/engines/cookies.js | 330 ++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 187 +--------- services/sync/modules/syncCores.js | 72 +--- services/sync/modules/trackers.js | 53 +-- services/sync/tests/unit/test_cookie_store.js | 14 +- 7 files changed, 349 insertions(+), 341 deletions(-) create mode 100644 services/sync/modules/engines/cookies.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 71ca41aae9d..c402bfa0fd1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', + 'BookmarksEngine', 'HistoryEngine', 'PasswordEngine', 'FormEngine']; const Cc = Components.classes; @@ -906,37 +906,6 @@ HistoryEngine.prototype = { }; 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); } diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js new file mode 100644 index 00000000000..a2ea2329434 --- /dev/null +++ b/services/sync/modules/engines/cookies.js @@ -0,0 +1,330 @@ +const EXPORTED_SYMBOLS = ['CookieEngine', 'CookieTracker', 'CookieStore']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +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(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb9868907d9..e2c68d969df 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -51,6 +51,7 @@ 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"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index e12ddb41ff7..b698477a560 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore']; + 'HistoryStore', 'PasswordStore', 'FormStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -764,191 +764,6 @@ HistoryStore.prototype = { }; 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(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 39e46901651..d4f2f863610 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', - 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore']; + 'PasswordSyncCore', 'FormSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -431,76 +431,6 @@ HistorySyncCore.prototype = { }; 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(); } diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 7ba1a09060d..1b1f2059947 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', - 'FormsTracker', 'CookieTracker']; + 'FormsTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -55,7 +55,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,7 +93,7 @@ Tracker.prototype = { this._score = 0; } }; - + /* * Tracker objects for each engine may need to subclass the * getScore routine, which returns the current 'score' for that @@ -121,7 +121,7 @@ BookmarksTracker.prototype = { 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? @@ -140,14 +140,14 @@ BookmarksTracker.prototype = { _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(); } @@ -167,7 +167,7 @@ HistoryTracker.prototype = { 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 @@ -189,7 +189,7 @@ HistoryTracker.prototype = { _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); @@ -197,41 +197,6 @@ HistoryTracker.prototype = { } 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(); } @@ -279,7 +244,7 @@ FormsTracker.prototype = { return 100; else return this._score; - }, + }, resetScore: function FormsTracker_resetScore() { var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/test_cookie_store.js index ae0dbbb4380..4131ac29a4f 100644 --- a/services/sync/tests/unit/test_cookie_store.js +++ b/services/sync/tests/unit/test_cookie_store.js @@ -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(); From 92b3748833d53ba1f550983d5b9982f7c26ce0d2 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 3 Jun 2008 11:32:59 -0700 Subject: [PATCH 05/22] bug 434817: sync tabs --- services/sync/locales/en-US/preferences.dtd | 1 + services/sync/locales/en-US/sync.dtd | 7 + services/sync/modules/engines.js | 34 ++- services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 294 +++++++++++++++++++- services/sync/modules/syncCores.js | 55 +++- services/sync/modules/trackers.js | 95 ++++++- services/sync/services-sync.js | 1 + 8 files changed, 484 insertions(+), 4 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 5196cdff55a..40f1e185dfc 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -24,6 +24,7 @@ + diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 3c3f89839ab..8e4e52ee10a 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -7,3 +7,10 @@ + + + + + + + diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 71ca41aae9d..e3cbd368a47 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -19,6 +19,7 @@ * * Contributor(s): * Dan Mills + * Myk Melez * * 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 @@ -36,7 +37,7 @@ const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', - 'PasswordEngine', 'FormEngine']; + 'PasswordEngine', 'FormEngine', 'TabEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -1016,3 +1017,34 @@ FormEngine.prototype = { } }; FormEngine.prototype.__proto__ = new Engine(); + +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; + } + +}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb9868907d9..2382a0a9463 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -93,6 +93,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); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 51feef1348c..10e6063859a 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,8 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore']; + 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore', + 'TabStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -1103,3 +1104,294 @@ FormStore.prototype = { } }; FormStore.prototype.__proto__ = new Store(); + +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. + } + +}; diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 39e46901651..356e484c68a 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,8 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', - 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore']; + 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore', + 'TabSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -582,3 +583,55 @@ FormSyncCore.prototype = { } }; FormSyncCore.prototype.__proto__ = new SyncCore(); + +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; + }, + + // XXX Should we put this into SyncCore so it's available to all subclasses? + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return this._json; + }, + + _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; + } +}; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 7ba1a09060d..e91e90643c5 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', - 'FormsTracker', 'CookieTracker']; + 'FormsTracker', 'CookieTracker', 'TabTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -300,3 +300,96 @@ FormsTracker.prototype = { } } FormsTracker.prototype.__proto__ = new Tracker(); + +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. + } +} diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9890ce14e46..561ee2937af 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -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"); From 6b9c06e8ed38bfa7d90ef88132d4111e600568cf Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 3 Jun 2008 11:50:08 -0700 Subject: [PATCH 06/22] remove unused _json property from TabSyncCore --- services/sync/modules/syncCores.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 356e484c68a..9fcbe264bdd 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -602,13 +602,6 @@ TabSyncCore.prototype = { return this._sessionStore; }, - // XXX Should we put this into SyncCore so it's available to all subclasses? - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return this._json; - }, - _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 From 0e8b9eb9af7f65f19d371ca258beaf1086aadf11 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 12:38:48 -0700 Subject: [PATCH 07/22] Re-removed cookie-related changes that were accidentally re-added by c1a58b24679c and/or 5a49daf87c94. Also moved all bookmark syncing logic into modules/engines/bookmarks.js. --- services/sync/modules/engines.js | 151 +---- services/sync/modules/engines/bookmarks.js | 700 +++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 602 +----------------- services/sync/modules/syncCores.js | 172 +---- services/sync/modules/trackers.js | 93 +-- 6 files changed, 708 insertions(+), 1011 deletions(-) create mode 100644 services/sync/modules/engines/bookmarks.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e3cbd368a47..aaa45771c77 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', + 'HistoryEngine', 'PasswordEngine', 'FormEngine', 'TabEngine']; const Cc = Components.classes; @@ -758,124 +758,6 @@ Engine.prototype = { } }; -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); } @@ -907,37 +789,6 @@ HistoryEngine.prototype = { }; 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); } diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js new file mode 100644 index 00000000000..f3d1ca72b23 --- /dev/null +++ b/services/sync/modules/engines/bookmarks.js @@ -0,0 +1,700 @@ +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; + 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(); + +/* + * 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(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 24d336d120f..0a29f18f819 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -52,6 +52,7 @@ 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"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index f98da788676..f8c9083988a 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore', +const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', + 'HistoryStore', 'PasswordStore', 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,419 +272,6 @@ 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"); - 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; - 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(); } @@ -765,191 +352,6 @@ HistoryStore.prototype = { }; 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(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 9fcbe264bdd..229720898ac 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', - 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore', +const EXPORTED_SYMBOLS = ['SyncCore', 'HistorySyncCore', + 'PasswordSyncCore', 'FormSyncCore', 'TabSyncCore']; const Cc = Components.classes; @@ -313,104 +313,6 @@ SyncCore.prototype = { } }; -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(); } @@ -432,76 +334,6 @@ HistorySyncCore.prototype = { }; 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(); } diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index abb5b9891db..108a194a530 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', - 'FormsTracker', 'CookieTracker', 'TabTracker']; +const EXPORTED_SYMBOLS = ['Tracker', 'HistoryTracker', + 'FormsTracker', 'TabTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -94,60 +94,6 @@ Tracker.prototype = { } }; -/* - * 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(); } @@ -197,41 +143,6 @@ HistoryTracker.prototype = { } 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(); } From b20c630abe86446b7f9e897fce4672a59f30b45a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 13:56:16 -0700 Subject: [PATCH 08/22] Moved all history-related functionality into modules/engines/history.js. --- services/sync/modules/engines.js | 32 ---- services/sync/modules/engines/history.js | 193 +++++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 82 +--------- services/sync/modules/syncCores.js | 23 +-- services/sync/modules/trackers.js | 51 +----- 6 files changed, 197 insertions(+), 185 deletions(-) create mode 100644 services/sync/modules/engines/history.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index aaa45771c77..cb19d93e799 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,6 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'HistoryEngine', 'PasswordEngine', 'FormEngine', 'TabEngine']; const Cc = Components.classes; @@ -758,37 +757,6 @@ Engine.prototype = { } }; -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 PasswordEngine(pbeId) { this._init(pbeId); } diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js new file mode 100644 index 00000000000..ec742427ee5 --- /dev/null +++ b/services/sync/modules/engines/history.js @@ -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(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0a29f18f819..e277ec9be73 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -53,6 +53,7 @@ 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"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index f8c9083988a..7a74cb91840 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'HistoryStore', 'PasswordStore', 'FormStore', + 'PasswordStore', 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,86 +272,6 @@ SnapshotStore.prototype = { }; SnapshotStore.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 PasswordStore() { this._init(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 229720898ac..017cfb1d321 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'HistorySyncCore', +const EXPORTED_SYMBOLS = ['SyncCore', 'PasswordSyncCore', 'FormSyncCore', 'TabSyncCore']; @@ -313,27 +313,6 @@ SyncCore.prototype = { } }; -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 PasswordSyncCore() { this._init(); } diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 108a194a530..6a78a6fa93b 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', 'HistoryTracker', +const EXPORTED_SYMBOLS = ['Tracker', 'FormsTracker', 'TabTracker']; const Cc = Components.classes; @@ -94,55 +94,6 @@ Tracker.prototype = { } }; -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 FormsTracker() { this._init(); } From 8ea237d26228168beab0eb8d7bcdec11de20e896 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:08:53 -0700 Subject: [PATCH 09/22] Moved all password-syncing code into modules/engines/passwords.js. --- services/sync/modules/engines.js | 51 +----- services/sync/modules/engines/passwords.js | 187 +++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 92 +--------- services/sync/modules/syncCores.js | 39 +---- 5 files changed, 191 insertions(+), 179 deletions(-) create mode 100644 services/sync/modules/engines/passwords.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cb19d93e799..9eb059758ad 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'PasswordEngine', 'FormEngine', 'TabEngine']; + 'FormEngine', 'TabEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -757,55 +757,6 @@ Engine.prototype = { } }; -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); } diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js new file mode 100644 index 00000000000..572ddfaad6e --- /dev/null +++ b/services/sync/modules/engines/passwords.js @@ -0,0 +1,187 @@ +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"); + +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 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 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(); + diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e277ec9be73..a26a0600e58 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -54,6 +54,7 @@ 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"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 7a74cb91840..66638bd2c10 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'PasswordStore', 'FormStore', + 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,96 +272,6 @@ SnapshotStore.prototype = { }; SnapshotStore.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(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 017cfb1d321..c1825ec5451 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', - 'PasswordSyncCore', 'FormSyncCore', + 'FormSyncCore', 'TabSyncCore']; const Cc = Components.classes; @@ -313,43 +313,6 @@ SyncCore.prototype = { } }; -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(); } From 56082383929a993d97a226c026c4c2d6ea53c393 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:20:51 -0700 Subject: [PATCH 10/22] Moved all form-syncing code into modules/engines/forms.js. --- services/sync/modules/engines.js | 33 +--- services/sync/modules/engines/forms.js | 225 +++++++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 69 -------- services/sync/modules/syncCores.js | 46 ----- services/sync/modules/trackers.js | 71 +------- 6 files changed, 228 insertions(+), 217 deletions(-) create mode 100644 services/sync/modules/engines/forms.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9eb059758ad..663aa36c0ee 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'FormEngine', 'TabEngine']; + 'TabEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -757,37 +757,6 @@ Engine.prototype = { } }; -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 TabEngine(pbeId) { this._init(pbeId); } diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js new file mode 100644 index 00000000000..24411de1ba8 --- /dev/null +++ b/services/sync/modules/engines/forms.js @@ -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(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a26a0600e58..e80ed2eb06a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -55,6 +55,7 @@ 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"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 66638bd2c10..290eb3bfa17 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,6 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,74 +271,6 @@ SnapshotStore.prototype = { }; SnapshotStore.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(); - function TabStore() { this._virtualTabs = {}; this._init(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index c1825ec5451..49156d28dc3 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,6 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', - 'FormSyncCore', 'TabSyncCore']; const Cc = Components.classes; @@ -313,51 +312,6 @@ SyncCore.prototype = { } }; -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 TabSyncCore(engine) { this._engine = engine; this._init(); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 6a78a6fa93b..c55f9eb2f6a 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Tracker', - 'FormsTracker', 'TabTracker']; + 'TabTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -94,75 +94,6 @@ Tracker.prototype = { } }; -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(); - function TabTracker(engine) { this._engine = engine; this._init(); From 26b0341c5c6235210660288a5126b57b229dbfa9 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:45:53 -0700 Subject: [PATCH 11/22] Moved all tab-syncing code to modules/engines/tabsjs. --- services/sync/modules/engines.js | 35 +- services/sync/modules/engines/tabs.js | 475 ++++++++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 295 +--------------- services/sync/modules/syncCores.js | 48 +-- services/sync/modules/trackers.js | 96 +----- 6 files changed, 482 insertions(+), 468 deletions(-) create mode 100644 services/sync/modules/engines/tabs.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 663aa36c0ee..05639e36032 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,8 +35,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'TabEngine']; +const EXPORTED_SYMBOLS = ['Engines', + 'Engine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -756,34 +756,3 @@ Engine.prototype = { this._notify("reset-client", this._resetClient).async(this, onComplete); } }; - -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; - } - -}; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js new file mode 100644 index 00000000000..bb53a191793 --- /dev/null +++ b/services/sync/modules/engines/tabs.js @@ -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. + } +} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e80ed2eb06a..6820c64b3a3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -56,6 +56,7 @@ 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; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 290eb3bfa17..d214e09201c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'TabStore']; +const EXPORTED_SYMBOLS = ['Store', + 'SnapshotStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -270,294 +270,3 @@ SnapshotStore.prototype = { } }; SnapshotStore.prototype.__proto__ = new Store(); - -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. - } - -}; diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 49156d28dc3..461e5d04f2b 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,8 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', - 'TabSyncCore']; +const EXPORTED_SYMBOLS = ['SyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -311,48 +310,3 @@ SyncCore.prototype = { return this._reconcile.async(this, onComplete, listA, listB); } }; - -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; - } -}; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index c55f9eb2f6a..cfb70d5eb59 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,8 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', - 'TabTracker']; +const EXPORTED_SYMBOLS = ['Tracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -93,96 +92,3 @@ Tracker.prototype = { this._score = 0; } }; - -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. - } -} From e9e278d370a5629d22646ef06cbd0f53d9006af9 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:49:22 -0700 Subject: [PATCH 12/22] Removed unused code from cookies.js, fixed a few js2-mode warnings. --- services/sync/modules/engines/cookies.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index a2ea2329434..9098755697b 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -2,7 +2,6 @@ const EXPORTED_SYMBOLS = ['CookieEngine', 'CookieTracker', 'CookieStore']; const Cc = Components.classes; const Ci = Components.interfaces; -const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); @@ -149,7 +148,7 @@ CookieStore.prototype = { getService(Ci.nsICookieManager2); // need the 2nd revision of the ICookieManager interface // because it supports add() and the 1st one doesn't. - return this.__cookieManager + return this.__cookieManager; }, _createCommand: function CookieStore__createCommand(command) { @@ -211,7 +210,7 @@ CookieStore.prototype = { // Update values in the cookie: for (var key in command.data) { // Whatever values command.data has, use them - matchingCookie[ key ] = command.data[ key ] + matchingCookie[ key ] = command.data[ key ]; } // Remove the old incorrect cookie from the manager: this._cookieManager.remove( matchingCookie.host, @@ -265,7 +264,7 @@ CookieStore.prototype = { rawHost: cookie.rawHost, isSession: cookie.isSession, expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly } + isHttpOnly: cookie.isHttpOnly }; /* See http://developer.mozilla.org/en/docs/nsICookie Note: not syncing "expires", "status", or "policy" @@ -281,7 +280,7 @@ CookieStore.prototype = { 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() + this._cookieManager.removeAll(); }, resetGUIDs: function CookieStore_resetGUIDs() { @@ -307,7 +306,7 @@ CookieTracker.prototype = { 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); + getService(Ci.nsIObserverService); observerService.addObserver( this, 'cookie-changed', false ); }, From 53a37dc1fd231163eeceee3f9d3695f7e35e9fb3 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 15:14:27 -0700 Subject: [PATCH 13/22] Minor js2-mode warning fixes. --- services/sync/modules/engines.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 05639e36032..882bd809842 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -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; @@ -603,7 +603,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() { From f4000cc8bca975f9593bde30b119faf825c6b6ef Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 16:56:58 -0700 Subject: [PATCH 14/22] In passwords.js, turned _hashLoginInfo() into a module-level function. --- services/sync/modules/engines/passwords.js | 54 ++++++++++------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 572ddfaad6e..beef082a5c8 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -9,6 +9,27 @@ 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); } @@ -19,41 +40,16 @@ PasswordEngine.prototype = { __core: null, get _core() { - if (!this.__core) { + if (!this.__core) this.__core = new PasswordSyncCore(); - this.__core._hashLoginInfo = this._hashLoginInfo; - } return this.__core; }, __store: null, get _store() { - if (!this.__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(); @@ -80,7 +76,7 @@ PasswordSyncCore.prototype = { // 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]); + var hash = _hashLoginInfo(logins[i]); if (hash == GUID) found = true;; } @@ -161,7 +157,7 @@ PasswordStore.prototype = { for (var i = 0; i < logins.length; i++) { var login = logins[i]; - var key = this._hashLoginInfo(login); + var key = _hashLoginInfo(login); items[key] = { hostname : login.hostname, formSubmitURL : login.formSubmitURL, From fabc5bcb1773f9ccfad4f74cb4c7bebf99030ad8 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 18:37:36 -0700 Subject: [PATCH 15/22] Added a basic testing suite for engines/passwords.js. It currently only tests _hashLoginInfo() and PasswordSyncCore._itemExists(). --- services/sync/tests/unit/test_passwords.js | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 services/sync/tests/unit/test_passwords.js diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js new file mode 100644 index 00000000000..0f8726a27c8 --- /dev/null +++ b/services/sync/tests/unit/test_passwords.js @@ -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)); +} From 7b5ca70bb56a510612fb0a369bc6905e7939c238 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 12:14:28 -0700 Subject: [PATCH 16/22] [mq]: xmpp-cleanup --- .../sync/modules/xmpp/authenticationLayer.js | 254 +++++++++--------- services/sync/modules/xmpp/transportLayer.js | 226 ++++++++-------- services/sync/modules/xmpp/xmppClient.js | 211 ++++++++------- services/sync/tests/unit/test_pbe.js | 2 +- services/sync/tests/unit/test_xmpp.js | 45 +++- services/sync/tests/unit/test_xmpp_simple.js | 72 +++++ 6 files changed, 467 insertions(+), 343 deletions(-) create mode 100644 services/sync/tests/unit/test_xmpp_simple.js diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index 83832135282..99059246327 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -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 // 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 "" + authString + ""; + } 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 ""; + // 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 ""; } } 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 ""; + // Do resource binding: + // Tell the server to generate the resource ID for us. + this._stepNumber = 3; + return ""; } 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 ""; + this._stepNumber = 4; + return ""; } else { - return this.COMPLETION_CODE; + return this.COMPLETION_CODE; } } else if ( this._stepNumber == 4 ) { // OK, now we're done. diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index a4c03537ecc..de3d63f91f4 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -3,26 +3,32 @@ const EXPORTED_SYMBOLS = ['HTTPPollingTransport']; var Cc = Components.classes; var Ci = Components.interfaces; +function LOG(aMsg) { + dump("Weave::Transport-HTTP-Poll: " + aMsg + "\n"); +} + 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 +38,7 @@ SocketClient.prototype = { this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); return this.__threadManager; }, + __transport: null, get _transport() { if (!this.__transport) { @@ -45,7 +52,7 @@ SocketClient.prototype = { return this.__transport; }, - _init: function( host, port ) { + _init: function( host, port ) { this._host = host; this._port = port; this._contentRead = ""; @@ -53,7 +60,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 +82,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 +115,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 +137,33 @@ 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(); +/* + 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(); @@ -167,41 +175,43 @@ HTTPPollingTransport.prototype = { this._outgoingRetryBuffer = ""; }, - __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 +219,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 +245,7 @@ HTTPPollingTransport.prototype = { } }, - _onError: function( errorText ) { + _onError: function( errorText ) { dump( "Transport error: " + errorText + "\n" ); if ( this._callbackObject != null ) { this._callbackObject.onTransportError( errorText ); @@ -243,72 +253,71 @@ 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: + */ + self._outgoingRetryBuffer = requestXml; + } + } } }; request.open( "POST", this._serverUrl, true ); //async = true - request.setRequestHeader( "Content-type", - "application/x-www-form-urlencoded;charset=UTF-8" ); + 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" ); + LOG("Sending: " + contents); request.send( contents ); }, - send: function( messageXml ) { + send: function( messageXml ) { this._doPost( messageXml ); }, - setCallbackObject: function( 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 +331,28 @@ HTTPPollingTransport.prototype = { this._doPost( outgoingMsg ); }, - connect: function() { - /* Set up a timer to poll the server periodically. */ + connect: function() { + /* Set up a timer to poll the server periodically. */ - // TODO doPost isn't reentrant; don't try to doPost if there's - //already a post in progress... or can that never happen? + // 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 ); + 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); } - }, - + } }; diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 34c1f302a27..1d53f94d3a9 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -11,6 +11,10 @@ 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 +22,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 +44,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 +52,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 +66,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 bla bla node inside @@ -74,13 +78,13 @@ 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 ) { var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); if (responseDOM.documentElement.nodeName == "parsererror" ) { @@ -92,13 +96,15 @@ XmppClient.prototype = { 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." ); @@ -114,64 +120,63 @@ 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; } if ( this._connectionStatus == this.CONNECTED ) { /* Check incoming xml to see if it contains errors, presence info, - or a message: */ + or a message: */ var errors = rootElem.getElementsByTagName( "stream:error" ); if ( errors.length > 0 ) { - this.setError( errors[0].firstChild.nodeName ); - return; + 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 +184,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 +207,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 +221,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 +286,31 @@ XmppClient.prototype = { // onIncomingData. }, - _makeHeaderXml: function( recipient ) { - return ""; + _makeHeaderXml: function( recipient ) { + return ""; }, - _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 = "" + messageText + ""; - dump( "Message xml: \n" ); - dump( msgXml ); + var msgXml = "" + + messageText + ""; + 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 ""; }, - - _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 containing a query, and get back an containing the answer to my @@ -313,11 +322,11 @@ XmppClient.prototype = { return "" + query + ""; }, - _makeClosingXml: function () { + _makeClosingXml: function () { return ""; }, - _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 +334,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 +359,28 @@ XmppClient.prototype = { // Can't happen? }, - iqGet: function( recipient, variable ) { + iqGet: function( recipient, variable ) { var query = ""; return this._sendIq( recipient, query, "get" ); }, - - iqSet: function( recipient, variable, value ) { + + iqSet: function( recipient, variable, value ) { var 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( "" ); + 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( "" ); // TODO @@ -379,22 +388,22 @@ 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(); }, - waitForConnection: function( ) { + 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 ); diff --git a/services/sync/tests/unit/test_pbe.js b/services/sync/tests/unit/test_pbe.js index 364ca0e2608..f00ffb6cc40 100644 --- a/services/sync/tests/unit/test_pbe.js +++ b/services/sync/tests/unit/test_pbe.js @@ -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); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index dcc84c5abad..a4d8d1f1a2d 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -2,14 +2,20 @@ var Cu = Components.utils; Cu.import( "resource://weave/xmpp/xmppClient.js" ); +function LOG(aMsg) { + dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); +} + var serverUrl = "http://127.0.0.1:5280/http-poll"; -var jabberDomain = "jonathan-dicarlos-macbook-pro.local"; +var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. + getService(Ci.nsIDNSService).myHostName; var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); -function run_test() { +var alice; +function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now return; @@ -18,20 +24,34 @@ function run_test() { false, 4000 ); var auth = new PlainAuthenticator(); - var alice = new XmppClient( "alice", jabberDomain, "iamalice", + alice = new XmppClient( "alice", jabberDomain, "iamalice", transport, auth ); - /* + // test connection 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); + // test re-connection + alice.disconnect(); + LOG("disconnected"); + alice.connect( jabberDomain ); + LOG("wait"); + alice.waitForConnection(); + LOG("waited"); + do_check_eq( alice._connectionStatus, alice.CONNECTED); + + /* + // test connection failure + alice.disconnect(); + 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; @@ -78,7 +98,8 @@ function run_test() { while( !testIsOver ) { currentThread.processNextEvent( true ); } + */ alice.disconnect(); - bob.disconnect(); + //bob.disconnect(); }; diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/test_xmpp_simple.js new file mode 100644 index 00000000000..50b01da295f --- /dev/null +++ b/services/sync/tests/unit/test_xmpp_simple.js @@ -0,0 +1,72 @@ +function LOG(aMsg) { + dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); +} + +Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); + +var serverUrl = "http://127.0.0.1:5280/http-poll"; +var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. + getService(Ci.nsIDNSService).myHostName; + +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; +} From 4041662e5ab73d371a48807fa728f3a0b19b84f2 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 4 Jun 2008 13:40:53 -0700 Subject: [PATCH 17/22] bug 436696: make sure we pass a valid URI to nsITaggingService::getTagsForURI when the bookmark record doesn't include a URI so the method doesn't throw and hork bookmarks sync --- services/sync/modules/engines/bookmarks.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f3d1ca72b23..5f3226f8052 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -558,7 +558,24 @@ BookmarksStore.prototype = { } item.URI = node.uri; - item.tags = this._ts.getTagsForURI(Utils.makeURI(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) { From 49101addf2bc9d31d45b98fd1bab8a282f696455 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 14:02:47 -0700 Subject: [PATCH 18/22] [mq]: xmpp-disconnect --- services/sync/modules/xmpp/xmppClient.js | 26 +++++++++++++++---- services/sync/tests/unit/test_xmpp.js | 33 ++++++++++++++---------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 1d53f94d3a9..52e326ffe96 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -85,14 +85,23 @@ XmppClient.prototype = { }, onIncomingData: function( messageText ) { + LOG("onIncomingData(): rcvd: " + messageText); var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); if (responseDOM.documentElement.nodeName == "parsererror" ) { - /* Before giving up, remember that XMPP doesn't close the top-level - 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. */ + // handle server disconnection + if (messageText.match("^$")) { + this._handleServerDisconnection(); + return; + } + + /* + Before giving up, remember that XMPP doesn't close the top-level + 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" ); } @@ -392,7 +401,13 @@ XmppClient.prototype = { // 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.waitForDisconnect(); + }, + + _handleServerDisconnection: function() { this._transportLayer.disconnect(); + this._connectionStatus = this.NOT_CONNECTED; }, waitForConnection: function( ) { @@ -404,6 +419,7 @@ XmppClient.prototype = { }, waitForDisconnect: function() { + LOG("waitForDisconnect(): starting"); var thread = this._threadManager.currentThread; while ( this._connectionStatus == this.CONNECTED ) { thread.processNextEvent( true ); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index a4d8d1f1a2d..04958495bfd 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -3,18 +3,17 @@ var Cu = Components.utils; Cu.import( "resource://weave/xmpp/xmppClient.js" ); function LOG(aMsg) { - dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); + dump("TEST_XMPP: " + aMsg + "\n"); } var serverUrl = "http://127.0.0.1:5280/http-poll"; var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. getService(Ci.nsIDNSService).myHostName; +LOG("DOMAIN: " + jabberDomain); var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); -var alice; - function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now return; @@ -24,26 +23,32 @@ function run_test() { false, 4000 ); var auth = new PlainAuthenticator(); - 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_eq( alice._connectionStatus, alice.CONNECTED); + LOG("connected"); - // test re-connection + // test disconnection + LOG("disconnecting"); alice.disconnect(); + do_check_eq( alice._connectionStatus, alice.NOT_CONNECTED); LOG("disconnected"); - alice.connect( jabberDomain ); - LOG("wait"); - alice.waitForConnection(); - LOG("waited"); - do_check_eq( alice._connectionStatus, alice.CONNECTED); /* - // test connection failure + // test re-connection + LOG("reconnecting"); + alice.connect( jabberDomain ); + alice.waitForConnection(); + LOG("reconnected"); + do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); + + // test connection failure alice.connect( "bad domain" ); alice.waitForConnection(); do_check_eq( alice._connectionStatus, alice.FAILED ); @@ -98,8 +103,8 @@ function run_test() { while( !testIsOver ) { currentThread.processNextEvent( true ); } - */ alice.disconnect(); - //bob.disconnect(); + bob.disconnect(); + */ }; From 8ec3277ff20629588e7166ccdde9e75c7f0a2792 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 17:00:02 -0700 Subject: [PATCH 19/22] [mq]: xmpp-reconnect --- services/sync/modules/xmpp/readme.txt | 6 +++--- services/sync/modules/xmpp/transportLayer.js | 5 ++++- services/sync/modules/xmpp/xmppClient.js | 9 +++------ services/sync/tests/unit/test_xmpp.js | 14 +++++--------- services/sync/tests/unit/test_xmpp_simple.js | 5 ++--- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/xmpp/readme.txt b/services/sync/modules/xmpp/readme.txt index 071e46a33ef..29397a9dd13 100644 --- a/services/sync/modules/xmpp/readme.txt +++ b/services/sync/modules/xmpp/readme.txt @@ -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.) - diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index de3d63f91f4..0b08f9d8630 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -314,7 +314,7 @@ HTTPPollingTransport.prototype = { }, setCallbackObject: function( callbackObject ) { - this._callbackObject = callbackObject; + this._callbackObject = callbackObject; }, notify: function( timer ) { @@ -332,6 +332,9 @@ HTTPPollingTransport.prototype = { }, connect: function() { + // In case this is a reconnect, make sure to re-initialize. + this._init(this._serverUrl, this._useKeys, this._interval); + /* Set up a timer to poll the server periodically. */ // TODO doPost isn't reentrant; don't try to doPost if there's diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 52e326ffe96..77d9ec5ce13 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -7,6 +7,9 @@ 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; @@ -419,15 +422,9 @@ XmppClient.prototype = { }, waitForDisconnect: function() { - LOG("waitForDisconnect(): starting"); 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 - diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 04958495bfd..09f718d8f0d 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -6,10 +6,8 @@ function LOG(aMsg) { dump("TEST_XMPP: " + aMsg + "\n"); } -var serverUrl = "http://127.0.0.1:5280/http-poll"; -var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. - getService(Ci.nsIDNSService).myHostName; -LOG("DOMAIN: " + jabberDomain); +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(); @@ -19,12 +17,10 @@ function run_test() { 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); + transport, auth); // test connection LOG("connecting"); @@ -39,7 +35,6 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.NOT_CONNECTED); LOG("disconnected"); - /* // test re-connection LOG("reconnecting"); alice.connect( jabberDomain ); @@ -48,6 +43,7 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); + /* // test connection failure alice.connect( "bad domain" ); alice.waitForConnection(); diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/test_xmpp_simple.js index 50b01da295f..b6b54eabd04 100644 --- a/services/sync/tests/unit/test_xmpp_simple.js +++ b/services/sync/tests/unit/test_xmpp_simple.js @@ -4,9 +4,8 @@ function LOG(aMsg) { Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); -var serverUrl = "http://127.0.0.1:5280/http-poll"; -var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. - getService(Ci.nsIDNSService).myHostName; +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 From db02ef89375c356bf3a7e5ee932c521df68b2c08 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 17:36:37 -0700 Subject: [PATCH 20/22] xmpp-stream-error-handling --- services/sync/modules/xmpp/xmppClient.js | 37 +++++++++++++++--------- services/sync/tests/unit/test_xmpp.js | 9 ++++-- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 77d9ec5ce13..aae2ce171e8 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -91,13 +91,15 @@ XmppClient.prototype = { LOG("onIncomingData(): rcvd: " + messageText); var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); - if (responseDOM.documentElement.nodeName == "parsererror" ) { - // handle server disconnection - if (messageText.match("^$")) { - this._handleServerDisconnection(); - return; - } + // Handle server disconnection + if (messageText.match("^$")) { + 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 element until the communication is done; this means @@ -123,8 +125,19 @@ XmppClient.prototype = { 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) @@ -142,14 +155,8 @@ XmppClient.prototype = { 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" ); @@ -157,6 +164,7 @@ XmppClient.prototype = { LOG( "I see that " + from + " is online." ); } } + if ( rootElem.nodeName == "message" ) { this.processIncomingMessage( rootElem ); } else { @@ -167,6 +175,7 @@ XmppClient.prototype = { } } } + if ( rootElem.nodeName == "iq" ) { this.processIncomingIq( rootElem ); } else { @@ -289,7 +298,7 @@ XmppClient.prototype = { connect: function( host ) { // Do the handshake to connect with the server and authenticate. - this._transportLayer.connect(); + this._transportLayer.connect(host); this._transportLayer.setCallbackObject( this ); this._transportLayer.send( this._makeHeaderXml( host ) ); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 09f718d8f0d..f5346f74d95 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -20,7 +20,7 @@ function run_test() { var transport = new HTTPPollingTransport(serverUrl, false, 4000); var auth = new PlainAuthenticator(); var alice = new XmppClient("alice", jabberDomain, "iamalice", - transport, auth); + transport, auth); // test connection LOG("connecting"); @@ -43,12 +43,15 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); - /* - // test connection failure + // TODO test connection failure - no server + // TODO test connection failure - server up, bad URL + + // 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(); From 58d219c0e7a9e2e52039ba28edf324e2bd72e475 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 18:34:37 -0700 Subject: [PATCH 21/22] imported patch xmpp-transport-fault-tolerance-and-test --- services/sync/modules/xmpp/transportLayer.js | 53 ++++++++++++------ services/sync/modules/xmpp/xmppClient.js | 2 +- services/sync/tests/unit/test_xmpp.js | 3 - .../tests/unit/test_xmpp_transport_http.js | 55 +++++++++++++++++++ 4 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 services/sync/tests/unit/test_xmpp_transport_http.js diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 0b08f9d8630..55e83b7c332 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -7,6 +7,15 @@ 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 = { @@ -141,15 +150,6 @@ SocketClient.prototype = { }; -/* - 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. @@ -173,6 +173,8 @@ HTTPPollingTransport.prototype = { this._useKeys = useKeys; this._interval = interval; this._outgoingRetryBuffer = ""; + this._retryCount = 0; + this._retryCap = 0; }, __request: null, @@ -294,19 +296,36 @@ HTTPPollingTransport.prototype = { 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 (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; - LOG("Sending: " + contents); - 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 ) { diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index aae2ce171e8..2ddc7fdcd28 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -298,7 +298,7 @@ XmppClient.prototype = { connect: function( host ) { // Do the handshake to connect with the server and authenticate. - this._transportLayer.connect(host); + this._transportLayer.connect(); this._transportLayer.setCallbackObject( this ); this._transportLayer.send( this._makeHeaderXml( host ) ); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index f5346f74d95..60396b2e054 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -43,9 +43,6 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); - // TODO test connection failure - no server - // TODO test connection failure - server up, bad URL - // test connection failure - bad domain alice.connect( "bad domain" ); alice.waitForConnection(); diff --git a/services/sync/tests/unit/test_xmpp_transport_http.js b/services/sync/tests/unit/test_xmpp_transport_http.js new file mode 100644 index 00000000000..73b2896465f --- /dev/null +++ b/services/sync/tests/unit/test_xmpp_transport_http.js @@ -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()(); +} From 06dabdbad5f412bae991d6fa37f909cf12bc13ba Mon Sep 17 00:00:00 2001 From: "jonathandicarlo@jonathan-dicarlos-macbook-pro.local" Date: Wed, 4 Jun 2008 20:30:37 -0700 Subject: [PATCH 22/22] The menu icon of a bookmark folder now changes when that folder is being shared out with others. --- services/sync/tests/unit/test_xmpp_simple.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/test_xmpp_simple.js index 50b01da295f..1079aab72b3 100644 --- a/services/sync/tests/unit/test_xmpp_simple.js +++ b/services/sync/tests/unit/test_xmpp_simple.js @@ -10,7 +10,7 @@ var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now - return; + // return; // async test do_test_pending();