Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | this.EXPORTED_SYMBOLS = ['PasswordEngine', 'LoginRec']; |
michael@0 | 6 | |
michael@0 | 7 | const Cu = Components.utils; |
michael@0 | 8 | const Cc = Components.classes; |
michael@0 | 9 | const Ci = Components.interfaces; |
michael@0 | 10 | const Cr = Components.results; |
michael@0 | 11 | |
michael@0 | 12 | Cu.import("resource://services-sync/record.js"); |
michael@0 | 13 | Cu.import("resource://services-sync/constants.js"); |
michael@0 | 14 | Cu.import("resource://services-sync/engines.js"); |
michael@0 | 15 | Cu.import("resource://services-sync/util.js"); |
michael@0 | 16 | |
michael@0 | 17 | this.LoginRec = function LoginRec(collection, id) { |
michael@0 | 18 | CryptoWrapper.call(this, collection, id); |
michael@0 | 19 | } |
michael@0 | 20 | LoginRec.prototype = { |
michael@0 | 21 | __proto__: CryptoWrapper.prototype, |
michael@0 | 22 | _logName: "Sync.Record.Login", |
michael@0 | 23 | }; |
michael@0 | 24 | |
michael@0 | 25 | Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL", |
michael@0 | 26 | "httpRealm", "username", "password", "usernameField", "passwordField"]); |
michael@0 | 27 | |
michael@0 | 28 | |
michael@0 | 29 | this.PasswordEngine = function PasswordEngine(service) { |
michael@0 | 30 | SyncEngine.call(this, "Passwords", service); |
michael@0 | 31 | } |
michael@0 | 32 | PasswordEngine.prototype = { |
michael@0 | 33 | __proto__: SyncEngine.prototype, |
michael@0 | 34 | _storeObj: PasswordStore, |
michael@0 | 35 | _trackerObj: PasswordTracker, |
michael@0 | 36 | _recordObj: LoginRec, |
michael@0 | 37 | applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE, |
michael@0 | 38 | |
michael@0 | 39 | get isAllowed() { |
michael@0 | 40 | return Cc["@mozilla.org/weave/service;1"] |
michael@0 | 41 | .getService(Ci.nsISupports) |
michael@0 | 42 | .wrappedJSObject |
michael@0 | 43 | .allowPasswordsEngine; |
michael@0 | 44 | }, |
michael@0 | 45 | |
michael@0 | 46 | get enabled() { |
michael@0 | 47 | // If we are disabled due to !isAllowed(), we must take care to ensure the |
michael@0 | 48 | // engine has actually had the enabled setter called which reflects this state. |
michael@0 | 49 | let prefVal = SyncEngine.prototype.__lookupGetter__("enabled").call(this); |
michael@0 | 50 | let newVal = this.isAllowed && prefVal; |
michael@0 | 51 | if (newVal != prefVal) { |
michael@0 | 52 | this.enabled = newVal; |
michael@0 | 53 | } |
michael@0 | 54 | return newVal; |
michael@0 | 55 | }, |
michael@0 | 56 | |
michael@0 | 57 | set enabled(val) { |
michael@0 | 58 | SyncEngine.prototype.__lookupSetter__("enabled").call(this, this.isAllowed && val); |
michael@0 | 59 | }, |
michael@0 | 60 | |
michael@0 | 61 | _syncFinish: function _syncFinish() { |
michael@0 | 62 | SyncEngine.prototype._syncFinish.call(this); |
michael@0 | 63 | |
michael@0 | 64 | // Delete the weave credentials from the server once |
michael@0 | 65 | if (!Svc.Prefs.get("deletePwdFxA", false)) { |
michael@0 | 66 | try { |
michael@0 | 67 | let ids = []; |
michael@0 | 68 | for (let host of Utils.getSyncCredentialsHosts()) { |
michael@0 | 69 | for (let info of Services.logins.findLogins({}, host, "", "")) { |
michael@0 | 70 | ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid); |
michael@0 | 71 | } |
michael@0 | 72 | } |
michael@0 | 73 | if (ids.length) { |
michael@0 | 74 | let coll = new Collection(this.engineURL, null, this.service); |
michael@0 | 75 | coll.ids = ids; |
michael@0 | 76 | let ret = coll.delete(); |
michael@0 | 77 | this._log.debug("Delete result: " + ret); |
michael@0 | 78 | if (!ret.success && ret.status != 400) { |
michael@0 | 79 | // A non-400 failure means try again next time. |
michael@0 | 80 | return; |
michael@0 | 81 | } |
michael@0 | 82 | } else { |
michael@0 | 83 | this._log.debug("Didn't find any passwords to delete"); |
michael@0 | 84 | } |
michael@0 | 85 | // If there were no ids to delete, or we succeeded, or got a 400, |
michael@0 | 86 | // record success. |
michael@0 | 87 | Svc.Prefs.set("deletePwdFxA", true); |
michael@0 | 88 | Svc.Prefs.reset("deletePwd"); // The old prefname we previously used. |
michael@0 | 89 | } |
michael@0 | 90 | catch(ex) { |
michael@0 | 91 | this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex)); |
michael@0 | 92 | } |
michael@0 | 93 | } |
michael@0 | 94 | }, |
michael@0 | 95 | |
michael@0 | 96 | _findDupe: function _findDupe(item) { |
michael@0 | 97 | let login = this._store._nsLoginInfoFromRecord(item); |
michael@0 | 98 | if (!login) |
michael@0 | 99 | return; |
michael@0 | 100 | |
michael@0 | 101 | let logins = Services.logins.findLogins( |
michael@0 | 102 | {}, login.hostname, login.formSubmitURL, login.httpRealm); |
michael@0 | 103 | this._store._sleep(0); // Yield back to main thread after synchronous operation. |
michael@0 | 104 | |
michael@0 | 105 | // Look for existing logins that match the hostname but ignore the password |
michael@0 | 106 | for each (let local in logins) |
michael@0 | 107 | if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) |
michael@0 | 108 | return local.guid; |
michael@0 | 109 | } |
michael@0 | 110 | }; |
michael@0 | 111 | |
michael@0 | 112 | function PasswordStore(name, engine) { |
michael@0 | 113 | Store.call(this, name, engine); |
michael@0 | 114 | this._nsLoginInfo = new Components.Constructor( |
michael@0 | 115 | "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); |
michael@0 | 116 | |
michael@0 | 117 | XPCOMUtils.defineLazyGetter(this, "DBConnection", function() { |
michael@0 | 118 | return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 119 | .getInterface(Ci.mozIStorageConnection); |
michael@0 | 120 | }); |
michael@0 | 121 | } |
michael@0 | 122 | PasswordStore.prototype = { |
michael@0 | 123 | __proto__: Store.prototype, |
michael@0 | 124 | |
michael@0 | 125 | _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { |
michael@0 | 126 | if (record.formSubmitURL && |
michael@0 | 127 | record.httpRealm) { |
michael@0 | 128 | this._log.warn("Record " + record.id + |
michael@0 | 129 | " has both formSubmitURL and httpRealm. Skipping."); |
michael@0 | 130 | return null; |
michael@0 | 131 | } |
michael@0 | 132 | |
michael@0 | 133 | // Passing in "undefined" results in an empty string, which later |
michael@0 | 134 | // counts as a value. Explicitly `|| null` these fields according to JS |
michael@0 | 135 | // truthiness. Records with empty strings or null will be unmolested. |
michael@0 | 136 | function nullUndefined(x) (x == undefined) ? null : x; |
michael@0 | 137 | let info = new this._nsLoginInfo(record.hostname, |
michael@0 | 138 | nullUndefined(record.formSubmitURL), |
michael@0 | 139 | nullUndefined(record.httpRealm), |
michael@0 | 140 | record.username, |
michael@0 | 141 | record.password, |
michael@0 | 142 | record.usernameField, |
michael@0 | 143 | record.passwordField); |
michael@0 | 144 | info.QueryInterface(Ci.nsILoginMetaInfo); |
michael@0 | 145 | info.guid = record.id; |
michael@0 | 146 | return info; |
michael@0 | 147 | }, |
michael@0 | 148 | |
michael@0 | 149 | _getLoginFromGUID: function PasswordStore__getLoginFromGUID(id) { |
michael@0 | 150 | let prop = Cc["@mozilla.org/hash-property-bag;1"]. |
michael@0 | 151 | createInstance(Ci.nsIWritablePropertyBag2); |
michael@0 | 152 | prop.setPropertyAsAUTF8String("guid", id); |
michael@0 | 153 | |
michael@0 | 154 | let logins = Services.logins.searchLogins({}, prop); |
michael@0 | 155 | this._sleep(0); // Yield back to main thread after synchronous operation. |
michael@0 | 156 | if (logins.length > 0) { |
michael@0 | 157 | this._log.trace(logins.length + " items matching " + id + " found."); |
michael@0 | 158 | return logins[0]; |
michael@0 | 159 | } else { |
michael@0 | 160 | this._log.trace("No items matching " + id + " found. Ignoring"); |
michael@0 | 161 | } |
michael@0 | 162 | return null; |
michael@0 | 163 | }, |
michael@0 | 164 | |
michael@0 | 165 | applyIncomingBatch: function applyIncomingBatch(records) { |
michael@0 | 166 | if (!this.DBConnection) { |
michael@0 | 167 | return Store.prototype.applyIncomingBatch.call(this, records); |
michael@0 | 168 | } |
michael@0 | 169 | |
michael@0 | 170 | return Utils.runInTransaction(this.DBConnection, function() { |
michael@0 | 171 | return Store.prototype.applyIncomingBatch.call(this, records); |
michael@0 | 172 | }, this); |
michael@0 | 173 | }, |
michael@0 | 174 | |
michael@0 | 175 | applyIncoming: function applyIncoming(record) { |
michael@0 | 176 | Store.prototype.applyIncoming.call(this, record); |
michael@0 | 177 | this._sleep(0); // Yield back to main thread after synchronous operation. |
michael@0 | 178 | }, |
michael@0 | 179 | |
michael@0 | 180 | getAllIDs: function PasswordStore__getAllIDs() { |
michael@0 | 181 | let items = {}; |
michael@0 | 182 | let logins = Services.logins.getAllLogins({}); |
michael@0 | 183 | |
michael@0 | 184 | for (let i = 0; i < logins.length; i++) { |
michael@0 | 185 | // Skip over Weave password/passphrase entries |
michael@0 | 186 | let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); |
michael@0 | 187 | if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) { |
michael@0 | 188 | continue; |
michael@0 | 189 | } |
michael@0 | 190 | |
michael@0 | 191 | items[metaInfo.guid] = metaInfo; |
michael@0 | 192 | } |
michael@0 | 193 | |
michael@0 | 194 | return items; |
michael@0 | 195 | }, |
michael@0 | 196 | |
michael@0 | 197 | changeItemID: function PasswordStore__changeItemID(oldID, newID) { |
michael@0 | 198 | this._log.trace("Changing item ID: " + oldID + " to " + newID); |
michael@0 | 199 | |
michael@0 | 200 | let oldLogin = this._getLoginFromGUID(oldID); |
michael@0 | 201 | if (!oldLogin) { |
michael@0 | 202 | this._log.trace("Can't change item ID: item doesn't exist"); |
michael@0 | 203 | return; |
michael@0 | 204 | } |
michael@0 | 205 | if (this._getLoginFromGUID(newID)) { |
michael@0 | 206 | this._log.trace("Can't change item ID: new ID already in use"); |
michael@0 | 207 | return; |
michael@0 | 208 | } |
michael@0 | 209 | |
michael@0 | 210 | let prop = Cc["@mozilla.org/hash-property-bag;1"]. |
michael@0 | 211 | createInstance(Ci.nsIWritablePropertyBag2); |
michael@0 | 212 | prop.setPropertyAsAUTF8String("guid", newID); |
michael@0 | 213 | |
michael@0 | 214 | Services.logins.modifyLogin(oldLogin, prop); |
michael@0 | 215 | }, |
michael@0 | 216 | |
michael@0 | 217 | itemExists: function PasswordStore__itemExists(id) { |
michael@0 | 218 | if (this._getLoginFromGUID(id)) |
michael@0 | 219 | return true; |
michael@0 | 220 | return false; |
michael@0 | 221 | }, |
michael@0 | 222 | |
michael@0 | 223 | createRecord: function createRecord(id, collection) { |
michael@0 | 224 | let record = new LoginRec(collection, id); |
michael@0 | 225 | let login = this._getLoginFromGUID(id); |
michael@0 | 226 | |
michael@0 | 227 | if (login) { |
michael@0 | 228 | record.hostname = login.hostname; |
michael@0 | 229 | record.formSubmitURL = login.formSubmitURL; |
michael@0 | 230 | record.httpRealm = login.httpRealm; |
michael@0 | 231 | record.username = login.username; |
michael@0 | 232 | record.password = login.password; |
michael@0 | 233 | record.usernameField = login.usernameField; |
michael@0 | 234 | record.passwordField = login.passwordField; |
michael@0 | 235 | } |
michael@0 | 236 | else |
michael@0 | 237 | record.deleted = true; |
michael@0 | 238 | return record; |
michael@0 | 239 | }, |
michael@0 | 240 | |
michael@0 | 241 | create: function PasswordStore__create(record) { |
michael@0 | 242 | let login = this._nsLoginInfoFromRecord(record); |
michael@0 | 243 | if (!login) |
michael@0 | 244 | return; |
michael@0 | 245 | this._log.debug("Adding login for " + record.hostname); |
michael@0 | 246 | this._log.trace("httpRealm: " + JSON.stringify(login.httpRealm) + "; " + |
michael@0 | 247 | "formSubmitURL: " + JSON.stringify(login.formSubmitURL)); |
michael@0 | 248 | try { |
michael@0 | 249 | Services.logins.addLogin(login); |
michael@0 | 250 | } catch(ex) { |
michael@0 | 251 | this._log.debug("Adding record " + record.id + |
michael@0 | 252 | " resulted in exception " + Utils.exceptionStr(ex)); |
michael@0 | 253 | } |
michael@0 | 254 | }, |
michael@0 | 255 | |
michael@0 | 256 | remove: function PasswordStore__remove(record) { |
michael@0 | 257 | this._log.trace("Removing login " + record.id); |
michael@0 | 258 | |
michael@0 | 259 | let loginItem = this._getLoginFromGUID(record.id); |
michael@0 | 260 | if (!loginItem) { |
michael@0 | 261 | this._log.trace("Asked to remove record that doesn't exist, ignoring"); |
michael@0 | 262 | return; |
michael@0 | 263 | } |
michael@0 | 264 | |
michael@0 | 265 | Services.logins.removeLogin(loginItem); |
michael@0 | 266 | }, |
michael@0 | 267 | |
michael@0 | 268 | update: function PasswordStore__update(record) { |
michael@0 | 269 | let loginItem = this._getLoginFromGUID(record.id); |
michael@0 | 270 | if (!loginItem) { |
michael@0 | 271 | this._log.debug("Skipping update for unknown item: " + record.hostname); |
michael@0 | 272 | return; |
michael@0 | 273 | } |
michael@0 | 274 | |
michael@0 | 275 | this._log.debug("Updating " + record.hostname); |
michael@0 | 276 | let newinfo = this._nsLoginInfoFromRecord(record); |
michael@0 | 277 | if (!newinfo) |
michael@0 | 278 | return; |
michael@0 | 279 | try { |
michael@0 | 280 | Services.logins.modifyLogin(loginItem, newinfo); |
michael@0 | 281 | } catch(ex) { |
michael@0 | 282 | this._log.debug("Modifying record " + record.id + |
michael@0 | 283 | " resulted in exception " + Utils.exceptionStr(ex) + |
michael@0 | 284 | ". Not modifying."); |
michael@0 | 285 | } |
michael@0 | 286 | }, |
michael@0 | 287 | |
michael@0 | 288 | wipe: function PasswordStore_wipe() { |
michael@0 | 289 | Services.logins.removeAllLogins(); |
michael@0 | 290 | } |
michael@0 | 291 | }; |
michael@0 | 292 | |
michael@0 | 293 | function PasswordTracker(name, engine) { |
michael@0 | 294 | Tracker.call(this, name, engine); |
michael@0 | 295 | Svc.Obs.add("weave:engine:start-tracking", this); |
michael@0 | 296 | Svc.Obs.add("weave:engine:stop-tracking", this); |
michael@0 | 297 | } |
michael@0 | 298 | PasswordTracker.prototype = { |
michael@0 | 299 | __proto__: Tracker.prototype, |
michael@0 | 300 | |
michael@0 | 301 | startTracking: function() { |
michael@0 | 302 | Svc.Obs.add("passwordmgr-storage-changed", this); |
michael@0 | 303 | }, |
michael@0 | 304 | |
michael@0 | 305 | stopTracking: function() { |
michael@0 | 306 | Svc.Obs.remove("passwordmgr-storage-changed", this); |
michael@0 | 307 | }, |
michael@0 | 308 | |
michael@0 | 309 | observe: function(subject, topic, data) { |
michael@0 | 310 | Tracker.prototype.observe.call(this, subject, topic, data); |
michael@0 | 311 | |
michael@0 | 312 | if (this.ignoreAll) { |
michael@0 | 313 | return; |
michael@0 | 314 | } |
michael@0 | 315 | |
michael@0 | 316 | // A single add, remove or change or removing all items |
michael@0 | 317 | // will trigger a sync for MULTI_DEVICE. |
michael@0 | 318 | switch (data) { |
michael@0 | 319 | case "modifyLogin": |
michael@0 | 320 | subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo); |
michael@0 | 321 | // fallthrough |
michael@0 | 322 | case "addLogin": |
michael@0 | 323 | case "removeLogin": |
michael@0 | 324 | // Skip over Weave password/passphrase changes. |
michael@0 | 325 | subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo); |
michael@0 | 326 | if (Utils.getSyncCredentialsHosts().has(subject.hostname)) { |
michael@0 | 327 | break; |
michael@0 | 328 | } |
michael@0 | 329 | |
michael@0 | 330 | this.score += SCORE_INCREMENT_XLARGE; |
michael@0 | 331 | this._log.trace(data + ": " + subject.guid); |
michael@0 | 332 | this.addChangedID(subject.guid); |
michael@0 | 333 | break; |
michael@0 | 334 | case "removeAllLogins": |
michael@0 | 335 | this._log.trace(data); |
michael@0 | 336 | this.score += SCORE_INCREMENT_XLARGE; |
michael@0 | 337 | break; |
michael@0 | 338 | } |
michael@0 | 339 | } |
michael@0 | 340 | }; |