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
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/. */
6 const Cc = Components.classes;
7 const Ci = Components.interfaces;
8 const Cr = Components.results;
10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
11 Components.utils.import("resource://gre/modules/Services.jsm");
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 }
25 LoginManagerPrompter.prototype = {
27 classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"),
28 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]),
30 _factory : null,
31 _window : null,
32 _debug : false, // mirrors signon.debug
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 },
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 },
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://passwordmgr/locale/passwordmgr.properties");
58 if (!this.__strBundle)
59 throw "String bundle for Login Manager not present!";
60 }
62 return this.__strBundle;
63 },
66 __ellipsis : null,
67 get _ellipsis() {
68 if (!this.__ellipsis) {
69 this.__ellipsis = "\u2026";
70 try {
71 this.__ellipsis = Services.prefs.getComplexValue(
72 "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
73 } catch (e) { }
74 }
75 return this.__ellipsis;
76 },
79 /*
80 * log
81 *
82 * Internal function for logging debug messages to the Error Console window.
83 */
84 log : function (message) {
85 if (!this._debug)
86 return;
88 dump("Pwmgr Prompter: " + message + "\n");
89 Services.console.logStringMessage("Pwmgr Prompter: " + message);
90 },
93 /* ---------- nsILoginManagerPrompter prompts ---------- */
98 /*
99 * init
100 *
101 */
102 init : function (aWindow, aFactory) {
103 this._window = aWindow;
104 this._factory = aFactory || null;
106 var prefBranch = Services.prefs.getBranch("signon.");
107 this._debug = prefBranch.getBoolPref("debug");
108 this.log("===== initialized =====");
109 },
112 /*
113 * promptToSavePassword
114 *
115 */
116 promptToSavePassword : function (aLogin) {
117 this._showSaveLoginNotification(aLogin);
118 },
121 /*
122 * _showLoginNotification
123 *
124 * Displays a notification doorhanger.
125 *
126 */
127 _showLoginNotification : function (aName, aText, aButtons) {
128 this.log("Adding new " + aName + " notification bar");
129 let notifyWin = this._window.top;
130 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
131 let browser = chromeWin.BrowserApp.getBrowserForWindow(notifyWin);
132 let tabID = chromeWin.BrowserApp.getTabForBrowser(browser).id;
134 // The page we're going to hasn't loaded yet, so we want to persist
135 // across the first location change.
137 // Sites like Gmail perform a funky redirect dance before you end up
138 // at the post-authentication page. I don't see a good way to
139 // heuristically determine when to ignore such location changes, so
140 // we'll try ignoring location changes based on a time interval.
142 let options = {
143 persistWhileVisible: true,
144 timeout: Date.now() + 10000
145 }
147 var nativeWindow = this._getNativeWindow();
148 if (nativeWindow)
149 nativeWindow.doorhanger.show(aText, aName, aButtons, tabID, options);
150 },
153 /*
154 * _showSaveLoginNotification
155 *
156 * Displays a notification doorhanger (rather than a popup), to allow the user to
157 * save the specified login. This allows the user to see the results of
158 * their login, and only save a login which they know worked.
159 *
160 */
161 _showSaveLoginNotification : function (aLogin) {
162 var displayHost = this._getShortDisplayHost(aLogin.hostname);
163 var notificationText;
164 if (aLogin.username) {
165 var displayUser = this._sanitizeUsername(aLogin.username);
166 notificationText = this._getLocalizedString("savePassword", [displayUser, displayHost]);
167 } else {
168 notificationText = this._getLocalizedString("savePasswordNoUser", [displayHost]);
169 }
171 // The callbacks in |buttons| have a closure to access the variables
172 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
173 // without a getService() call.
174 var pwmgr = this._pwmgr;
176 var buttons = [
177 {
178 label: this._getLocalizedString("saveButton"),
179 callback: function() {
180 pwmgr.addLogin(aLogin);
181 }
182 },
183 {
184 label: this._getLocalizedString("dontSaveButton"),
185 callback: function() {
186 // Don't set a permanent exception
187 }
188 }
189 ];
191 this._showLoginNotification("password-save", notificationText, buttons);
192 },
194 /*
195 * promptToChangePassword
196 *
197 * Called when we think we detect a password change for an existing
198 * login, when the form being submitted contains multiple password
199 * fields.
200 *
201 */
202 promptToChangePassword : function (aOldLogin, aNewLogin) {
203 this._showChangeLoginNotification(aOldLogin, aNewLogin.password);
204 },
206 /*
207 * _showChangeLoginNotification
208 *
209 * Shows the Change Password notification doorhanger.
210 *
211 */
212 _showChangeLoginNotification : function (aOldLogin, aNewPassword) {
213 var notificationText;
214 if (aOldLogin.username) {
215 let displayUser = this._sanitizeUsername(aOldLogin.username);
216 notificationText = this._getLocalizedString("updatePassword", [displayUser]);
217 } else {
218 notificationText = this._getLocalizedString("updatePasswordNoUser");
219 }
221 // The callbacks in |buttons| have a closure to access the variables
222 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
223 // without a getService() call.
224 var self = this;
226 var buttons = [
227 {
228 label: this._getLocalizedString("updateButton"),
229 callback: function() {
230 self._updateLogin(aOldLogin, aNewPassword);
231 }
232 },
233 {
234 label: this._getLocalizedString("dontUpdateButton"),
235 callback: function() {
236 // do nothing
237 }
238 }
239 ];
241 this._showLoginNotification("password-change", notificationText, buttons);
242 },
245 /*
246 * promptToChangePasswordWithUsernames
247 *
248 * Called when we detect a password change in a form submission, but we
249 * don't know which existing login (username) it's for. Asks the user
250 * to select a username and confirm the password change.
251 *
252 * Note: The caller doesn't know the username for aNewLogin, so this
253 * function fills in .username and .usernameField with the values
254 * from the login selected by the user.
255 *
256 * Note; XPCOM stupidity: |count| is just |logins.length|.
257 */
258 promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
259 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
261 var usernames = logins.map(function (l) l.username);
262 var dialogText = this._getLocalizedString("userSelectText");
263 var dialogTitle = this._getLocalizedString("passwordChangeTitle");
264 var selectedIndex = { value: null };
266 // If user selects ok, outparam.value is set to the index
267 // of the selected username.
268 var ok = this._promptService.select(null,
269 dialogTitle, dialogText,
270 usernames.length, usernames,
271 selectedIndex);
272 if (ok) {
273 // Now that we know which login to use, modify its password.
274 var selectedLogin = logins[selectedIndex.value];
275 this.log("Updating password for user " + selectedLogin.username);
276 this._updateLogin(selectedLogin, aNewLogin.password);
277 }
278 },
283 /* ---------- Internal Methods ---------- */
288 /*
289 * _updateLogin
290 */
291 _updateLogin : function (login, newPassword) {
292 var now = Date.now();
293 var propBag = Cc["@mozilla.org/hash-property-bag;1"].
294 createInstance(Ci.nsIWritablePropertyBag);
295 if (newPassword) {
296 propBag.setProperty("password", newPassword);
297 // Explicitly set the password change time here (even though it would
298 // be changed automatically), to ensure that it's exactly the same
299 // value as timeLastUsed.
300 propBag.setProperty("timePasswordChanged", now);
301 }
302 propBag.setProperty("timeLastUsed", now);
303 propBag.setProperty("timesUsedIncrement", 1);
304 this._pwmgr.modifyLogin(login, propBag);
305 },
307 /*
308 * _getChromeWindow
309 *
310 * Given a content DOM window, returns the chrome window it's in.
311 */
312 _getChromeWindow: function (aWindow) {
313 var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
314 .getInterface(Ci.nsIWebNavigation)
315 .QueryInterface(Ci.nsIDocShell)
316 .chromeEventHandler.ownerDocument.defaultView;
317 return chromeWin;
318 },
320 /*
321 * _getNativeWindow
322 *
323 * Returns the NativeWindow to this prompter, or null if there isn't
324 * a NativeWindow available (w/ error sent to logcat).
325 */
326 _getNativeWindow : function () {
327 let nativeWindow = null;
328 try {
329 let notifyWin = this._window.top;
330 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
331 if (chromeWin.NativeWindow) {
332 nativeWindow = chromeWin.NativeWindow;
333 } else {
334 Cu.reportError("NativeWindow not available on window");
335 }
337 } catch (e) {
338 // If any errors happen, just assume no native window helper.
339 Cu.reportError("No NativeWindow available: " + e);
340 }
341 return nativeWindow;
342 },
344 /*
345 * _getLocalizedString
346 *
347 * Can be called as:
348 * _getLocalizedString("key1");
349 * _getLocalizedString("key2", ["arg1"]);
350 * _getLocalizedString("key3", ["arg1", "arg2"]);
351 * (etc)
352 *
353 * Returns the localized string for the specified key,
354 * formatted if required.
355 *
356 */
357 _getLocalizedString : function (key, formatArgs) {
358 if (formatArgs)
359 return this._strBundle.formatStringFromName(
360 key, formatArgs, formatArgs.length);
361 else
362 return this._strBundle.GetStringFromName(key);
363 },
366 /*
367 * _sanitizeUsername
368 *
369 * Sanitizes the specified username, by stripping quotes and truncating if
370 * it's too long. This helps prevent an evil site from messing with the
371 * "save password?" prompt too much.
372 */
373 _sanitizeUsername : function (username) {
374 if (username.length > 30) {
375 username = username.substring(0, 30);
376 username += this._ellipsis;
377 }
378 return username.replace(/['"]/g, "");
379 },
382 /*
383 * _getFormattedHostname
384 *
385 * The aURI parameter may either be a string uri, or an nsIURI instance.
386 *
387 * Returns the hostname to use in a nsILoginInfo object (for example,
388 * "http://example.com").
389 */
390 _getFormattedHostname : function (aURI) {
391 var uri;
392 if (aURI instanceof Ci.nsIURI) {
393 uri = aURI;
394 } else {
395 uri = Services.io.newURI(aURI, null, null);
396 }
397 var scheme = uri.scheme;
399 var hostname = scheme + "://" + uri.host;
401 // If the URI explicitly specified a port, only include it when
402 // it's not the default. (We never want "http://foo.com:80")
403 port = uri.port;
404 if (port != -1) {
405 var handler = Services.io.getProtocolHandler(scheme);
406 if (port != handler.defaultPort)
407 hostname += ":" + port;
408 }
410 return hostname;
411 },
414 /*
415 * _getShortDisplayHost
416 *
417 * Converts a login's hostname field (a URL) to a short string for
418 * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
419 * "ftp://www.site.co.uk" --> "site.co.uk".
420 */
421 _getShortDisplayHost: function (aURIString) {
422 var displayHost;
424 var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
425 getService(Ci.nsIEffectiveTLDService);
426 var idnService = Cc["@mozilla.org/network/idn-service;1"].
427 getService(Ci.nsIIDNService);
428 try {
429 var uri = Services.io.newURI(aURIString, null, null);
430 var baseDomain = eTLDService.getBaseDomain(uri);
431 displayHost = idnService.convertToDisplayIDN(baseDomain, {});
432 } catch (e) {
433 this.log("_getShortDisplayHost couldn't process " + aURIString);
434 }
436 if (!displayHost)
437 displayHost = aURIString;
439 return displayHost;
440 },
442 }; // end of LoginManagerPrompter implementation
445 var component = [LoginManagerPrompter];
446 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);