|
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://passwordmgr/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 |
|
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 }, |
|
77 |
|
78 |
|
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; |
|
87 |
|
88 dump("Pwmgr Prompter: " + message + "\n"); |
|
89 Services.console.logStringMessage("Pwmgr Prompter: " + message); |
|
90 }, |
|
91 |
|
92 |
|
93 /* ---------- nsILoginManagerPrompter prompts ---------- */ |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 /* |
|
99 * init |
|
100 * |
|
101 */ |
|
102 init : function (aWindow, aFactory) { |
|
103 this._window = aWindow; |
|
104 this._factory = aFactory || null; |
|
105 |
|
106 var prefBranch = Services.prefs.getBranch("signon."); |
|
107 this._debug = prefBranch.getBoolPref("debug"); |
|
108 this.log("===== initialized ====="); |
|
109 }, |
|
110 |
|
111 |
|
112 /* |
|
113 * promptToSavePassword |
|
114 * |
|
115 */ |
|
116 promptToSavePassword : function (aLogin) { |
|
117 this._showSaveLoginNotification(aLogin); |
|
118 }, |
|
119 |
|
120 |
|
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; |
|
133 |
|
134 // The page we're going to hasn't loaded yet, so we want to persist |
|
135 // across the first location change. |
|
136 |
|
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. |
|
141 |
|
142 let options = { |
|
143 persistWhileVisible: true, |
|
144 timeout: Date.now() + 10000 |
|
145 } |
|
146 |
|
147 var nativeWindow = this._getNativeWindow(); |
|
148 if (nativeWindow) |
|
149 nativeWindow.doorhanger.show(aText, aName, aButtons, tabID, options); |
|
150 }, |
|
151 |
|
152 |
|
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 } |
|
170 |
|
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; |
|
175 |
|
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 ]; |
|
190 |
|
191 this._showLoginNotification("password-save", notificationText, buttons); |
|
192 }, |
|
193 |
|
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 }, |
|
205 |
|
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 } |
|
220 |
|
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; |
|
225 |
|
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 ]; |
|
240 |
|
241 this._showLoginNotification("password-change", notificationText, buttons); |
|
242 }, |
|
243 |
|
244 |
|
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; |
|
260 |
|
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 }; |
|
265 |
|
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 }, |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 /* ---------- Internal Methods ---------- */ |
|
284 |
|
285 |
|
286 |
|
287 |
|
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 }, |
|
306 |
|
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 }, |
|
319 |
|
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 } |
|
336 |
|
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 }, |
|
343 |
|
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 }, |
|
364 |
|
365 |
|
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 }, |
|
380 |
|
381 |
|
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; |
|
398 |
|
399 var hostname = scheme + "://" + uri.host; |
|
400 |
|
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 } |
|
409 |
|
410 return hostname; |
|
411 }, |
|
412 |
|
413 |
|
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; |
|
423 |
|
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 } |
|
435 |
|
436 if (!displayHost) |
|
437 displayHost = aURIString; |
|
438 |
|
439 return displayHost; |
|
440 }, |
|
441 |
|
442 }; // end of LoginManagerPrompter implementation |
|
443 |
|
444 |
|
445 var component = [LoginManagerPrompter]; |
|
446 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); |
|
447 |