diff --git a/services/sync/FormNotifier.js b/services/sync/FormNotifier.js deleted file mode 100644 index 36bc00ad7ca7..000000000000 --- a/services/sync/FormNotifier.js +++ /dev/null @@ -1,67 +0,0 @@ -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -function FormNotifier() { - let formClass = Components.classesByID["{a2059c0e-5a58-4c55-ab7c-26f0557546ef}"] || - Components.classesByID["{0c1bb408-71a2-403f-854a-3a0659829ded}"]; - let baseForm = formClass.getService(Ci.nsIFormHistory2); - let obs = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - - function wrap(method) { - return function() { - let args = Array.slice(arguments); - let notify = function(type) { - obs.notifyObservers(null, "form-notifier", JSON.stringify({ - args: args, - func: method, - type: type - })); - }; - - notify("before"); - try { - return baseForm[method].apply(this, arguments); - } - finally { - notify("after"); - } - }; - } - - this.__defineGetter__("DBConnection", function() baseForm.DBConnection); - this.__defineGetter__("hasEntries", function() baseForm.hasEntries); - - this.addEntry = wrap("addEntry"); - this.entryExists = wrap("entryExists"); - this.nameExists = wrap("nameExists"); - this.removeAllEntries = wrap("removeAllEntries"); - this.removeEntriesByTimeframe = wrap("removeEntriesByTimeframe"); - this.removeEntriesForName = wrap("removeEntriesForName"); - this.removeEntry = wrap("removeEntry"); - - // Avoid leaking the base form service. - obs.addObserver({ - observe: function() { - obs.removeObserver(this, "profile-before-change"); - baseForm = null; - }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, - Ci.nsIObserver]) - }, "profile-before-change", true); -} -FormNotifier.prototype = { - classDescription: "Form Notifier Wrapper", - contractID: "@mozilla.org/satchel/form-history;1", - classID: Components.ID("{be5a097b-6ee6-4c6a-8eca-6bce87d570e9}"), - QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormHistory2]), -}; - -// Gecko <2.0 -function NSGetModule(compMgr, fileSpec) XPCOMUtils.generateModule([FormNotifier]); - -// Gecko >=2.0 -if (typeof XPCOMUtils.generateNSGetFactory == "function") - const NSGetFactory = XPCOMUtils.generateNSGetFactory([FormNotifier]); diff --git a/services/sync/Weave.js b/services/sync/Weave.js index b76885e40721..d5ad96fff4b5 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -53,12 +53,13 @@ WeaveService.prototype = { switch (topic) { case "app-startup": let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); + getService(Ci.nsIObserverService); os.addObserver(this, "final-ui-startup", true); - this.addResourceAlias(); break; case "final-ui-startup": + this.addResourceAlias(); + // Force Weave service to load if it hasn't triggered from overlays this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this.timer.initWithCallback({ diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b50ed0c09011..af7983fbb705 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -54,7 +54,6 @@ catch(ex) { } Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/ext/Observers.js"); Cu.import("resource://services-sync/stores.js"); Cu.import("resource://services-sync/trackers.js"); Cu.import("resource://services-sync/type_records/bookmark.js"); @@ -115,12 +114,12 @@ BookmarksEngine.prototype = { _trackerObj: BookmarksTracker, _handleImport: function _handleImport() { - Observers.add("bookmarks-restore-begin", function() { + Svc.Obs.add("bookmarks-restore-begin", function() { this._log.debug("Ignoring changes from importing bookmarks"); this._tracker.ignoreAll = true; }, this); - Observers.add("bookmarks-restore-success", function() { + Svc.Obs.add("bookmarks-restore-success", function() { this._log.debug("Tracking all items on successful import"); this._tracker.ignoreAll = false; @@ -129,7 +128,7 @@ BookmarksEngine.prototype = { this._tracker.addChangedID(id); }, this); - Observers.add("bookmarks-restore-failed", function() { + Svc.Obs.add("bookmarks-restore-failed", function() { this._tracker.ignoreAll = false; }, this); }, @@ -251,7 +250,7 @@ function BookmarksStore(name) { Store.call(this, name); // Explicitly nullify our references to our cached services so we don't leak - Observers.add("places-shutdown", function() { + Svc.Obs.add("places-shutdown", function() { this.__bms = null; this.__hsvc = null; this.__ls = null; @@ -852,10 +851,12 @@ BookmarksStore.prototype = { get _frecencyStm() { if (!this.__frecencyStm) { this._log.trace("Creating SQL statement: _frecencyStm"); - this.__frecencyStm = Svc.History.DBConnection.createStatement( + this.__frecencyStm = Utils.createStatement( + Svc.History.DBConnection, "SELECT frecency " + "FROM moz_places " + - "WHERE url = :url"); + "WHERE url = :url " + + "LIMIT 1"); } return this.__frecencyStm; }, @@ -869,14 +870,10 @@ BookmarksStore.prototype = { // Add in the bookmark's frecency if we have something if (record.bmkUri != null) { - try { - this._frecencyStm.params.url = record.bmkUri; - if (this._frecencyStm.step()) - index += this._frecencyStm.row.frecency; - } - finally { - this._frecencyStm.reset(); - } + this._frecencyStm.params.url = record.bmkUri; + let result = Utils.queryAsync(this._frecencyStm, ["frecency"]); + if (result.length) + index += result[0].frecency; } return index; @@ -1001,17 +998,37 @@ function BookmarksTracker(name) { for (let guid in kSpecialIds) this.ignoreID(guid); - Svc.Bookmark.addObserver(this, false); - - // Explicitly nullify our references to our cached services so we don't leak - Observers.add("places-shutdown", function() { - this.__ls = null; - this.__bms = null; - }, this); + Svc.Obs.add("places-shutdown", this); + Svc.Obs.add("weave:engine:start-tracking", this); + Svc.Obs.add("weave:engine:stop-tracking", this); } BookmarksTracker.prototype = { __proto__: Tracker.prototype, + _enabled: false, + observe: function observe(subject, topic, data) { + switch (topic) { + case "weave:engine:start-tracking": + if (!this._enabled) { + Svc.Bookmark.addObserver(this, true); + this._enabled = true; + } + break; + case "weave:engine:stop-tracking": + if (this._enabled) { + Svc.Bookmark.removeObserver(this); + this._enabled = false; + } + // Fall through to clean up. + case "places-shutdown": + // Explicitly nullify our references to our cached services so + // we don't leak + this.__ls = null; + this.__bms = null; + break; + } + }, + __bms: null, get _bms() { if (!this.__bms) @@ -1030,7 +1047,8 @@ BookmarksTracker.prototype = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver, - Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS + Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS, + Ci.nsISupportsWeakReference ]), /** diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 69bec968a95c..2a46fc902629 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -110,7 +110,7 @@ let FormWrapper = { createStatement: function createStatement(query) { try { // Just return the statement right away if it's okay - return Svc.Form.DBConnection.createStatement(query); + return Utils.createStatement(Svc.Form.DBConnection, query); } catch(ex) { // Assume guid column must not exist yet, so add it with an index @@ -121,7 +121,7 @@ let FormWrapper = { "ON moz_formhistory (guid)"); // Try creating the query now that the column exists - return Svc.Form.DBConnection.createStatement(query); + return Utils.createStatement(Svc.Form.DBConnection, query); } } }; @@ -202,26 +202,68 @@ FormStore.prototype = { function FormTracker(name) { Tracker.call(this, name); - Svc.Obs.add("form-notifier", this); - - // nsHTMLFormElement doesn't use the normal observer/observe pattern and looks - // up nsIFormSubmitObservers to .notify() them so add manually to observers - Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService). - addObserver(this, "earlyformsubmit", false); + Svc.Obs.add("weave:engine:start-tracking", this); + Svc.Obs.add("weave:engine:stop-tracking", this); } FormTracker.prototype = { __proto__: Tracker.prototype, QueryInterface: XPCOMUtils.generateQI([ Ci.nsIFormSubmitObserver, - Ci.nsIObserver]), + Ci.nsIObserver, + Ci.nsISupportsWeakReference]), trackEntry: function trackEntry(name, value) { this.addChangedID(FormWrapper.getGUID(name, value)); this.score += 10; }, + _enabled: false, observe: function observe(subject, topic, data) { + switch (topic) { + case "weave:engine:start-tracking": + if (!this._enabled) { + Svc.Obs.add("form-notifier", this); + Svc.Obs.add("satchel-storage-changed", this); + // nsHTMLFormElement doesn't use the normal observer/observe + // pattern and looks up nsIFormSubmitObservers to .notify() + // them so add manually to observers + Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService) + .addObserver(this, "earlyformsubmit", true); + this._enabled = true; + } + break; + case "weave:engine:stop-tracking": + if (this._enabled) { + Svc.Obs.remove("form-notifier", this); + Svc.Obs.remove("satchel-storage-changed", this); + Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService) + .removeObserver(this, "earlyformsubmit"); + this._enabled = false; + } + break; + // Firefox 4.0 + case "satchel-storage-changed": + if (data == "addEntry" || data == "before-removeEntry") { + subject = subject.QueryInterface(Ci.nsIArray); + let name = subject.queryElementAt(0, Ci.nsISupportsString) + .toString(); + let value = subject.queryElementAt(1, Ci.nsISupportsString) + .toString(); + this.trackEntry(name, value); + } + break; + // Firefox 3.5/3.6 + case "form-notifier": + this.onFormNotifier(data); + break; + } + }, + + // Firefox 3.5/3.6 + onFormNotifier: function onFormNotifier(data) { let name, value; // Figure out if it's a function that we care about tracking diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 526fa3122fb2..30e17a9e0384 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -88,8 +88,8 @@ function HistoryStore(name) { Store.call(this, name); // Explicitly nullify our references to our cached services so we don't leak - Observers.add("places-shutdown", function() { - for each([query, stmt] in Iterator(this._stmts)) + Svc.Obs.add("places-shutdown", function() { + for each ([query, stmt] in Iterator(this._stmts)) stmt.finalize(); this.__hsvc = null; this._stmts = []; @@ -113,43 +113,87 @@ HistoryStore.prototype = { return this._hsvc.DBConnection; }, - _stmts: [], + _stmts: {}, _getStmt: function(query) { if (query in this._stmts) return this._stmts[query]; this._log.trace("Creating SQL statement: " + query); - return this._stmts[query] = this._db.createStatement(query); + return this._stmts[query] = Utils.createStatement(this._db, query); + }, + + get _haveTempTablesStm() { + return this._getStmt( + "SELECT name FROM sqlite_temp_master " + + "WHERE name IN ('moz_places_temp', 'moz_historyvisits_temp')"); + }, + + get _haveTempTables() { + if (this.__haveTempTables == null) + this.__haveTempTables = !!Utils.queryAsync(this._haveTempTablesStm, + ["name"]).length; + return this.__haveTempTables; }, get _visitStm() { + // Gecko <2.0 + if (this._haveTempTables) { + let where = + "WHERE place_id = IFNULL( " + + "(SELECT id FROM moz_places_temp WHERE url = :url), " + + "(SELECT id FROM moz_places WHERE url = :url) " + + ") "; + return this._getStmt( + "SELECT visit_type type, visit_date date " + + "FROM moz_historyvisits_temp " + where + "UNION " + + "SELECT visit_type type, visit_date date " + + "FROM moz_historyvisits " + where + + "ORDER BY date DESC LIMIT 10 "); + } + // Gecko 2.0 return this._getStmt( "SELECT visit_type type, visit_date date " + - "FROM moz_historyvisits_view " + - "WHERE place_id = (" + - "SELECT id " + - "FROM moz_places_view " + - "WHERE url = :url) " + + "FROM moz_historyvisits " + + "WHERE place_id = (SELECT id FROM moz_places WHERE url = :url) " + "ORDER BY date DESC LIMIT 10"); }, get _urlStm() { - return this._getStmt( - "SELECT url, title, frecency " + - "FROM moz_places_view " + + let where = "WHERE id = (" + "SELECT place_id " + "FROM moz_annos " + "WHERE content = :guid AND anno_attribute_id = (" + "SELECT id " + "FROM moz_anno_attributes " + - "WHERE name = '" + GUID_ANNO + "'))"); + "WHERE name = '" + GUID_ANNO + "')) "; + // Gecko <2.0 + if (this._haveTempTables) + return this._getStmt( + "SELECT url, title, frecency FROM moz_places_temp " + where + + "UNION ALL " + + "SELECT url, title, frecency FROM moz_places " + where + "LIMIT 1"); + // Gecko 2.0 + return this._getStmt( + "SELECT url, title, frecency FROM moz_places " + where + "LIMIT 1"); }, get _allUrlStm() { + // Gecko <2.0 + if (this._haveTempTables) + return this._getStmt( + "SELECT url, frecency FROM moz_places_temp " + + "WHERE last_visit_date > :cutoff_date " + + "UNION " + + "SELECT url, frecency FROM moz_places " + + "WHERE last_visit_date > :cutoff_date " + + "ORDER BY 2 DESC " + + "LIMIT :max_results"); + + // Gecko 2.0 return this._getStmt( "SELECT url " + - "FROM moz_places_view " + + "FROM moz_places " + "WHERE last_visit_date > :cutoff_date " + "ORDER BY frecency DESC " + "LIMIT :max_results"); @@ -191,7 +235,7 @@ HistoryStore.prototype = { }, remove: function HistStore_remove(record) { - let page = this._findURLByGUID(record.id) + let page = this._findURLByGUID(record.id); if (page == null) { this._log.debug("Page already removed: " + record.id); return; @@ -257,14 +301,34 @@ HistoryStore.prototype = { function HistoryTracker(name) { Tracker.call(this, name); - Svc.History.addObserver(this, false); + Svc.Obs.add("weave:engine:start-tracking", this); + Svc.Obs.add("weave:engine:stop-tracking", this); } HistoryTracker.prototype = { __proto__: Tracker.prototype, + _enabled: false, + observe: function observe(subject, topic, data) { + switch (topic) { + case "weave:engine:start-tracking": + if (!this._enabled) { + Svc.History.addObserver(this, true); + this._enabled = true; + } + break; + case "weave:engine:stop-tracking": + if (this._enabled) { + Svc.History.removeObserver(this); + this._enabled = false; + } + break; + } + }, + QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavHistoryObserver, - Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS + Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS, + Ci.nsISupportsWeakReference ]), onBeginUpdateBatch: function HT_onBeginUpdateBatch() {}, diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 8721436d2ff4..f2650be9505a 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -44,7 +44,6 @@ const Ci = Components.interfaces; Cu.import("resource://services-sync/base_records/collection.js"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/ext/Observers.js"); Cu.import("resource://services-sync/stores.js"); Cu.import("resource://services-sync/trackers.js"); Cu.import("resource://services-sync/type_records/passwords.js"); @@ -224,16 +223,33 @@ PasswordStore.prototype = { function PasswordTracker(name) { Tracker.call(this, name); - Observers.add("passwordmgr-storage-changed", this); + Svc.Obs.add("weave:engine:start-tracking", this); + Svc.Obs.add("weave:engine:stop-tracking", this); } PasswordTracker.prototype = { __proto__: Tracker.prototype, - /* A single add, remove or change is 15 points, all items removed is 50 */ + _enabled: false, observe: function PasswordTracker_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "weave:engine:start-tracking": + if (!this._enabled) { + Svc.Obs.add("passwordmgr-storage-changed", this); + this._enabled = true; + } + return; + case "weave:engine:stop-tracking": + if (this._enabled) { + Svc.Obs.remove("passwordmgr-storage-changed", this); + this._enabled = false; + } + return; + } + if (this.ignoreAll) return; + // A single add, remove or change is 15 points, all items removed is 50 switch (aData) { case 'modifyLogin': aSubject = aSubject.QueryInterface(Ci.nsIArray). diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 757954b63fdf..7eb863314fb2 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -76,25 +76,30 @@ PrefsEngine.prototype = { function PrefStore(name) { Store.call(this, name); + Svc.Obs.add("profile-before-change", function() { + this.__prefs = null; + this.__syncPrefs = null; + }, this); } PrefStore.prototype = { __proto__: Store.prototype, + __prefs: null, get _prefs() { - let prefs = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); - - this.__defineGetter__("_prefs", function() prefs); - return prefs; + if (!this.__prefs) + this.__prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch2); + return this.__prefs; }, + __syncPrefs: null, get _syncPrefs() { - let service = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefService); - let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}); - - this.__defineGetter__("_syncPrefs", function() syncPrefs); - return syncPrefs; + if (!this.__syncPrefs) + this.__syncPrefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(WEAVE_SYNC_PREFS). + getChildList("", {}); + return this.__syncPrefs; }, _getAllPrefs: function PrefStore__getAllPrefs() { @@ -211,7 +216,7 @@ PrefStore.prototype = { }, remove: function PrefStore_remove(record) { - this._log.trace("Ignoring remove request") + this._log.trace("Ignoring remove request"); }, update: function PrefStore_update(record) { @@ -226,37 +231,59 @@ PrefStore.prototype = { function PrefTracker(name) { Tracker.call(this, name); - this._prefs.addObserver("", this, false); + Svc.Obs.add("profile-before-change", this); + Svc.Obs.add("weave:engine:start-tracking", this); + Svc.Obs.add("weave:engine:stop-tracking", this); } PrefTracker.prototype = { __proto__: Tracker.prototype, + __prefs: null, get _prefs() { - let prefs = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch2); - - this.__defineGetter__("_prefs", function() prefs); - return prefs; + if (!this.__prefs) + this.__prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch2); + return this.__prefs; }, + __syncPrefs: null, get _syncPrefs() { - let service = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefService); - let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}); - - this.__defineGetter__("_syncPrefs", function() syncPrefs); - return syncPrefs; + if (!this.__syncPrefs) + this.__syncPrefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(WEAVE_SYNC_PREFS). + getChildList("", {}); + return this.__syncPrefs; }, - /* 25 points per pref change */ + _enabled: false, observe: function(aSubject, aTopic, aData) { - if (aTopic != "nsPref:changed") - return; - - if (this._syncPrefs.indexOf(aData) != -1) { - this.score += 1; - this.addChangedID(WEAVE_PREFS_GUID); - this._log.trace("Preference " + aData + " changed"); + switch (aTopic) { + case "weave:engine:start-tracking": + if (!this._enabled) { + this._prefs.addObserver("", this, false); + this._enabled = true; + } + break; + case "weave:engine:stop-tracking": + if (this._enabled) { + this._prefs.removeObserver("", this); + this._enabled = false; + } + // Fall through to clean up. + case "profile-before-change": + this.__prefs = null; + this.__syncPrefs = null; + this._prefs.removeObserver("", this); + break; + case "nsPref:changed": + // 25 points per pref change + if (this._syncPrefs.indexOf(aData) != -1) { + this.score += 1; + this.addChangedID(WEAVE_PREFS_GUID); + this._log.trace("Preference " + aData + " changed"); + } + break; } } }; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e8eab8e2f85c..4c1e0f5a9eea 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -225,53 +225,75 @@ TabStore.prototype = { function TabTracker(name) { Tracker.call(this, name); - - Svc.Obs.add("private-browsing", this); + Svc.Obs.add("weave:engine:start-tracking", this); + Svc.Obs.add("weave:engine:stop-tracking", this); // Make sure "this" pointer is always set correctly for event listeners this.onTab = Utils.bind2(this, this.onTab); - - // Register as an observer so we can catch windows opening and closing: - Svc.WinWatcher.registerNotification(this); - - // Also register listeners on already open windows - let wins = Svc.WinMediator.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) - this._registerListenersForWindow(wins.getNext()); + this._unregisterListeners = Utils.bind2(this, this._unregisterListeners); } TabTracker.prototype = { __proto__: Tracker.prototype, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - _registerListenersForWindow: function TabTracker__registerListen(window) { - this._log.trace("Registering tab listeners in new window"); - - // For each topic, add or remove onTab as the listener - let topics = ["pageshow", "TabOpen", "TabClose", "TabSelect"]; - let onTab = this.onTab; - let addRem = function(add) topics.forEach(function(topic) { - window[(add ? "add" : "remove") + "EventListener"](topic, onTab, false); - }); - - // Add the listeners now and remove them on unload - addRem(true); - window.addEventListener("unload", function() addRem(false), false); + _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"], + _registerListenersForWindow: function registerListenersFW(window) { + this._log.trace("Registering tab listeners in window"); + for each (let topic in this._topics) { + window.addEventListener(topic, this.onTab, false); + } + window.addEventListener("unload", this._unregisterListeners, false); }, - observe: function TabTracker_observe(aSubject, aTopic, aData) { - // Add tab listeners now that a window has opened - if (aTopic == "domwindowopened") { - let self = this; - aSubject.addEventListener("load", function onLoad(event) { - aSubject.removeEventListener("load", onLoad, false); - // Only register after the window is done loading to avoid unloads - self._registerListenersForWindow(aSubject); - }, false); + _unregisterListeners: function unregisterListeners(event) { + this._unregisterListenersForWindow(event.target); + }, + + _unregisterListenersForWindow: function unregisterListenersFW(window) { + this._log.trace("Removing tab listeners in window"); + window.removeEventListener("unload", this._unregisterListeners, false); + for each (let topic in this._topics) { + window.removeEventListener(topic, this.onTab, false); + } + }, + + _enabled: false, + observe: function TabTracker_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "weave:engine:start-tracking": + if (!this._enabled) { + Svc.Obs.add("private-browsing", this); + Svc.Obs.add("domwindowopened", this); + let wins = Svc.WinMediator.getEnumerator("navigator:browser"); + while (wins.hasMoreElements()) + this._registerListenersForWindow(wins.getNext()); + this._enabled = true; + } + break; + case "weave:engine:stop-tracking": + if (this._enabled) { + Svc.Obs.remove("private-browsing", this); + Svc.Obs.remove("domwindowopened", this); + let wins = Svc.WinMediator.getEnumerator("navigator:browser"); + while (wins.hasMoreElements()) + this._unregisterListenersForWindow(wins.getNext()); + this._enabled = false; + } + return; + case "domwindowopened": + // Add tab listeners now that a window has opened + let self = this; + aSubject.addEventListener("load", function onLoad(event) { + aSubject.removeEventListener("load", onLoad, false); + // Only register after the window is done loading to avoid unloads + self._registerListenersForWindow(aSubject); + }, false); + break; + case "private-browsing": + if (aData == "enter" && !PBPrefs.get("autostart")) + this.clearChangedIDs(); } - else if (aTopic == "private-browsing" && aData == "enter" - && !PBPrefs.get("autostart")) - this.clearChangedIDs(); }, onTab: function onTab(event) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e01f332f8df8..aef2258403de 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -273,6 +273,7 @@ WeaveSvc.prototype = { "Weave, since it will not work correctly."); } + Svc.Obs.add("weave:service:setup-complete", this); Svc.Obs.add("network:offline-status-changed", this); Svc.Obs.add("weave:service:sync:finish", this); Svc.Obs.add("weave:service:sync:error", this); @@ -292,6 +293,10 @@ WeaveSvc.prototype = { this._updateCachedURLs(); + let status = this._checkSetup(); + if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) + Svc.Obs.notify("weave:engine:start-tracking"); + // Applications can specify this preference if they want autoconnect // to happen after a fixed delay. let delay = Svc.Prefs.get("autoconnectDelay"); @@ -306,7 +311,10 @@ WeaveSvc.prototype = { }, _checkSetup: function WeaveSvc__checkSetup() { - if (!this.username) { + if (!this.enabled) { + Status.service = STATUS_DISABLED; + } + else if (!this.username) { this._log.debug("checkSetup: no username set"); Status.login = LOGIN_FAILED_NO_USERNAME; } @@ -404,6 +412,11 @@ WeaveSvc.prototype = { observe: function WeaveSvc__observe(subject, topic, data) { switch (topic) { + case "weave:service:setup-complete": + let status = this._checkSetup(); + if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) + Svc.Obs.notify("weave:engine:start-tracking"); + break; case "network:offline-status-changed": // Whether online or offline, we'll reschedule syncs this._log.trace("Network offline status change: " + data); @@ -704,6 +717,7 @@ WeaveSvc.prototype = { Svc.Login.removeLogin(login); }); Svc.Obs.notify("weave:service:start-over"); + Svc.Obs.notify("weave:engine:stop-tracking"); }, delayedAutoConnect: function delayedAutoConnect(delay) { @@ -752,6 +766,7 @@ WeaveSvc.prototype = { if (Svc.IO.offline) throw "Application is offline, login should not be called"; + let initialStatus = this._checkSetup(); if (username) this.username = username; if (password) @@ -762,6 +777,12 @@ WeaveSvc.prototype = { if (this._checkSetup() == CLIENT_NOT_CONFIGURED) throw "aborting login, client not configured"; + // Calling login() with parameters when the client was + // previously not configured means setup was completed. + if (initialStatus == CLIENT_NOT_CONFIGURED + && (username || password || passphrase)) + Svc.Obs.notify("weave:service:setup-complete"); + this._log.info("Logging in user " + this.username); if (!this.verifyLogin()) { @@ -1317,8 +1338,6 @@ WeaveSvc.prototype = { // we'll handle that later Status.resetBackoff(); - this.globalScore = 0; - // Ping the server with a special info request once a day let infoURL = this.infoURL; let now = Math.floor(Date.now() / 1000); @@ -1330,8 +1349,15 @@ WeaveSvc.prototype = { // Figure out what the last modified time is for each collection let info = new Resource(infoURL).get(); - if (!info.success) + if (!info.success) { + if (info.status == 401) { + this.logout(); + Status.login = LOGIN_FAILED_LOGIN_REJECTED; + } throw "aborting sync, failed to get collections"; + } + + this.globalScore = 0; // Convert the response to an object and read out the modified times for each (let engine in [Clients].concat(Engines.getAll())) @@ -1498,7 +1524,7 @@ WeaveSvc.prototype = { if (Utils.checkStatus(resp.status, null, [500, [502, 504]])) { Status.enforceBackoff = true; if (resp.status == 503 && resp.headers["retry-after"]) - Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["retry-after"], 10)); + Svc.Obs.notify("weave:service:backoff:interval", parseInt(resp.headers["retry-after"], 10)); } }, /** @@ -1521,7 +1547,7 @@ WeaveSvc.prototype = { * Array of collections to wipe. If not given, all collections are wiped. */ wipeServer: function WeaveSvc_wipeServer(collections) - this._catch(this._notify("wipe-server", "", function() { + this._notify("wipe-server", "", function() { if (!collections) { collections = []; let info = new Resource(this.infoURL).get(); @@ -1529,19 +1555,23 @@ WeaveSvc.prototype = { collections.push(name); } for each (let name in collections) { - try { - new Resource(this.storageURL + name).delete(); - - // Remove the crypto record from the server and local cache - let crypto = this.storageURL + "crypto/" + name; - new Resource(crypto).delete(); - CryptoMetas.del(crypto); + let url = this.storageURL + name; + let response = new Resource(url).delete(); + if (response.status != 200 && response.status != 404) { + throw "Aborting wipeServer. Server responded with " + + response.status + " response for " + url; } - catch(ex) { - this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); + + // Remove the crypto record from the server and local cache + let crypto = this.storageURL + "crypto/" + name; + response = new Resource(crypto).delete(); + CryptoMetas.del(crypto); + if (response.status != 200 && response.status != 404) { + throw "Aborting wipeServer. Server responded with " + + response.status + " response for " + crypto; } } - }))(), + })(), /** * Wipe all local user data. diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 2db234d030a2..93edf619e757 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -146,6 +146,15 @@ let Utils = { throw batchEx; }; }, + + createStatement: function createStatement(db, query) { + // Gecko 2.0 + if (db.createAsyncStatement) + return db.createAsyncStatement(query); + + // Gecko <2.0 + return db.createStatement(query); + }, queryAsync: function(query, names) { // Allow array of names, single name, and no name @@ -909,3 +918,8 @@ Svc.Obs = Observers; let Str = {}; ["errors", "sync"] .forEach(function(lazy) Utils.lazy2(Str, lazy, Utils.lazyStrings(lazy))); + +Svc.Obs.add("xpcom-shutdown", function () { + for (let name in Svc) + delete Svc[name]; +}); diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js index 596498f5408e..83a056e5d00c 100644 --- a/services/sync/tests/unit/head_helpers.js +++ b/services/sync/tests/unit/head_helpers.js @@ -8,11 +8,18 @@ let ds = Cc["@mozilla.org/file/directory_service;1"] let provider = { getFile: function(prop, persistent) { persistent.value = true; - if (prop == "ExtPrefDL") - return [ds.get("CurProcD", Ci.nsIFile)]; - else if (prop == "ProfD") - return ds.get("CurProcD", Ci.nsIFile); - throw Cr.NS_ERROR_FAILURE; + switch (prop) { + case "ExtPrefDL": + return [ds.get("CurProcD", Ci.nsIFile)]; + case "ProfD": + return ds.get("CurProcD", Ci.nsIFile); + case "UHist": + let histFile = ds.get("CurProcD", Ci.nsIFile); + histFile.append("history.dat"); + return histFile; + default: + throw Cr.NS_ERROR_FAILURE; + } }, QueryInterface: function(iid) { if (iid.equals(Ci.nsIDirectoryServiceProvider) || diff --git a/services/sync/tests/unit/test_bookmark_store.js b/services/sync/tests/unit/test_bookmark_store.js new file mode 100644 index 000000000000..b337a2daf51d --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_store.js @@ -0,0 +1,49 @@ +Cu.import("resource://services-sync/engines/bookmarks.js"); +Cu.import("resource://services-sync/util.js"); + +function run_test() { + let store = new BookmarksEngine()._store; + store.wipe(); + + try { + _("Ensure the record isn't present yet."); + let fxuri = Utils.makeURI("http://getfirefox.com/"); + let ids = Svc.Bookmark.getBookmarkIdsForURI(fxuri, {}); + do_check_eq(ids.length, 0); + + _("Let's create a new record."); + let fxrecord = {id: "{5d81b87c-d5fc-42d9-a114-d69b7342f10e}0", + type: "bookmark", + bmkUri: fxuri.spec, + title: "Get Firefox!", + tags: [], + keyword: "awesome", + loadInSidebar: false, + parentName: "Bookmarks Toolbar", + parentid: "toolbar"}; + store.applyIncoming(fxrecord); + + _("Verify it has been created correctly."); + ids = Svc.Bookmark.getBookmarkIdsForURI(fxuri, {}); + do_check_eq(ids.length, 1); + let id = ids[0]; + do_check_eq(Svc.Bookmark.getItemGUID(id), fxrecord.id); + do_check_eq(Svc.Bookmark.getItemType(id), Svc.Bookmark.TYPE_BOOKMARK); + do_check_eq(Svc.Bookmark.getItemTitle(id), fxrecord.title); + do_check_eq(Svc.Bookmark.getFolderIdForItem(id), + Svc.Bookmark.toolbarFolder); + do_check_eq(Svc.Bookmark.getKeywordForBookmark(id), fxrecord.keyword); + + _("Have the store create a new record object. Verify that it has the same data."); + let newrecord = store.createRecord(fxrecord.id); + for each (let property in ["type", "bmkUri", "title", "keyword", + "parentName", "parentid"]) + do_check_eq(newrecord[property], fxrecord[property]); + + _("The calculated sort index is based on frecency data."); + do_check_true(newrecord.sortindex >= 150); + } finally { + _("Clean up."); + store.wipe(); + } +} diff --git a/services/sync/tests/unit/test_bookmark_tracker.js b/services/sync/tests/unit/test_bookmark_tracker.js new file mode 100644 index 000000000000..b837afb78f3b --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_tracker.js @@ -0,0 +1,51 @@ +Cu.import("resource://services-sync/engines/bookmarks.js"); +Cu.import("resource://services-sync/util.js"); + +function run_test() { + let engine = new BookmarksEngine(); + engine._store.wipe(); + + _("Verify we've got an empty tracker to work with."); + let tracker = engine._tracker; + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + let folder = Svc.Bookmark.createFolder(Svc.Bookmark.bookmarksMenuFolder, + "Test Folder", + Svc.Bookmark.DEFAULT_INDEX); + function createBmk() { + Svc.Bookmark.insertBookmark(folder, + Utils.makeURI("http://getfirefox.com"), + Svc.Bookmark.DEFAULT_INDEX, + "Get Firefox!"); + } + + try { + _("Create bookmark. Won't show because we haven't started tracking yet"); + createBmk(); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + _("Tell the tracker to start tracking changes."); + Svc.Obs.notify("weave:engine:start-tracking"); + createBmk(); + do_check_eq([id for (id in tracker.changedIDs)].length, 1); + + _("Notifying twice won't do any harm."); + Svc.Obs.notify("weave:engine:start-tracking"); + createBmk(); + do_check_eq([id for (id in tracker.changedIDs)].length, 2); + + _("Let's stop tracking again."); + tracker.clearChangedIDs(); + Svc.Obs.notify("weave:engine:stop-tracking"); + createBmk(); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + _("Notifying twice won't do any harm."); + Svc.Obs.notify("weave:engine:stop-tracking"); + createBmk(); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + } finally { + _("Clean up."); + engine._store.wipe(); + } +} diff --git a/services/sync/tests/unit/test_forms_store.js b/services/sync/tests/unit/test_forms_store.js new file mode 100644 index 000000000000..9caa4c68c2b6 --- /dev/null +++ b/services/sync/tests/unit/test_forms_store.js @@ -0,0 +1,81 @@ +_("Make sure the form store follows the Store api and correctly accesses the backend form storage"); +Cu.import("resource://services-sync/engines/forms.js"); +Cu.import("resource://services-sync/type_records/forms.js"); + +function run_test() { + let store = new FormEngine()._store; + + _("Remove any existing entries"); + store.wipe(); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } + + _("Add a form entry"); + store.create({ + name: "name!!", + value: "value??" + }); + + _("Should have 1 entry now"); + let id = ""; + for (let _id in store.getAllIDs()) { + if (id == "") + id = _id; + else + do_throw("Should have only gotten one!"); + } + do_check_true(store.itemExists(id)); + + let rec = store.createRecord(id); + _("Got record for id", id, rec); + do_check_eq(rec.name, "name!!"); + do_check_eq(rec.value, "value??"); + + _("Create a non-existant id for delete"); + do_check_true(store.createRecord("deleted!!").deleted); + + _("Try updating.. doesn't do anything yet"); + store.update({}); + + _("Remove all entries"); + store.wipe(); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } + + _("Add another entry"); + store.create({ + name: "another", + value: "entry" + }); + id = ""; + for (let _id in store.getAllIDs()) { + if (id == "") + id = _id; + else + do_throw("Should have only gotten one!"); + } + + _("Change the id of the new entry to something else"); + store.changeItemID(id, "newid"); + + _("Make sure it's there"); + do_check_true(store.itemExists("newid")); + + _("Remove the entry"); + store.remove({ + id: "newid" + }); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } + + _("Removing the entry again shouldn't matter"); + store.remove({ + id: "newid" + }); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } +} diff --git a/services/sync/tests/unit/test_forms_tracker.js b/services/sync/tests/unit/test_forms_tracker.js new file mode 100644 index 000000000000..46ff13802be6 --- /dev/null +++ b/services/sync/tests/unit/test_forms_tracker.js @@ -0,0 +1,39 @@ +Cu.import("resource://services-sync/engines/forms.js"); +Cu.import("resource://services-sync/util.js"); + +function run_test() { + _("Verify we've got an empty tracker to work with."); + let tracker = new FormEngine()._tracker; + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + try { + _("Create an entry. Won't show because we haven't started tracking yet"); + Svc.Form.addEntry("name", "John Doe"); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + _("Tell the tracker to start tracking changes."); + Svc.Obs.notify("weave:engine:start-tracking"); + Svc.Form.removeEntry("name", "John Doe"); + Svc.Form.addEntry("email", "john@doe.com"); + do_check_eq([id for (id in tracker.changedIDs)].length, 2); + + _("Notifying twice won't do any harm."); + Svc.Obs.notify("weave:engine:start-tracking"); + Svc.Form.addEntry("address", "Memory Lane"); + do_check_eq([id for (id in tracker.changedIDs)].length, 3); + + _("Let's stop tracking again."); + tracker.clearChangedIDs(); + Svc.Obs.notify("weave:engine:stop-tracking"); + Svc.Form.removeEntry("address", "Memory Lane"); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + _("Notifying twice won't do any harm."); + Svc.Obs.notify("weave:engine:stop-tracking"); + Svc.Form.removeEntry("email", "john@doe.com"); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + } finally { + _("Clean up."); + Svc.Form.removeAllEntries(); + } +} diff --git a/services/sync/tests/unit/test_history_store.js b/services/sync/tests/unit/test_history_store.js new file mode 100644 index 000000000000..c239136c9dc3 --- /dev/null +++ b/services/sync/tests/unit/test_history_store.js @@ -0,0 +1,135 @@ +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://services-sync/engines/history.js"); +Cu.import("resource://services-sync/type_records/history.js"); +Cu.import("resource://services-sync/ext/Sync.js"); +Cu.import("resource://services-sync/util.js"); + +const TIMESTAMP1 = 1281077113313976; +const TIMESTAMP2 = 1281088209595212; +const TIMESTAMP3 = 1281199249129950; + +function queryPlaces(uri, options) { + let query = Svc.History.getNewQuery(); + query.uri = uri; + let res = Svc.History.executeQuery(query, options); + res.root.containerOpen = true; + + let results = []; + for (let i = 0; i < res.root.childCount; i++) + results.push(res.root.getChild(i)); + return results; +} + +function queryHistoryVisits(uri) { + let options = Svc.History.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; + options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING; + return queryPlaces(uri, options); +} + +function waitForTitleChanged(test) { + let [exec, cb] = Sync.withCb(function (callback) { + Svc.History.addObserver({ + onBeginUpdateBatch: function onBeginUpdateBatch() {}, + onEndUpdateBatch: function onEndUpdateBatch() {}, + onPageChanged: function onPageChanged() {}, + onTitleChanged: function onTitleChanged() { + Svc.History.removeObserver(this); + callback(); + }, + onVisit: function onVisit() {}, + onDeleteVisits: function onDeleteVisits() {}, + onPageExpired: function onPageExpired() {}, + onBeforeDeleteURI: function onBeforeDeleteURI() {}, + onDeleteURI: function onDeleteURI() {}, + onClearHistory: function onClearHistory() {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavHistoryObserver, + Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS, + Ci.nsISupportsWeakReference + ]) + }, true); + test(); + }); + exec(cb); +} + +function run_test() { + _("Verify that we've got an empty store to work with."); + let store = new HistoryEngine()._store; + do_check_eq([id for (id in store.getAllIDs())].length, 0); + + try { + _("Let's create an entry in the database."); + let fxuri = Utils.makeURI("http://getfirefox.com/"); + Svc.History.addPageWithDetails(fxuri, "Get Firefox!", TIMESTAMP1); + + _("Verify that the entry exists."); + let ids = [id for (id in store.getAllIDs())]; + do_check_eq(ids.length, 1); + let fxguid = ids[0]; + do_check_true(store.itemExists(fxguid)); + + _("If we query a non-existent record, it's marked as deleted."); + let record = store.createRecord("non-existent"); + do_check_true(record.deleted); + + _("Verify createRecord() returns a complete record."); + record = store.createRecord(fxguid); + do_check_eq(record.histUri, fxuri.spec); + do_check_eq(record.title, "Get Firefox!"); + do_check_eq(record.visits.length, 1); + do_check_eq(record.visits[0].date, TIMESTAMP1); + do_check_eq(record.visits[0].type, Ci.nsINavHistoryService.TRANSITION_LINK); + + _("Let's modify the record and have the store update the database."); + let secondvisit = {date: TIMESTAMP2, + type: Ci.nsINavHistoryService.TRANSITION_TYPED}; + waitForTitleChanged(function() { + store.update({histUri: record.histUri, + title: "Hol Dir Firefox!", + visits: [record.visits[0], secondvisit]}); + }); + let queryres = queryHistoryVisits(fxuri); + do_check_eq(queryres.length, 2); + do_check_eq(queryres[0].time, TIMESTAMP1); + do_check_eq(queryres[0].title, "Hol Dir Firefox!"); + do_check_eq(queryres[1].time, TIMESTAMP2); + do_check_eq(queryres[1].title, "Hol Dir Firefox!"); + + _("Create a brand new record through the store."); + let tbguid = Utils.makeGUID(); + let tburi = Utils.makeURI("http://getthunderbird.com"); + waitForTitleChanged(function() { + store.create({id: tbguid, + histUri: tburi.spec, + title: "The bird is the word!", + visits: [{date: TIMESTAMP3, + type: Ci.nsINavHistoryService.TRANSITION_TYPED}]}); + }); + do_check_eq([id for (id in store.getAllIDs())].length, 2); + queryres = queryHistoryVisits(tburi); + do_check_eq(queryres.length, 1); + do_check_eq(queryres[0].time, TIMESTAMP3); + do_check_eq(queryres[0].title, "The bird is the word!"); + + _("Remove a record from the store."); + store.remove({id: fxguid}); + do_check_false(store.itemExists(fxguid)); + queryres = queryHistoryVisits(fxuri); + do_check_eq(queryres.length, 0); + + _("Make sure wipe works."); + store.wipe(); + do_check_eq([id for (id in store.getAllIDs())].length, 0); + queryres = queryHistoryVisits(fxuri); + do_check_eq(queryres.length, 0); + queryres = queryHistoryVisits(tburi); + do_check_eq(queryres.length, 0); + + } finally { + _("Clean up."); + Svc.History.removeAllPages(); + } +} diff --git a/services/sync/tests/unit/test_history_tracker.js b/services/sync/tests/unit/test_history_tracker.js new file mode 100644 index 000000000000..9bc512a0ca6c --- /dev/null +++ b/services/sync/tests/unit/test_history_tracker.js @@ -0,0 +1,46 @@ +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://services-sync/engines/history.js"); +Cu.import("resource://services-sync/util.js"); + +function run_test() { + _("Verify we've got an empty tracker to work with."); + let tracker = new HistoryEngine()._tracker; + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + let _counter = 0; + function addVisit() { + Svc.History.addVisit(Utils.makeURI("http://getfirefox.com/" + _counter), + Date.now() * 1000, null, 1, false, 0); + _counter += 1; + } + + try { + _("Create bookmark. Won't show because we haven't started tracking yet"); + addVisit(); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + _("Tell the tracker to start tracking changes."); + Svc.Obs.notify("weave:engine:start-tracking"); + addVisit(); + do_check_eq([id for (id in tracker.changedIDs)].length, 1); + + _("Notifying twice won't do any harm."); + Svc.Obs.notify("weave:engine:start-tracking"); + addVisit(); + do_check_eq([id for (id in tracker.changedIDs)].length, 2); + + _("Let's stop tracking again."); + tracker.clearChangedIDs(); + Svc.Obs.notify("weave:engine:stop-tracking"); + addVisit(); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + + _("Notifying twice won't do any harm."); + Svc.Obs.notify("weave:engine:stop-tracking"); + addVisit(); + do_check_eq([id for (id in tracker.changedIDs)].length, 0); + } finally { + _("Clean up."); + Svc.History.removeAllPages(); + } +} diff --git a/services/sync/tests/unit/test_service_login.js b/services/sync/tests/unit/test_service_login.js index 73d14a0238d7..7148f2aa2647 100644 --- a/services/sync/tests/unit/test_service_login.js +++ b/services/sync/tests/unit/test_service_login.js @@ -35,7 +35,8 @@ function run_test() { Weave.Service.clusterURL = "http://localhost:8080/"; Svc.Prefs.set("autoconnect", false); - _("Initial state is ok."); + _("Force the initial state."); + Status.service = STATUS_OK; do_check_eq(Status.service, STATUS_OK); _("Try logging in. It wont' work because we're not configured yet."); @@ -77,6 +78,21 @@ function run_test() { do_check_eq(Status.login, LOGIN_SUCCEEDED); do_check_true(Weave.Service.isLoggedIn); do_check_true(Svc.Prefs.get("autoconnect")); + + _("Calling login() with parameters when the client is unconfigured sends notification."); + let notified = false; + Weave.Svc.Obs.add("weave:service:setup-complete", function() { + notified = true; + }); + Weave.Service.username = ""; + Weave.Service.password = ""; + Weave.Service.passphrase = ""; + Weave.Service.login("janedoe", "ilovejohn", "bar"); + do_check_true(notified); + do_check_eq(Status.service, STATUS_OK); + do_check_eq(Status.login, LOGIN_SUCCEEDED); + do_check_true(Weave.Service.isLoggedIn); + do_check_true(Svc.Prefs.get("autoconnect")); _("Logout."); Weave.Service.logout(); diff --git a/services/sync/tests/unit/test_service_sync_401.js b/services/sync/tests/unit/test_service_sync_401.js new file mode 100644 index 000000000000..ab348dd0a321 --- /dev/null +++ b/services/sync/tests/unit/test_service_sync_401.js @@ -0,0 +1,71 @@ +Cu.import("resource://services-sync/service.js"); + +function login_handler(request, response) { + // btoa('johndoe:ilovejane') == am9obmRvZTppbG92ZWphbmU= + let body; + if (request.hasHeader("Authorization") && + request.getHeader("Authorization") == "Basic am9obmRvZTppbG92ZWphbmU=") { + body = "{}"; + response.setStatusLine(request.httpVersion, 200, "OK"); + } else { + body = "Unauthorized"; + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + } + response.bodyOutputStream.write(body, body.length); +} + +function run_test() { + let logger = Log4Moz.repository.rootLogger; + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + do_test_pending(); + let server = httpd_setup({ + "/1.0/johndoe/info/collections": login_handler + }); + + const GLOBAL_SCORE = 42; + + try { + _("Set up test fixtures."); + Weave.Service.serverURL = "http://localhost:8080/"; + Weave.Service.clusterURL = "http://localhost:8080/"; + Weave.Service.username = "johndoe"; + Weave.Service.password = "ilovejane"; + Weave.Service.passphrase = "foo"; + Weave.Service.globalScore = GLOBAL_SCORE; + // Avoid daily ping + Weave.Svc.Prefs.set("lastPing", Math.floor(Date.now() / 1000)); + + let threw = false; + Weave.Svc.Obs.add("weave:service:sync:error", function (subject, data) { + threw = true; + }); + + _("Initial state: We're successfully logged in."); + Weave.Service.login(); + do_check_true(Weave.Service.isLoggedIn); + do_check_eq(Weave.Status.login, Weave.LOGIN_SUCCEEDED); + + _("Simulate having changed the password somehwere else."); + Weave.Service.password = "ilovejosephine"; + + _("Let's try to sync."); + Weave.Service.sync(); + + _("Verify that sync() threw an exception."); + do_check_true(threw); + + _("We're no longer logged in."); + do_check_false(Weave.Service.isLoggedIn); + + _("Sync status."); + do_check_eq(Weave.Status.login, Weave.LOGIN_FAILED_LOGIN_REJECTED); + + _("globalScore is unchanged."); + do_check_eq(Weave.Service.globalScore, GLOBAL_SCORE); + + } finally { + Weave.Svc.Prefs.resetBranch(""); + server.stop(do_test_finished); + } +} diff --git a/services/sync/tests/unit/test_service_verifyLogin.js b/services/sync/tests/unit/test_service_verifyLogin.js index ef0e369f2f47..8cfcabf63b73 100644 --- a/services/sync/tests/unit/test_service_verifyLogin.js +++ b/services/sync/tests/unit/test_service_verifyLogin.js @@ -18,20 +18,36 @@ function login_handler(request, response) { response.bodyOutputStream.write(body, body.length); } +function send(statusCode, status, body) { + return function(request, response) { + response.setStatusLine(request.httpVersion, statusCode, status); + response.bodyOutputStream.write(body, body.length); + }; +} + +function service_unavailable(request, response) { + let body = "Service Unavailable"; + response.setStatusLine(request.httpVersion, 503, "Service Unavailable"); + response.setHeader("Retry-After", "42"); + response.bodyOutputStream.write(body, body.length); +} + function run_test() { let logger = Log4Moz.repository.rootLogger; Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); do_test_pending(); let server = httpd_setup({ - "/1.0/johndoe/info/collections": login_handler + "/api/1.0/johndoe/info/collections": login_handler, + "/api/1.0/janedoe/info/collections": service_unavailable, + "/user/1.0/johndoe/node/weave": send(200, "OK", "http://localhost:8080/api/") }); try { Weave.Service.serverURL = "http://localhost:8080/"; - Weave.Service.clusterURL = "http://localhost:8080/"; - _("Initial state is ok."); + _("Force the initial state."); + Status.service = STATUS_OK; do_check_eq(Status.service, STATUS_OK); _("Credentials won't check out because we're not configured yet."); @@ -46,12 +62,27 @@ function run_test() { do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE); + _("verifyLogin() has found out the user's cluster URL, though."); + do_check_eq(Weave.Service.clusterURL, "http://localhost:8080/api/"); + _("Success if passphrase is set."); Weave.Service.passphrase = "foo"; - Weave.Service.login(); + do_check_true(Weave.Service.verifyLogin()); do_check_eq(Status.service, STATUS_OK); do_check_eq(Status.login, LOGIN_SUCCEEDED); - do_check_true(Weave.Service.isLoggedIn); + + _("If verifyLogin() encounters a server error, it flips on the backoff flag and notifies observers on a 503 with Retry-After."); + Weave.Service.username = "janedoe"; + do_check_false(Status.enforceBackoff); + let backoffInterval; + Svc.Obs.add("weave:service:backoff:interval", function(subject, data) { + backoffInterval = subject; + }); + do_check_false(Weave.Service.verifyLogin()); + do_check_true(Status.enforceBackoff); + do_check_eq(backoffInterval, 42); + do_check_eq(Status.service, LOGIN_FAILED); + do_check_eq(Status.login, LOGIN_FAILED_SERVER_ERROR); } finally { Svc.Prefs.resetBranch(""); diff --git a/services/sync/tests/unit/test_service_wipeServer.js b/services/sync/tests/unit/test_service_wipeServer.js new file mode 100644 index 000000000000..07d17b3de43c --- /dev/null +++ b/services/sync/tests/unit/test_service_wipeServer.js @@ -0,0 +1,194 @@ +Cu.import("resource://services-sync/util.js"); +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/resource.js"); + +function FakeCollection() { + this.deleted = false; +} +FakeCollection.prototype = { + handler: function() { + let self = this; + return function(request, response) { + let body = ""; + if (request.method == "DELETE") { + body = JSON.stringify(Date.now() / 1000); + self.deleted = true; + } + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); + }; + } +}; + +function serviceUnavailable(request, response) { + let body = "Service Unavailable"; + response.setStatusLine(request.httpVersion, 503, "Service Unavailable"); + response.bodyOutputStream.write(body, body.length); +} + +function createAndUploadKeypair() { + let keys = PubKeys.createKeypair(ID.get("WeaveCryptoID"), + PubKeys.defaultKeyUri, + PrivKeys.defaultKeyUri); + PubKeys.uploadKeypair(keys); +} + +function createAndUploadSymKey(url) { + let symkey = Svc.Crypto.generateRandomKey(); + let pubkey = PubKeys.getDefaultKey(); + let meta = new CryptoMeta(url); + meta.addUnwrappedKey(pubkey, symkey); + let res = new Resource(meta.uri); + res.put(meta); + CryptoMetas.set(url, meta); +} + +function setUpTestFixtures() { + let cryptoService = new FakeCryptoService(); + + Weave.Service.clusterURL = "http://localhost:8080/"; + Weave.Service.username = "johndoe"; + Weave.Service.passphrase = "secret"; + + createAndUploadKeypair(); + createAndUploadSymKey("http://localhost:8080/1.0/johndoe/storage/crypto/steam"); + createAndUploadSymKey("http://localhost:8080/1.0/johndoe/storage/crypto/petrol"); + createAndUploadSymKey("http://localhost:8080/1.0/johndoe/storage/crypto/diesel"); +} + +function test_withCollectionList_failOnCrypto() { + _("Weave.Service.wipeServer() deletes collections given as argument and aborts if a collection delete fails."); + + let steam_coll = new FakeCollection(); + let petrol_coll = new FakeCollection(); + let diesel_coll = new FakeCollection(); + let crypto_steam = new ServerWBO('steam'); + let crypto_diesel = new ServerWBO('diesel'); + + let server = httpd_setup({ + "/1.0/johndoe/storage/keys/pubkey": (new ServerWBO('pubkey')).handler(), + "/1.0/johndoe/storage/keys/privkey": (new ServerWBO('privkey')).handler(), + "/1.0/johndoe/storage/steam": steam_coll.handler(), + "/1.0/johndoe/storage/petrol": petrol_coll.handler(), + "/1.0/johndoe/storage/diesel": diesel_coll.handler(), + "/1.0/johndoe/storage/crypto/steam": crypto_steam.handler(), + "/1.0/johndoe/storage/crypto/petrol": serviceUnavailable, + "/1.0/johndoe/storage/crypto/diesel": crypto_diesel.handler() + }); + do_test_pending(); + + try { + setUpTestFixtures(); + + _("Confirm initial environment."); + do_check_false(steam_coll.deleted); + do_check_false(petrol_coll.deleted); + do_check_false(diesel_coll.deleted); + + do_check_true(crypto_steam.payload != undefined); + do_check_true(crypto_diesel.payload != undefined); + + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam")); + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol")); + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel")); + + _("wipeServer() will happily ignore the non-existent collection, delete the 'steam' collection and abort after an receiving an error on the 'petrol' collection's symkey."); + let error; + try { + Weave.Service.wipeServer(["non-existent", "steam", "petrol", "diesel"]); + } catch(ex) { + error = ex; + } + _("wipeServer() threw this exception: " + error); + do_check_true(error != undefined); + + _("wipeServer stopped deleting after encountering an error with the 'petrol' collection's symkey, thus only 'steam' and 'petrol' have been deleted."); + do_check_true(steam_coll.deleted); + do_check_true(petrol_coll.deleted); + do_check_false(diesel_coll.deleted); + + do_check_true(crypto_steam.payload == undefined); + do_check_true(crypto_diesel.payload != undefined); + + do_check_false(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam")); + do_check_false(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol")); + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel")); + + } finally { + server.stop(do_test_finished); + Svc.Prefs.resetBranch(""); + CryptoMetas.clearCache(); + } +} + +function test_withCollectionList_failOnCollection() { + _("Weave.Service.wipeServer() deletes collections given as argument."); + + let steam_coll = new FakeCollection(); + let diesel_coll = new FakeCollection(); + let crypto_steam = new ServerWBO('steam'); + let crypto_petrol = new ServerWBO('petrol'); + let crypto_diesel = new ServerWBO('diesel'); + + let server = httpd_setup({ + "/1.0/johndoe/storage/keys/pubkey": (new ServerWBO('pubkey')).handler(), + "/1.0/johndoe/storage/keys/privkey": (new ServerWBO('privkey')).handler(), + "/1.0/johndoe/storage/steam": steam_coll.handler(), + "/1.0/johndoe/storage/petrol": serviceUnavailable, + "/1.0/johndoe/storage/diesel": diesel_coll.handler(), + "/1.0/johndoe/storage/crypto/steam": crypto_steam.handler(), + "/1.0/johndoe/storage/crypto/petrol": crypto_petrol.handler(), + "/1.0/johndoe/storage/crypto/diesel": crypto_diesel.handler() + }); + do_test_pending(); + + try { + setUpTestFixtures(); + + _("Confirm initial environment."); + do_check_false(steam_coll.deleted); + do_check_false(diesel_coll.deleted); + + do_check_true(crypto_steam.payload != undefined); + do_check_true(crypto_petrol.payload != undefined); + do_check_true(crypto_diesel.payload != undefined); + + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam")); + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol")); + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel")); + + _("wipeServer() will happily ignore the non-existent collection, delete the 'steam' collection and abort after an receiving an error on the 'petrol' collection."); + let error; + try { + Weave.Service.wipeServer(["non-existent", "steam", "petrol", "diesel"]); + } catch(ex) { + error = ex; + } + _("wipeServer() threw this exception: " + error); + do_check_true(error != undefined); + + _("wipeServer stopped deleting after encountering an error with the 'petrol' collection, thus only 'steam' has been deleted."); + do_check_true(steam_coll.deleted); + do_check_false(diesel_coll.deleted); + + do_check_true(crypto_steam.payload == undefined); + do_check_true(crypto_petrol.payload != undefined); + do_check_true(crypto_diesel.payload != undefined); + + do_check_false(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam")); + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol")); + do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel")); + + } finally { + server.stop(do_test_finished); + Svc.Prefs.resetBranch(""); + CryptoMetas.clearCache(); + } +} + +function run_test() { + test_withCollectionList_failOnCollection(); + test_withCollectionList_failOnCrypto(); +} diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js index c6582df865c8..b70236b85cc1 100644 --- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -863,6 +863,7 @@ function test_uploadOutgoing_failed() { "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), "/1.0/foo/storage/steam": collection.handler() }); + do_test_pending(); createAndUploadKeypair(); createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam"); @@ -898,7 +899,7 @@ function test_uploadOutgoing_failed() { do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED); } finally { - server.stop(function() {}); + server.stop(do_test_finished); Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache();