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