1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/components/LoginManagerPrompter.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,516 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 + 1.9 +const Cc = Components.classes; 1.10 +const Ci = Components.interfaces; 1.11 +const Cr = Components.results; 1.12 + 1.13 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.14 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.15 + 1.16 +/* ==================== LoginManagerPrompter ==================== */ 1.17 +/* 1.18 + * LoginManagerPrompter 1.19 + * 1.20 + * Implements interfaces for prompting the user to enter/save/change auth info. 1.21 + * 1.22 + * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins 1.23 + * found in HTML forms. 1.24 + */ 1.25 +function LoginManagerPrompter() { 1.26 +} 1.27 + 1.28 +LoginManagerPrompter.prototype = { 1.29 + 1.30 + classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"), 1.31 + QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]), 1.32 + 1.33 + _factory : null, 1.34 + _window : null, 1.35 + _debug : false, // mirrors signon.debug 1.36 + 1.37 + __pwmgr : null, // Password Manager service 1.38 + get _pwmgr() { 1.39 + if (!this.__pwmgr) 1.40 + this.__pwmgr = Cc["@mozilla.org/login-manager;1"]. 1.41 + getService(Ci.nsILoginManager); 1.42 + return this.__pwmgr; 1.43 + }, 1.44 + 1.45 + __promptService : null, // Prompt service for user interaction 1.46 + get _promptService() { 1.47 + if (!this.__promptService) 1.48 + this.__promptService = 1.49 + Cc["@mozilla.org/embedcomp/prompt-service;1"]. 1.50 + getService(Ci.nsIPromptService2); 1.51 + return this.__promptService; 1.52 + }, 1.53 + 1.54 + __strBundle : null, // String bundle for L10N 1.55 + get _strBundle() { 1.56 + if (!this.__strBundle) { 1.57 + var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. 1.58 + getService(Ci.nsIStringBundleService); 1.59 + this.__strBundle = bunService.createBundle( 1.60 + "chrome://browser/locale/passwordmgr.properties"); 1.61 + if (!this.__strBundle) 1.62 + throw "String bundle for Login Manager not present!"; 1.63 + } 1.64 + 1.65 + return this.__strBundle; 1.66 + }, 1.67 + 1.68 + __brandBundle : null, // String bundle for L10N 1.69 + get _brandBundle() { 1.70 + if (!this.__brandBundle) { 1.71 + var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. 1.72 + getService(Ci.nsIStringBundleService); 1.73 + this.__brandBundle = bunService.createBundle( 1.74 + "chrome://branding/locale/brand.properties"); 1.75 + if (!this.__brandBundle) 1.76 + throw "Branding string bundle not present!"; 1.77 + } 1.78 + 1.79 + return this.__brandBundle; 1.80 + }, 1.81 + 1.82 + 1.83 + __ellipsis : null, 1.84 + get _ellipsis() { 1.85 + if (!this.__ellipsis) { 1.86 + this.__ellipsis = "\u2026"; 1.87 + try { 1.88 + this.__ellipsis = Services.prefs.getComplexValue( 1.89 + "intl.ellipsis", Ci.nsIPrefLocalizedString).data; 1.90 + } catch (e) { } 1.91 + } 1.92 + return this.__ellipsis; 1.93 + }, 1.94 + 1.95 + 1.96 + /* 1.97 + * log 1.98 + * 1.99 + * Internal function for logging debug messages to the Error Console window. 1.100 + */ 1.101 + log : function (message) { 1.102 + if (!this._debug) 1.103 + return; 1.104 + 1.105 + dump("Pwmgr Prompter: " + message + "\n"); 1.106 + Services.console.logStringMessage("Pwmgr Prompter: " + message); 1.107 + }, 1.108 + 1.109 + 1.110 + /* ---------- nsILoginManagerPrompter prompts ---------- */ 1.111 + 1.112 + 1.113 + 1.114 + 1.115 + /* 1.116 + * init 1.117 + * 1.118 + */ 1.119 + init : function (aWindow, aFactory) { 1.120 + this._window = aWindow; 1.121 + this._factory = aFactory || null; 1.122 + 1.123 + var prefBranch = Services.prefs.getBranch("signon."); 1.124 + this._debug = prefBranch.getBoolPref("debug"); 1.125 + this.log("===== initialized ====="); 1.126 + }, 1.127 + 1.128 + 1.129 + /* 1.130 + * promptToSavePassword 1.131 + * 1.132 + */ 1.133 + promptToSavePassword : function (aLogin) { 1.134 + var notifyBox = this._getNotifyBox(); 1.135 + if (notifyBox) 1.136 + this._showSaveLoginNotification(notifyBox, aLogin); 1.137 + }, 1.138 + 1.139 + 1.140 + /* 1.141 + * _showLoginNotification 1.142 + * 1.143 + * Displays a notification bar. 1.144 + * 1.145 + */ 1.146 + _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) { 1.147 + var oldBar = aNotifyBox.getNotificationWithValue(aName); 1.148 + const priority = aNotifyBox.PRIORITY_INFO_MEDIUM; 1.149 + 1.150 + this.log("Adding new " + aName + " notification bar"); 1.151 + var newBar = aNotifyBox.appendNotification( 1.152 + aText, aName, 1.153 + "chrome://browser/skin/images/infobar-key.png", 1.154 + priority, aButtons); 1.155 + 1.156 + // The page we're going to hasn't loaded yet, so we want to persist 1.157 + // across the first location change. 1.158 + newBar.persistence++; 1.159 + 1.160 + // Sites like Gmail perform a funky redirect dance before you end up 1.161 + // at the post-authentication page. I don't see a good way to 1.162 + // heuristically determine when to ignore such location changes, so 1.163 + // we'll try ignoring location changes based on a time interval. 1.164 + newBar.timeout = Date.now() + 20000; // 20 seconds 1.165 + 1.166 + if (oldBar) { 1.167 + this.log("(...and removing old " + aName + " notification bar)"); 1.168 + aNotifyBox.removeNotification(oldBar); 1.169 + } 1.170 + }, 1.171 + 1.172 + 1.173 + /* 1.174 + * _showSaveLoginNotification 1.175 + * 1.176 + * Displays a notification bar (rather than a popup), to allow the user to 1.177 + * save the specified login. This allows the user to see the results of 1.178 + * their login, and only save a login which they know worked. 1.179 + * 1.180 + */ 1.181 + _showSaveLoginNotification : function (aNotifyBox, aLogin) { 1.182 + // Ugh. We can't use the strings from the popup window, because they 1.183 + // have the access key marked in the string (eg "Mo&zilla"), along 1.184 + // with some weird rules for handling access keys that do not occur 1.185 + // in the string, for L10N. See commonDialog.js's setLabelForNode(). 1.186 + var neverButtonText = 1.187 + this._getLocalizedString("notifyBarNotForThisSiteButtonText"); 1.188 + var neverButtonAccessKey = 1.189 + this._getLocalizedString("notifyBarNotForThisSiteButtonAccessKey"); 1.190 + var rememberButtonText = 1.191 + this._getLocalizedString("notifyBarRememberPasswordButtonText"); 1.192 + var rememberButtonAccessKey = 1.193 + this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey"); 1.194 + 1.195 + var brandShortName = 1.196 + this._brandBundle.GetStringFromName("brandShortName"); 1.197 + var displayHost = this._getShortDisplayHost(aLogin.hostname); 1.198 + var notificationText; 1.199 + if (aLogin.username) { 1.200 + var displayUser = this._sanitizeUsername(aLogin.username); 1.201 + notificationText = this._getLocalizedString( 1.202 + "saveLoginText", 1.203 + [brandShortName, displayUser, displayHost]); 1.204 + } else { 1.205 + notificationText = this._getLocalizedString( 1.206 + "saveLoginTextNoUsername", 1.207 + [brandShortName, displayHost]); 1.208 + } 1.209 + 1.210 + // The callbacks in |buttons| have a closure to access the variables 1.211 + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr 1.212 + // without a getService() call. 1.213 + var pwmgr = this._pwmgr; 1.214 + 1.215 + 1.216 + var buttons = [ 1.217 + // "Remember" button 1.218 + { 1.219 + label: rememberButtonText, 1.220 + accessKey: rememberButtonAccessKey, 1.221 + popup: null, 1.222 + callback: function(aNotificationBar, aButton) { 1.223 + pwmgr.addLogin(aLogin); 1.224 + } 1.225 + }, 1.226 + 1.227 + // "Never for this site" button 1.228 + { 1.229 + label: neverButtonText, 1.230 + accessKey: neverButtonAccessKey, 1.231 + popup: null, 1.232 + callback: function(aNotificationBar, aButton) { 1.233 + pwmgr.setLoginSavingEnabled(aLogin.hostname, false); 1.234 + } 1.235 + } 1.236 + ]; 1.237 + 1.238 + this._showLoginNotification(aNotifyBox, "password-save", 1.239 + notificationText, buttons); 1.240 + }, 1.241 + 1.242 + 1.243 + /* 1.244 + * promptToChangePassword 1.245 + * 1.246 + * Called when we think we detect a password change for an existing 1.247 + * login, when the form being submitted contains multiple password 1.248 + * fields. 1.249 + * 1.250 + */ 1.251 + promptToChangePassword : function (aOldLogin, aNewLogin) { 1.252 + var notifyBox = this._getNotifyBox(); 1.253 + if (notifyBox) 1.254 + this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin.password); 1.255 + }, 1.256 + 1.257 + /* 1.258 + * _showChangeLoginNotification 1.259 + * 1.260 + * Shows the Change Password notification bar. 1.261 + * 1.262 + */ 1.263 + _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewPassword) { 1.264 + var notificationText; 1.265 + if (aOldLogin.username) 1.266 + notificationText = this._getLocalizedString( 1.267 + "passwordChangeText", 1.268 + [aOldLogin.username]); 1.269 + else 1.270 + notificationText = this._getLocalizedString( 1.271 + "passwordChangeTextNoUser"); 1.272 + 1.273 + var changeButtonText = 1.274 + this._getLocalizedString("notifyBarChangeButtonText"); 1.275 + var changeButtonAccessKey = 1.276 + this._getLocalizedString("notifyBarChangeButtonAccessKey"); 1.277 + var dontChangeButtonText = 1.278 + this._getLocalizedString("notifyBarDontChangeButtonText2"); 1.279 + var dontChangeButtonAccessKey = 1.280 + this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); 1.281 + 1.282 + // The callbacks in |buttons| have a closure to access the variables 1.283 + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr 1.284 + // without a getService() call. 1.285 + var self = this; 1.286 + 1.287 + var buttons = [ 1.288 + // "Yes" button 1.289 + { 1.290 + label: changeButtonText, 1.291 + accessKey: changeButtonAccessKey, 1.292 + popup: null, 1.293 + callback: function(aNotificationBar, aButton) { 1.294 + self._updateLogin(aOldLogin, aNewPassword); 1.295 + } 1.296 + }, 1.297 + 1.298 + // "No" button 1.299 + { 1.300 + label: dontChangeButtonText, 1.301 + accessKey: dontChangeButtonAccessKey, 1.302 + popup: null, 1.303 + callback: function(aNotificationBar, aButton) { 1.304 + // do nothing 1.305 + } 1.306 + } 1.307 + ]; 1.308 + 1.309 + this._showLoginNotification(aNotifyBox, "password-change", 1.310 + notificationText, buttons); 1.311 + }, 1.312 + 1.313 + /* 1.314 + * promptToChangePasswordWithUsernames 1.315 + * 1.316 + * Called when we detect a password change in a form submission, but we 1.317 + * don't know which existing login (username) it's for. Asks the user 1.318 + * to select a username and confirm the password change. 1.319 + * 1.320 + * Note: The caller doesn't know the username for aNewLogin, so this 1.321 + * function fills in .username and .usernameField with the values 1.322 + * from the login selected by the user. 1.323 + * 1.324 + * Note; XPCOM stupidity: |count| is just |logins.length|. 1.325 + */ 1.326 + promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) { 1.327 + const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; 1.328 + 1.329 + var usernames = logins.map(function (l) l.username); 1.330 + var dialogText = this._getLocalizedString("userSelectText"); 1.331 + var dialogTitle = this._getLocalizedString("passwordChangeTitle"); 1.332 + var selectedIndex = { value: null }; 1.333 + 1.334 + // If user selects ok, outparam.value is set to the index 1.335 + // of the selected username. 1.336 + var ok = this._promptService.select(null, 1.337 + dialogTitle, dialogText, 1.338 + usernames.length, usernames, 1.339 + selectedIndex); 1.340 + if (ok) { 1.341 + // Now that we know which login to use, modify its password. 1.342 + var selectedLogin = logins[selectedIndex.value]; 1.343 + this.log("Updating password for user " + selectedLogin.username); 1.344 + this._updateLogin(selectedLogin, aNewLogin.password); 1.345 + } 1.346 + }, 1.347 + 1.348 + 1.349 + /* ---------- Internal Methods ---------- */ 1.350 + 1.351 + /* 1.352 + * _updateLogin 1.353 + */ 1.354 + _updateLogin : function (login, newPassword) { 1.355 + var now = Date.now(); 1.356 + var propBag = Cc["@mozilla.org/hash-property-bag;1"]. 1.357 + createInstance(Ci.nsIWritablePropertyBag); 1.358 + if (newPassword) { 1.359 + propBag.setProperty("password", newPassword); 1.360 + // Explicitly set the password change time here (even though it would 1.361 + // be changed automatically), to ensure that it's exactly the same 1.362 + // value as timeLastUsed. 1.363 + propBag.setProperty("timePasswordChanged", now); 1.364 + } 1.365 + propBag.setProperty("timeLastUsed", now); 1.366 + propBag.setProperty("timesUsedIncrement", 1); 1.367 + this._pwmgr.modifyLogin(login, propBag); 1.368 + }, 1.369 + 1.370 + /* 1.371 + * _getNotifyWindow 1.372 + */ 1.373 + _getNotifyWindow: function () { 1.374 + try { 1.375 + // Get topmost window, in case we're in a frame. 1.376 + var notifyWin = this._window.top; 1.377 + 1.378 + // Some sites pop up a temporary login window, when disappears 1.379 + // upon submission of credentials. We want to put the notification 1.380 + // bar in the opener window if this seems to be happening. 1.381 + if (notifyWin.opener) { 1.382 + var chromeDoc = this._getChromeWindow(notifyWin). 1.383 + document.documentElement; 1.384 + var webnav = notifyWin. 1.385 + QueryInterface(Ci.nsIInterfaceRequestor). 1.386 + getInterface(Ci.nsIWebNavigation); 1.387 + 1.388 + // Check to see if the current window was opened with chrome 1.389 + // disabled, and if so use the opener window. But if the window 1.390 + // has been used to visit other pages (ie, has a history), 1.391 + // assume it'll stick around and *don't* use the opener. 1.392 + if (chromeDoc.getAttribute("chromehidden") && 1.393 + webnav.sessionHistory.count == 1) { 1.394 + this.log("Using opener window for notification bar."); 1.395 + notifyWin = notifyWin.opener; 1.396 + } 1.397 + } 1.398 + 1.399 + return notifyWin; 1.400 + 1.401 + } catch (e) { 1.402 + // If any errors happen, just assume no notification box. 1.403 + this.log("Unable to get notify window"); 1.404 + return null; 1.405 + } 1.406 + }, 1.407 + 1.408 + /* 1.409 + * _getChromeWindow 1.410 + * 1.411 + * Given a content DOM window, returns the chrome window it's in. 1.412 + */ 1.413 + _getChromeWindow: function (aWindow) { 1.414 + var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.415 + .getInterface(Ci.nsIWebNavigation) 1.416 + .QueryInterface(Ci.nsIDocShell) 1.417 + .chromeEventHandler.ownerDocument.defaultView; 1.418 + return chromeWin; 1.419 + }, 1.420 + 1.421 + /* 1.422 + * _getNotifyBox 1.423 + * 1.424 + * Returns the notification box to this prompter, or null if there isn't 1.425 + * a notification box available. 1.426 + */ 1.427 + _getNotifyBox : function () { 1.428 + let notifyBox = null; 1.429 + 1.430 + try { 1.431 + let notifyWin = this._getNotifyWindow(); 1.432 + let windowID = notifyWin.QueryInterface(Ci.nsIInterfaceRequestor) 1.433 + .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; 1.434 + 1.435 + // Get the chrome window for the content window we're using. 1.436 + // .wrappedJSObject needed here -- see bug 422974 comment 5. 1.437 + let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; 1.438 + let browser = chromeWin.Browser.getBrowserForWindowId(windowID); 1.439 + 1.440 + notifyBox = chromeWin.getNotificationBox(browser); 1.441 + } catch (e) { 1.442 + Cu.reportError(e); 1.443 + } 1.444 + 1.445 + return notifyBox; 1.446 + }, 1.447 + 1.448 + /* 1.449 + * _getLocalizedString 1.450 + * 1.451 + * Can be called as: 1.452 + * _getLocalizedString("key1"); 1.453 + * _getLocalizedString("key2", ["arg1"]); 1.454 + * _getLocalizedString("key3", ["arg1", "arg2"]); 1.455 + * (etc) 1.456 + * 1.457 + * Returns the localized string for the specified key, 1.458 + * formatted if required. 1.459 + * 1.460 + */ 1.461 + _getLocalizedString : function (key, formatArgs) { 1.462 + if (formatArgs) 1.463 + return this._strBundle.formatStringFromName( 1.464 + key, formatArgs, formatArgs.length); 1.465 + else 1.466 + return this._strBundle.GetStringFromName(key); 1.467 + }, 1.468 + 1.469 + 1.470 + /* 1.471 + * _sanitizeUsername 1.472 + * 1.473 + * Sanitizes the specified username, by stripping quotes and truncating if 1.474 + * it's too long. This helps prevent an evil site from messing with the 1.475 + * "save password?" prompt too much. 1.476 + */ 1.477 + _sanitizeUsername : function (username) { 1.478 + if (username.length > 30) { 1.479 + username = username.substring(0, 30); 1.480 + username += this._ellipsis; 1.481 + } 1.482 + return username.replace(/['"]/g, ""); 1.483 + }, 1.484 + 1.485 + 1.486 + /* 1.487 + * _getShortDisplayHost 1.488 + * 1.489 + * Converts a login's hostname field (a URL) to a short string for 1.490 + * prompting purposes. Eg, "http://foo.com" --> "foo.com", or 1.491 + * "ftp://www.site.co.uk" --> "site.co.uk". 1.492 + */ 1.493 + _getShortDisplayHost: function (aURIString) { 1.494 + var displayHost; 1.495 + 1.496 + var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. 1.497 + getService(Ci.nsIEffectiveTLDService); 1.498 + var idnService = Cc["@mozilla.org/network/idn-service;1"]. 1.499 + getService(Ci.nsIIDNService); 1.500 + try { 1.501 + var uri = Services.io.newURI(aURIString, null, null); 1.502 + var baseDomain = eTLDService.getBaseDomain(uri); 1.503 + displayHost = idnService.convertToDisplayIDN(baseDomain, {}); 1.504 + } catch (e) { 1.505 + this.log("_getShortDisplayHost couldn't process " + aURIString); 1.506 + } 1.507 + 1.508 + if (!displayHost) 1.509 + displayHost = aURIString; 1.510 + 1.511 + return displayHost; 1.512 + }, 1.513 + 1.514 +}; // end of LoginManagerPrompter implementation 1.515 + 1.516 + 1.517 +var component = [LoginManagerPrompter]; 1.518 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); 1.519 +