browser/metro/components/LoginManagerPrompter.js

changeset 2
7e26c7da4463
equal deleted inserted replaced
-1:000000000000 0:47836d47f861
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

mercurial