michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ['PasswordEngine', 'LoginRec']; michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://services-sync/record.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: michael@0: this.LoginRec = function LoginRec(collection, id) { michael@0: CryptoWrapper.call(this, collection, id); michael@0: } michael@0: LoginRec.prototype = { michael@0: __proto__: CryptoWrapper.prototype, michael@0: _logName: "Sync.Record.Login", michael@0: }; michael@0: michael@0: Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL", michael@0: "httpRealm", "username", "password", "usernameField", "passwordField"]); michael@0: michael@0: michael@0: this.PasswordEngine = function PasswordEngine(service) { michael@0: SyncEngine.call(this, "Passwords", service); michael@0: } michael@0: PasswordEngine.prototype = { michael@0: __proto__: SyncEngine.prototype, michael@0: _storeObj: PasswordStore, michael@0: _trackerObj: PasswordTracker, michael@0: _recordObj: LoginRec, michael@0: applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE, michael@0: michael@0: get isAllowed() { michael@0: return Cc["@mozilla.org/weave/service;1"] michael@0: .getService(Ci.nsISupports) michael@0: .wrappedJSObject michael@0: .allowPasswordsEngine; michael@0: }, michael@0: michael@0: get enabled() { michael@0: // If we are disabled due to !isAllowed(), we must take care to ensure the michael@0: // engine has actually had the enabled setter called which reflects this state. michael@0: let prefVal = SyncEngine.prototype.__lookupGetter__("enabled").call(this); michael@0: let newVal = this.isAllowed && prefVal; michael@0: if (newVal != prefVal) { michael@0: this.enabled = newVal; michael@0: } michael@0: return newVal; michael@0: }, michael@0: michael@0: set enabled(val) { michael@0: SyncEngine.prototype.__lookupSetter__("enabled").call(this, this.isAllowed && val); michael@0: }, michael@0: michael@0: _syncFinish: function _syncFinish() { michael@0: SyncEngine.prototype._syncFinish.call(this); michael@0: michael@0: // Delete the weave credentials from the server once michael@0: if (!Svc.Prefs.get("deletePwdFxA", false)) { michael@0: try { michael@0: let ids = []; michael@0: for (let host of Utils.getSyncCredentialsHosts()) { michael@0: for (let info of Services.logins.findLogins({}, host, "", "")) { michael@0: ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid); michael@0: } michael@0: } michael@0: if (ids.length) { michael@0: let coll = new Collection(this.engineURL, null, this.service); michael@0: coll.ids = ids; michael@0: let ret = coll.delete(); michael@0: this._log.debug("Delete result: " + ret); michael@0: if (!ret.success && ret.status != 400) { michael@0: // A non-400 failure means try again next time. michael@0: return; michael@0: } michael@0: } else { michael@0: this._log.debug("Didn't find any passwords to delete"); michael@0: } michael@0: // If there were no ids to delete, or we succeeded, or got a 400, michael@0: // record success. michael@0: Svc.Prefs.set("deletePwdFxA", true); michael@0: Svc.Prefs.reset("deletePwd"); // The old prefname we previously used. michael@0: } michael@0: catch(ex) { michael@0: this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex)); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _findDupe: function _findDupe(item) { michael@0: let login = this._store._nsLoginInfoFromRecord(item); michael@0: if (!login) michael@0: return; michael@0: michael@0: let logins = Services.logins.findLogins( michael@0: {}, login.hostname, login.formSubmitURL, login.httpRealm); michael@0: this._store._sleep(0); // Yield back to main thread after synchronous operation. michael@0: michael@0: // Look for existing logins that match the hostname but ignore the password michael@0: for each (let local in logins) michael@0: if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) michael@0: return local.guid; michael@0: } michael@0: }; michael@0: michael@0: function PasswordStore(name, engine) { michael@0: Store.call(this, name, engine); michael@0: this._nsLoginInfo = new Components.Constructor( michael@0: "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "DBConnection", function() { michael@0: return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.mozIStorageConnection); michael@0: }); michael@0: } michael@0: PasswordStore.prototype = { michael@0: __proto__: Store.prototype, michael@0: michael@0: _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { michael@0: if (record.formSubmitURL && michael@0: record.httpRealm) { michael@0: this._log.warn("Record " + record.id + michael@0: " has both formSubmitURL and httpRealm. Skipping."); michael@0: return null; michael@0: } michael@0: michael@0: // Passing in "undefined" results in an empty string, which later michael@0: // counts as a value. Explicitly `|| null` these fields according to JS michael@0: // truthiness. Records with empty strings or null will be unmolested. michael@0: function nullUndefined(x) (x == undefined) ? null : x; michael@0: let info = new this._nsLoginInfo(record.hostname, michael@0: nullUndefined(record.formSubmitURL), michael@0: nullUndefined(record.httpRealm), michael@0: record.username, michael@0: record.password, michael@0: record.usernameField, michael@0: record.passwordField); michael@0: info.QueryInterface(Ci.nsILoginMetaInfo); michael@0: info.guid = record.id; michael@0: return info; michael@0: }, michael@0: michael@0: _getLoginFromGUID: function PasswordStore__getLoginFromGUID(id) { michael@0: let prop = Cc["@mozilla.org/hash-property-bag;1"]. michael@0: createInstance(Ci.nsIWritablePropertyBag2); michael@0: prop.setPropertyAsAUTF8String("guid", id); michael@0: michael@0: let logins = Services.logins.searchLogins({}, prop); michael@0: this._sleep(0); // Yield back to main thread after synchronous operation. michael@0: if (logins.length > 0) { michael@0: this._log.trace(logins.length + " items matching " + id + " found."); michael@0: return logins[0]; michael@0: } else { michael@0: this._log.trace("No items matching " + id + " found. Ignoring"); michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: applyIncomingBatch: function applyIncomingBatch(records) { michael@0: if (!this.DBConnection) { michael@0: return Store.prototype.applyIncomingBatch.call(this, records); michael@0: } michael@0: michael@0: return Utils.runInTransaction(this.DBConnection, function() { michael@0: return Store.prototype.applyIncomingBatch.call(this, records); michael@0: }, this); michael@0: }, michael@0: michael@0: applyIncoming: function applyIncoming(record) { michael@0: Store.prototype.applyIncoming.call(this, record); michael@0: this._sleep(0); // Yield back to main thread after synchronous operation. michael@0: }, michael@0: michael@0: getAllIDs: function PasswordStore__getAllIDs() { michael@0: let items = {}; michael@0: let logins = Services.logins.getAllLogins({}); michael@0: michael@0: for (let i = 0; i < logins.length; i++) { michael@0: // Skip over Weave password/passphrase entries michael@0: let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); michael@0: if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) { michael@0: continue; michael@0: } michael@0: michael@0: items[metaInfo.guid] = metaInfo; michael@0: } michael@0: michael@0: return items; michael@0: }, michael@0: michael@0: changeItemID: function PasswordStore__changeItemID(oldID, newID) { michael@0: this._log.trace("Changing item ID: " + oldID + " to " + newID); michael@0: michael@0: let oldLogin = this._getLoginFromGUID(oldID); michael@0: if (!oldLogin) { michael@0: this._log.trace("Can't change item ID: item doesn't exist"); michael@0: return; michael@0: } michael@0: if (this._getLoginFromGUID(newID)) { michael@0: this._log.trace("Can't change item ID: new ID already in use"); michael@0: return; michael@0: } michael@0: michael@0: let prop = Cc["@mozilla.org/hash-property-bag;1"]. michael@0: createInstance(Ci.nsIWritablePropertyBag2); michael@0: prop.setPropertyAsAUTF8String("guid", newID); michael@0: michael@0: Services.logins.modifyLogin(oldLogin, prop); michael@0: }, michael@0: michael@0: itemExists: function PasswordStore__itemExists(id) { michael@0: if (this._getLoginFromGUID(id)) michael@0: return true; michael@0: return false; michael@0: }, michael@0: michael@0: createRecord: function createRecord(id, collection) { michael@0: let record = new LoginRec(collection, id); michael@0: let login = this._getLoginFromGUID(id); michael@0: michael@0: if (login) { michael@0: record.hostname = login.hostname; michael@0: record.formSubmitURL = login.formSubmitURL; michael@0: record.httpRealm = login.httpRealm; michael@0: record.username = login.username; michael@0: record.password = login.password; michael@0: record.usernameField = login.usernameField; michael@0: record.passwordField = login.passwordField; michael@0: } michael@0: else michael@0: record.deleted = true; michael@0: return record; michael@0: }, michael@0: michael@0: create: function PasswordStore__create(record) { michael@0: let login = this._nsLoginInfoFromRecord(record); michael@0: if (!login) michael@0: return; michael@0: this._log.debug("Adding login for " + record.hostname); michael@0: this._log.trace("httpRealm: " + JSON.stringify(login.httpRealm) + "; " + michael@0: "formSubmitURL: " + JSON.stringify(login.formSubmitURL)); michael@0: try { michael@0: Services.logins.addLogin(login); michael@0: } catch(ex) { michael@0: this._log.debug("Adding record " + record.id + michael@0: " resulted in exception " + Utils.exceptionStr(ex)); michael@0: } michael@0: }, michael@0: michael@0: remove: function PasswordStore__remove(record) { michael@0: this._log.trace("Removing login " + record.id); michael@0: michael@0: let loginItem = this._getLoginFromGUID(record.id); michael@0: if (!loginItem) { michael@0: this._log.trace("Asked to remove record that doesn't exist, ignoring"); michael@0: return; michael@0: } michael@0: michael@0: Services.logins.removeLogin(loginItem); michael@0: }, michael@0: michael@0: update: function PasswordStore__update(record) { michael@0: let loginItem = this._getLoginFromGUID(record.id); michael@0: if (!loginItem) { michael@0: this._log.debug("Skipping update for unknown item: " + record.hostname); michael@0: return; michael@0: } michael@0: michael@0: this._log.debug("Updating " + record.hostname); michael@0: let newinfo = this._nsLoginInfoFromRecord(record); michael@0: if (!newinfo) michael@0: return; michael@0: try { michael@0: Services.logins.modifyLogin(loginItem, newinfo); michael@0: } catch(ex) { michael@0: this._log.debug("Modifying record " + record.id + michael@0: " resulted in exception " + Utils.exceptionStr(ex) + michael@0: ". Not modifying."); michael@0: } michael@0: }, michael@0: michael@0: wipe: function PasswordStore_wipe() { michael@0: Services.logins.removeAllLogins(); michael@0: } michael@0: }; michael@0: michael@0: function PasswordTracker(name, engine) { michael@0: Tracker.call(this, name, engine); michael@0: Svc.Obs.add("weave:engine:start-tracking", this); michael@0: Svc.Obs.add("weave:engine:stop-tracking", this); michael@0: } michael@0: PasswordTracker.prototype = { michael@0: __proto__: Tracker.prototype, michael@0: michael@0: startTracking: function() { michael@0: Svc.Obs.add("passwordmgr-storage-changed", this); michael@0: }, michael@0: michael@0: stopTracking: function() { michael@0: Svc.Obs.remove("passwordmgr-storage-changed", this); michael@0: }, michael@0: michael@0: observe: function(subject, topic, data) { michael@0: Tracker.prototype.observe.call(this, subject, topic, data); michael@0: michael@0: if (this.ignoreAll) { michael@0: return; michael@0: } michael@0: michael@0: // A single add, remove or change or removing all items michael@0: // will trigger a sync for MULTI_DEVICE. michael@0: switch (data) { michael@0: case "modifyLogin": michael@0: subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo); michael@0: // fallthrough michael@0: case "addLogin": michael@0: case "removeLogin": michael@0: // Skip over Weave password/passphrase changes. michael@0: subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo); michael@0: if (Utils.getSyncCredentialsHosts().has(subject.hostname)) { michael@0: break; michael@0: } michael@0: michael@0: this.score += SCORE_INCREMENT_XLARGE; michael@0: this._log.trace(data + ": " + subject.guid); michael@0: this.addChangedID(subject.guid); michael@0: break; michael@0: case "removeAllLogins": michael@0: this._log.trace(data); michael@0: this.score += SCORE_INCREMENT_XLARGE; michael@0: break; michael@0: } michael@0: } michael@0: };