|
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 |
|
6 const Cc = Components.classes; |
|
7 const Ci = Components.interfaces; |
|
8 const Cr = Components.results; |
|
9 |
|
10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
11 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
12 |
|
13 /* ==================== LoginManagerPrompter ==================== */ |
|
14 /* |
|
15 * LoginManagerPrompter |
|
16 * |
|
17 * Implements interfaces for prompting the user to enter/save/change auth info. |
|
18 * |
|
19 * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins |
|
20 * found in HTML forms. |
|
21 */ |
|
22 function LoginManagerPrompter() { |
|
23 } |
|
24 |
|
25 LoginManagerPrompter.prototype = { |
|
26 |
|
27 classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"), |
|
28 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]), |
|
29 |
|
30 _factory : null, |
|
31 _window : null, |
|
32 _debug : false, // mirrors signon.debug |
|
33 |
|
34 __pwmgr : null, // Password Manager service |
|
35 get _pwmgr() { |
|
36 if (!this.__pwmgr) |
|
37 this.__pwmgr = Cc["@mozilla.org/login-manager;1"]. |
|
38 getService(Ci.nsILoginManager); |
|
39 return this.__pwmgr; |
|
40 }, |
|
41 |
|
42 __promptService : null, // Prompt service for user interaction |
|
43 get _promptService() { |
|
44 if (!this.__promptService) |
|
45 this.__promptService = |
|
46 Cc["@mozilla.org/embedcomp/prompt-service;1"]. |
|
47 getService(Ci.nsIPromptService2); |
|
48 return this.__promptService; |
|
49 }, |
|
50 |
|
51 __strBundle : null, // String bundle for L10N |
|
52 get _strBundle() { |
|
53 if (!this.__strBundle) { |
|
54 var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. |
|
55 getService(Ci.nsIStringBundleService); |
|
56 this.__strBundle = bunService.createBundle( |
|
57 "chrome://browser/locale/passwordmgr.properties"); |
|
58 if (!this.__strBundle) |
|
59 throw "String bundle for Login Manager not present!"; |
|
60 } |
|
61 |
|
62 return this.__strBundle; |
|
63 }, |
|
64 |
|
65 __brandBundle : null, // String bundle for L10N |
|
66 get _brandBundle() { |
|
67 if (!this.__brandBundle) { |
|
68 var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. |
|
69 getService(Ci.nsIStringBundleService); |
|
70 this.__brandBundle = bunService.createBundle( |
|
71 "chrome://branding/locale/brand.properties"); |
|
72 if (!this.__brandBundle) |
|
73 throw "Branding string bundle not present!"; |
|
74 } |
|
75 |
|
76 return this.__brandBundle; |
|
77 }, |
|
78 |
|
79 |
|
80 __ellipsis : null, |
|
81 get _ellipsis() { |
|
82 if (!this.__ellipsis) { |
|
83 this.__ellipsis = "\u2026"; |
|
84 try { |
|
85 this.__ellipsis = Services.prefs.getComplexValue( |
|
86 "intl.ellipsis", Ci.nsIPrefLocalizedString).data; |
|
87 } catch (e) { } |
|
88 } |
|
89 return this.__ellipsis; |
|
90 }, |
|
91 |
|
92 |
|
93 /* |
|
94 * log |
|
95 * |
|
96 * Internal function for logging debug messages to the Error Console window. |
|
97 */ |
|
98 log : function (message) { |
|
99 if (!this._debug) |
|
100 return; |
|
101 |
|
102 dump("Pwmgr Prompter: " + message + "\n"); |
|
103 Services.console.logStringMessage("Pwmgr Prompter: " + message); |
|
104 }, |
|
105 |
|
106 |
|
107 /* ---------- nsILoginManagerPrompter prompts ---------- */ |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 /* |
|
113 * init |
|
114 * |
|
115 */ |
|
116 init : function (aWindow, aFactory) { |
|
117 this._window = aWindow; |
|
118 this._factory = aFactory || null; |
|
119 |
|
120 var prefBranch = Services.prefs.getBranch("signon."); |
|
121 this._debug = prefBranch.getBoolPref("debug"); |
|
122 this.log("===== initialized ====="); |
|
123 }, |
|
124 |
|
125 |
|
126 /* |
|
127 * promptToSavePassword |
|
128 * |
|
129 */ |
|
130 promptToSavePassword : function (aLogin) { |
|
131 var notifyBox = this._getNotifyBox(); |
|
132 if (notifyBox) |
|
133 this._showSaveLoginNotification(notifyBox, aLogin); |
|
134 }, |
|
135 |
|
136 |
|
137 /* |
|
138 * _showLoginNotification |
|
139 * |
|
140 * Displays a notification bar. |
|
141 * |
|
142 */ |
|
143 _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) { |
|
144 var oldBar = aNotifyBox.getNotificationWithValue(aName); |
|
145 const priority = aNotifyBox.PRIORITY_INFO_MEDIUM; |
|
146 |
|
147 this.log("Adding new " + aName + " notification bar"); |
|
148 var newBar = aNotifyBox.appendNotification( |
|
149 aText, aName, |
|
150 "chrome://browser/skin/images/infobar-key.png", |
|
151 priority, aButtons); |
|
152 |
|
153 // The page we're going to hasn't loaded yet, so we want to persist |
|
154 // across the first location change. |
|
155 newBar.persistence++; |
|
156 |
|
157 // Sites like Gmail perform a funky redirect dance before you end up |
|
158 // at the post-authentication page. I don't see a good way to |
|
159 // heuristically determine when to ignore such location changes, so |
|
160 // we'll try ignoring location changes based on a time interval. |
|
161 newBar.timeout = Date.now() + 20000; // 20 seconds |
|
162 |
|
163 if (oldBar) { |
|
164 this.log("(...and removing old " + aName + " notification bar)"); |
|
165 aNotifyBox.removeNotification(oldBar); |
|
166 } |
|
167 }, |
|
168 |
|
169 |
|
170 /* |
|
171 * _showSaveLoginNotification |
|
172 * |
|
173 * Displays a notification bar (rather than a popup), to allow the user to |
|
174 * save the specified login. This allows the user to see the results of |
|
175 * their login, and only save a login which they know worked. |
|
176 * |
|
177 */ |
|
178 _showSaveLoginNotification : function (aNotifyBox, aLogin) { |
|
179 // Ugh. We can't use the strings from the popup window, because they |
|
180 // have the access key marked in the string (eg "Mo&zilla"), along |
|
181 // with some weird rules for handling access keys that do not occur |
|
182 // in the string, for L10N. See commonDialog.js's setLabelForNode(). |
|
183 var neverButtonText = |
|
184 this._getLocalizedString("notifyBarNotForThisSiteButtonText"); |
|
185 var neverButtonAccessKey = |
|
186 this._getLocalizedString("notifyBarNotForThisSiteButtonAccessKey"); |
|
187 var rememberButtonText = |
|
188 this._getLocalizedString("notifyBarRememberPasswordButtonText"); |
|
189 var rememberButtonAccessKey = |
|
190 this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey"); |
|
191 |
|
192 var brandShortName = |
|
193 this._brandBundle.GetStringFromName("brandShortName"); |
|
194 var displayHost = this._getShortDisplayHost(aLogin.hostname); |
|
195 var notificationText; |
|
196 if (aLogin.username) { |
|
197 var displayUser = this._sanitizeUsername(aLogin.username); |
|
198 notificationText = this._getLocalizedString( |
|
199 "saveLoginText", |
|
200 [brandShortName, displayUser, displayHost]); |
|
201 } else { |
|
202 notificationText = this._getLocalizedString( |
|
203 "saveLoginTextNoUsername", |
|
204 [brandShortName, displayHost]); |
|
205 } |
|
206 |
|
207 // The callbacks in |buttons| have a closure to access the variables |
|
208 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr |
|
209 // without a getService() call. |
|
210 var pwmgr = this._pwmgr; |
|
211 |
|
212 |
|
213 var buttons = [ |
|
214 // "Remember" button |
|
215 { |
|
216 label: rememberButtonText, |
|
217 accessKey: rememberButtonAccessKey, |
|
218 popup: null, |
|
219 callback: function(aNotificationBar, aButton) { |
|
220 pwmgr.addLogin(aLogin); |
|
221 } |
|
222 }, |
|
223 |
|
224 // "Never for this site" button |
|
225 { |
|
226 label: neverButtonText, |
|
227 accessKey: neverButtonAccessKey, |
|
228 popup: null, |
|
229 callback: function(aNotificationBar, aButton) { |
|
230 pwmgr.setLoginSavingEnabled(aLogin.hostname, false); |
|
231 } |
|
232 } |
|
233 ]; |
|
234 |
|
235 this._showLoginNotification(aNotifyBox, "password-save", |
|
236 notificationText, buttons); |
|
237 }, |
|
238 |
|
239 |
|
240 /* |
|
241 * promptToChangePassword |
|
242 * |
|
243 * Called when we think we detect a password change for an existing |
|
244 * login, when the form being submitted contains multiple password |
|
245 * fields. |
|
246 * |
|
247 */ |
|
248 promptToChangePassword : function (aOldLogin, aNewLogin) { |
|
249 var notifyBox = this._getNotifyBox(); |
|
250 if (notifyBox) |
|
251 this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin.password); |
|
252 }, |
|
253 |
|
254 /* |
|
255 * _showChangeLoginNotification |
|
256 * |
|
257 * Shows the Change Password notification bar. |
|
258 * |
|
259 */ |
|
260 _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewPassword) { |
|
261 var notificationText; |
|
262 if (aOldLogin.username) |
|
263 notificationText = this._getLocalizedString( |
|
264 "passwordChangeText", |
|
265 [aOldLogin.username]); |
|
266 else |
|
267 notificationText = this._getLocalizedString( |
|
268 "passwordChangeTextNoUser"); |
|
269 |
|
270 var changeButtonText = |
|
271 this._getLocalizedString("notifyBarChangeButtonText"); |
|
272 var changeButtonAccessKey = |
|
273 this._getLocalizedString("notifyBarChangeButtonAccessKey"); |
|
274 var dontChangeButtonText = |
|
275 this._getLocalizedString("notifyBarDontChangeButtonText2"); |
|
276 var dontChangeButtonAccessKey = |
|
277 this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); |
|
278 |
|
279 // The callbacks in |buttons| have a closure to access the variables |
|
280 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr |
|
281 // without a getService() call. |
|
282 var self = this; |
|
283 |
|
284 var buttons = [ |
|
285 // "Yes" button |
|
286 { |
|
287 label: changeButtonText, |
|
288 accessKey: changeButtonAccessKey, |
|
289 popup: null, |
|
290 callback: function(aNotificationBar, aButton) { |
|
291 self._updateLogin(aOldLogin, aNewPassword); |
|
292 } |
|
293 }, |
|
294 |
|
295 // "No" button |
|
296 { |
|
297 label: dontChangeButtonText, |
|
298 accessKey: dontChangeButtonAccessKey, |
|
299 popup: null, |
|
300 callback: function(aNotificationBar, aButton) { |
|
301 // do nothing |
|
302 } |
|
303 } |
|
304 ]; |
|
305 |
|
306 this._showLoginNotification(aNotifyBox, "password-change", |
|
307 notificationText, buttons); |
|
308 }, |
|
309 |
|
310 /* |
|
311 * promptToChangePasswordWithUsernames |
|
312 * |
|
313 * Called when we detect a password change in a form submission, but we |
|
314 * don't know which existing login (username) it's for. Asks the user |
|
315 * to select a username and confirm the password change. |
|
316 * |
|
317 * Note: The caller doesn't know the username for aNewLogin, so this |
|
318 * function fills in .username and .usernameField with the values |
|
319 * from the login selected by the user. |
|
320 * |
|
321 * Note; XPCOM stupidity: |count| is just |logins.length|. |
|
322 */ |
|
323 promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) { |
|
324 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; |
|
325 |
|
326 var usernames = logins.map(function (l) l.username); |
|
327 var dialogText = this._getLocalizedString("userSelectText"); |
|
328 var dialogTitle = this._getLocalizedString("passwordChangeTitle"); |
|
329 var selectedIndex = { value: null }; |
|
330 |
|
331 // If user selects ok, outparam.value is set to the index |
|
332 // of the selected username. |
|
333 var ok = this._promptService.select(null, |
|
334 dialogTitle, dialogText, |
|
335 usernames.length, usernames, |
|
336 selectedIndex); |
|
337 if (ok) { |
|
338 // Now that we know which login to use, modify its password. |
|
339 var selectedLogin = logins[selectedIndex.value]; |
|
340 this.log("Updating password for user " + selectedLogin.username); |
|
341 this._updateLogin(selectedLogin, aNewLogin.password); |
|
342 } |
|
343 }, |
|
344 |
|
345 |
|
346 /* ---------- Internal Methods ---------- */ |
|
347 |
|
348 /* |
|
349 * _updateLogin |
|
350 */ |
|
351 _updateLogin : function (login, newPassword) { |
|
352 var now = Date.now(); |
|
353 var propBag = Cc["@mozilla.org/hash-property-bag;1"]. |
|
354 createInstance(Ci.nsIWritablePropertyBag); |
|
355 if (newPassword) { |
|
356 propBag.setProperty("password", newPassword); |
|
357 // Explicitly set the password change time here (even though it would |
|
358 // be changed automatically), to ensure that it's exactly the same |
|
359 // value as timeLastUsed. |
|
360 propBag.setProperty("timePasswordChanged", now); |
|
361 } |
|
362 propBag.setProperty("timeLastUsed", now); |
|
363 propBag.setProperty("timesUsedIncrement", 1); |
|
364 this._pwmgr.modifyLogin(login, propBag); |
|
365 }, |
|
366 |
|
367 /* |
|
368 * _getNotifyWindow |
|
369 */ |
|
370 _getNotifyWindow: function () { |
|
371 try { |
|
372 // Get topmost window, in case we're in a frame. |
|
373 var notifyWin = this._window.top; |
|
374 |
|
375 // Some sites pop up a temporary login window, when disappears |
|
376 // upon submission of credentials. We want to put the notification |
|
377 // bar in the opener window if this seems to be happening. |
|
378 if (notifyWin.opener) { |
|
379 var chromeDoc = this._getChromeWindow(notifyWin). |
|
380 document.documentElement; |
|
381 var webnav = notifyWin. |
|
382 QueryInterface(Ci.nsIInterfaceRequestor). |
|
383 getInterface(Ci.nsIWebNavigation); |
|
384 |
|
385 // Check to see if the current window was opened with chrome |
|
386 // disabled, and if so use the opener window. But if the window |
|
387 // has been used to visit other pages (ie, has a history), |
|
388 // assume it'll stick around and *don't* use the opener. |
|
389 if (chromeDoc.getAttribute("chromehidden") && |
|
390 webnav.sessionHistory.count == 1) { |
|
391 this.log("Using opener window for notification bar."); |
|
392 notifyWin = notifyWin.opener; |
|
393 } |
|
394 } |
|
395 |
|
396 return notifyWin; |
|
397 |
|
398 } catch (e) { |
|
399 // If any errors happen, just assume no notification box. |
|
400 this.log("Unable to get notify window"); |
|
401 return null; |
|
402 } |
|
403 }, |
|
404 |
|
405 /* |
|
406 * _getChromeWindow |
|
407 * |
|
408 * Given a content DOM window, returns the chrome window it's in. |
|
409 */ |
|
410 _getChromeWindow: function (aWindow) { |
|
411 var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
412 .getInterface(Ci.nsIWebNavigation) |
|
413 .QueryInterface(Ci.nsIDocShell) |
|
414 .chromeEventHandler.ownerDocument.defaultView; |
|
415 return chromeWin; |
|
416 }, |
|
417 |
|
418 /* |
|
419 * _getNotifyBox |
|
420 * |
|
421 * Returns the notification box to this prompter, or null if there isn't |
|
422 * a notification box available. |
|
423 */ |
|
424 _getNotifyBox : function () { |
|
425 let notifyBox = null; |
|
426 |
|
427 try { |
|
428 let notifyWin = this._getNotifyWindow(); |
|
429 let windowID = notifyWin.QueryInterface(Ci.nsIInterfaceRequestor) |
|
430 .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; |
|
431 |
|
432 // Get the chrome window for the content window we're using. |
|
433 // .wrappedJSObject needed here -- see bug 422974 comment 5. |
|
434 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; |
|
435 let browser = chromeWin.Browser.getBrowserForWindowId(windowID); |
|
436 |
|
437 notifyBox = chromeWin.getNotificationBox(browser); |
|
438 } catch (e) { |
|
439 Cu.reportError(e); |
|
440 } |
|
441 |
|
442 return notifyBox; |
|
443 }, |
|
444 |
|
445 /* |
|
446 * _getLocalizedString |
|
447 * |
|
448 * Can be called as: |
|
449 * _getLocalizedString("key1"); |
|
450 * _getLocalizedString("key2", ["arg1"]); |
|
451 * _getLocalizedString("key3", ["arg1", "arg2"]); |
|
452 * (etc) |
|
453 * |
|
454 * Returns the localized string for the specified key, |
|
455 * formatted if required. |
|
456 * |
|
457 */ |
|
458 _getLocalizedString : function (key, formatArgs) { |
|
459 if (formatArgs) |
|
460 return this._strBundle.formatStringFromName( |
|
461 key, formatArgs, formatArgs.length); |
|
462 else |
|
463 return this._strBundle.GetStringFromName(key); |
|
464 }, |
|
465 |
|
466 |
|
467 /* |
|
468 * _sanitizeUsername |
|
469 * |
|
470 * Sanitizes the specified username, by stripping quotes and truncating if |
|
471 * it's too long. This helps prevent an evil site from messing with the |
|
472 * "save password?" prompt too much. |
|
473 */ |
|
474 _sanitizeUsername : function (username) { |
|
475 if (username.length > 30) { |
|
476 username = username.substring(0, 30); |
|
477 username += this._ellipsis; |
|
478 } |
|
479 return username.replace(/['"]/g, ""); |
|
480 }, |
|
481 |
|
482 |
|
483 /* |
|
484 * _getShortDisplayHost |
|
485 * |
|
486 * Converts a login's hostname field (a URL) to a short string for |
|
487 * prompting purposes. Eg, "http://foo.com" --> "foo.com", or |
|
488 * "ftp://www.site.co.uk" --> "site.co.uk". |
|
489 */ |
|
490 _getShortDisplayHost: function (aURIString) { |
|
491 var displayHost; |
|
492 |
|
493 var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. |
|
494 getService(Ci.nsIEffectiveTLDService); |
|
495 var idnService = Cc["@mozilla.org/network/idn-service;1"]. |
|
496 getService(Ci.nsIIDNService); |
|
497 try { |
|
498 var uri = Services.io.newURI(aURIString, null, null); |
|
499 var baseDomain = eTLDService.getBaseDomain(uri); |
|
500 displayHost = idnService.convertToDisplayIDN(baseDomain, {}); |
|
501 } catch (e) { |
|
502 this.log("_getShortDisplayHost couldn't process " + aURIString); |
|
503 } |
|
504 |
|
505 if (!displayHost) |
|
506 displayHost = aURIString; |
|
507 |
|
508 return displayHost; |
|
509 }, |
|
510 |
|
511 }; // end of LoginManagerPrompter implementation |
|
512 |
|
513 |
|
514 var component = [LoginManagerPrompter]; |
|
515 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); |
|
516 |