1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/components/LoginManagerPrompter.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,447 @@ 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://passwordmgr/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 + 1.69 + __ellipsis : null, 1.70 + get _ellipsis() { 1.71 + if (!this.__ellipsis) { 1.72 + this.__ellipsis = "\u2026"; 1.73 + try { 1.74 + this.__ellipsis = Services.prefs.getComplexValue( 1.75 + "intl.ellipsis", Ci.nsIPrefLocalizedString).data; 1.76 + } catch (e) { } 1.77 + } 1.78 + return this.__ellipsis; 1.79 + }, 1.80 + 1.81 + 1.82 + /* 1.83 + * log 1.84 + * 1.85 + * Internal function for logging debug messages to the Error Console window. 1.86 + */ 1.87 + log : function (message) { 1.88 + if (!this._debug) 1.89 + return; 1.90 + 1.91 + dump("Pwmgr Prompter: " + message + "\n"); 1.92 + Services.console.logStringMessage("Pwmgr Prompter: " + message); 1.93 + }, 1.94 + 1.95 + 1.96 + /* ---------- nsILoginManagerPrompter prompts ---------- */ 1.97 + 1.98 + 1.99 + 1.100 + 1.101 + /* 1.102 + * init 1.103 + * 1.104 + */ 1.105 + init : function (aWindow, aFactory) { 1.106 + this._window = aWindow; 1.107 + this._factory = aFactory || null; 1.108 + 1.109 + var prefBranch = Services.prefs.getBranch("signon."); 1.110 + this._debug = prefBranch.getBoolPref("debug"); 1.111 + this.log("===== initialized ====="); 1.112 + }, 1.113 + 1.114 + 1.115 + /* 1.116 + * promptToSavePassword 1.117 + * 1.118 + */ 1.119 + promptToSavePassword : function (aLogin) { 1.120 + this._showSaveLoginNotification(aLogin); 1.121 + }, 1.122 + 1.123 + 1.124 + /* 1.125 + * _showLoginNotification 1.126 + * 1.127 + * Displays a notification doorhanger. 1.128 + * 1.129 + */ 1.130 + _showLoginNotification : function (aName, aText, aButtons) { 1.131 + this.log("Adding new " + aName + " notification bar"); 1.132 + let notifyWin = this._window.top; 1.133 + let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; 1.134 + let browser = chromeWin.BrowserApp.getBrowserForWindow(notifyWin); 1.135 + let tabID = chromeWin.BrowserApp.getTabForBrowser(browser).id; 1.136 + 1.137 + // The page we're going to hasn't loaded yet, so we want to persist 1.138 + // across the first location change. 1.139 + 1.140 + // Sites like Gmail perform a funky redirect dance before you end up 1.141 + // at the post-authentication page. I don't see a good way to 1.142 + // heuristically determine when to ignore such location changes, so 1.143 + // we'll try ignoring location changes based on a time interval. 1.144 + 1.145 + let options = { 1.146 + persistWhileVisible: true, 1.147 + timeout: Date.now() + 10000 1.148 + } 1.149 + 1.150 + var nativeWindow = this._getNativeWindow(); 1.151 + if (nativeWindow) 1.152 + nativeWindow.doorhanger.show(aText, aName, aButtons, tabID, options); 1.153 + }, 1.154 + 1.155 + 1.156 + /* 1.157 + * _showSaveLoginNotification 1.158 + * 1.159 + * Displays a notification doorhanger (rather than a popup), to allow the user to 1.160 + * save the specified login. This allows the user to see the results of 1.161 + * their login, and only save a login which they know worked. 1.162 + * 1.163 + */ 1.164 + _showSaveLoginNotification : function (aLogin) { 1.165 + var displayHost = this._getShortDisplayHost(aLogin.hostname); 1.166 + var notificationText; 1.167 + if (aLogin.username) { 1.168 + var displayUser = this._sanitizeUsername(aLogin.username); 1.169 + notificationText = this._getLocalizedString("savePassword", [displayUser, displayHost]); 1.170 + } else { 1.171 + notificationText = this._getLocalizedString("savePasswordNoUser", [displayHost]); 1.172 + } 1.173 + 1.174 + // The callbacks in |buttons| have a closure to access the variables 1.175 + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr 1.176 + // without a getService() call. 1.177 + var pwmgr = this._pwmgr; 1.178 + 1.179 + var buttons = [ 1.180 + { 1.181 + label: this._getLocalizedString("saveButton"), 1.182 + callback: function() { 1.183 + pwmgr.addLogin(aLogin); 1.184 + } 1.185 + }, 1.186 + { 1.187 + label: this._getLocalizedString("dontSaveButton"), 1.188 + callback: function() { 1.189 + // Don't set a permanent exception 1.190 + } 1.191 + } 1.192 + ]; 1.193 + 1.194 + this._showLoginNotification("password-save", notificationText, buttons); 1.195 + }, 1.196 + 1.197 + /* 1.198 + * promptToChangePassword 1.199 + * 1.200 + * Called when we think we detect a password change for an existing 1.201 + * login, when the form being submitted contains multiple password 1.202 + * fields. 1.203 + * 1.204 + */ 1.205 + promptToChangePassword : function (aOldLogin, aNewLogin) { 1.206 + this._showChangeLoginNotification(aOldLogin, aNewLogin.password); 1.207 + }, 1.208 + 1.209 + /* 1.210 + * _showChangeLoginNotification 1.211 + * 1.212 + * Shows the Change Password notification doorhanger. 1.213 + * 1.214 + */ 1.215 + _showChangeLoginNotification : function (aOldLogin, aNewPassword) { 1.216 + var notificationText; 1.217 + if (aOldLogin.username) { 1.218 + let displayUser = this._sanitizeUsername(aOldLogin.username); 1.219 + notificationText = this._getLocalizedString("updatePassword", [displayUser]); 1.220 + } else { 1.221 + notificationText = this._getLocalizedString("updatePasswordNoUser"); 1.222 + } 1.223 + 1.224 + // The callbacks in |buttons| have a closure to access the variables 1.225 + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr 1.226 + // without a getService() call. 1.227 + var self = this; 1.228 + 1.229 + var buttons = [ 1.230 + { 1.231 + label: this._getLocalizedString("updateButton"), 1.232 + callback: function() { 1.233 + self._updateLogin(aOldLogin, aNewPassword); 1.234 + } 1.235 + }, 1.236 + { 1.237 + label: this._getLocalizedString("dontUpdateButton"), 1.238 + callback: function() { 1.239 + // do nothing 1.240 + } 1.241 + } 1.242 + ]; 1.243 + 1.244 + this._showLoginNotification("password-change", notificationText, buttons); 1.245 + }, 1.246 + 1.247 + 1.248 + /* 1.249 + * promptToChangePasswordWithUsernames 1.250 + * 1.251 + * Called when we detect a password change in a form submission, but we 1.252 + * don't know which existing login (username) it's for. Asks the user 1.253 + * to select a username and confirm the password change. 1.254 + * 1.255 + * Note: The caller doesn't know the username for aNewLogin, so this 1.256 + * function fills in .username and .usernameField with the values 1.257 + * from the login selected by the user. 1.258 + * 1.259 + * Note; XPCOM stupidity: |count| is just |logins.length|. 1.260 + */ 1.261 + promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) { 1.262 + const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; 1.263 + 1.264 + var usernames = logins.map(function (l) l.username); 1.265 + var dialogText = this._getLocalizedString("userSelectText"); 1.266 + var dialogTitle = this._getLocalizedString("passwordChangeTitle"); 1.267 + var selectedIndex = { value: null }; 1.268 + 1.269 + // If user selects ok, outparam.value is set to the index 1.270 + // of the selected username. 1.271 + var ok = this._promptService.select(null, 1.272 + dialogTitle, dialogText, 1.273 + usernames.length, usernames, 1.274 + selectedIndex); 1.275 + if (ok) { 1.276 + // Now that we know which login to use, modify its password. 1.277 + var selectedLogin = logins[selectedIndex.value]; 1.278 + this.log("Updating password for user " + selectedLogin.username); 1.279 + this._updateLogin(selectedLogin, aNewLogin.password); 1.280 + } 1.281 + }, 1.282 + 1.283 + 1.284 + 1.285 + 1.286 + /* ---------- Internal Methods ---------- */ 1.287 + 1.288 + 1.289 + 1.290 + 1.291 + /* 1.292 + * _updateLogin 1.293 + */ 1.294 + _updateLogin : function (login, newPassword) { 1.295 + var now = Date.now(); 1.296 + var propBag = Cc["@mozilla.org/hash-property-bag;1"]. 1.297 + createInstance(Ci.nsIWritablePropertyBag); 1.298 + if (newPassword) { 1.299 + propBag.setProperty("password", newPassword); 1.300 + // Explicitly set the password change time here (even though it would 1.301 + // be changed automatically), to ensure that it's exactly the same 1.302 + // value as timeLastUsed. 1.303 + propBag.setProperty("timePasswordChanged", now); 1.304 + } 1.305 + propBag.setProperty("timeLastUsed", now); 1.306 + propBag.setProperty("timesUsedIncrement", 1); 1.307 + this._pwmgr.modifyLogin(login, propBag); 1.308 + }, 1.309 + 1.310 + /* 1.311 + * _getChromeWindow 1.312 + * 1.313 + * Given a content DOM window, returns the chrome window it's in. 1.314 + */ 1.315 + _getChromeWindow: function (aWindow) { 1.316 + var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.317 + .getInterface(Ci.nsIWebNavigation) 1.318 + .QueryInterface(Ci.nsIDocShell) 1.319 + .chromeEventHandler.ownerDocument.defaultView; 1.320 + return chromeWin; 1.321 + }, 1.322 + 1.323 + /* 1.324 + * _getNativeWindow 1.325 + * 1.326 + * Returns the NativeWindow to this prompter, or null if there isn't 1.327 + * a NativeWindow available (w/ error sent to logcat). 1.328 + */ 1.329 + _getNativeWindow : function () { 1.330 + let nativeWindow = null; 1.331 + try { 1.332 + let notifyWin = this._window.top; 1.333 + let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; 1.334 + if (chromeWin.NativeWindow) { 1.335 + nativeWindow = chromeWin.NativeWindow; 1.336 + } else { 1.337 + Cu.reportError("NativeWindow not available on window"); 1.338 + } 1.339 + 1.340 + } catch (e) { 1.341 + // If any errors happen, just assume no native window helper. 1.342 + Cu.reportError("No NativeWindow available: " + e); 1.343 + } 1.344 + return nativeWindow; 1.345 + }, 1.346 + 1.347 + /* 1.348 + * _getLocalizedString 1.349 + * 1.350 + * Can be called as: 1.351 + * _getLocalizedString("key1"); 1.352 + * _getLocalizedString("key2", ["arg1"]); 1.353 + * _getLocalizedString("key3", ["arg1", "arg2"]); 1.354 + * (etc) 1.355 + * 1.356 + * Returns the localized string for the specified key, 1.357 + * formatted if required. 1.358 + * 1.359 + */ 1.360 + _getLocalizedString : function (key, formatArgs) { 1.361 + if (formatArgs) 1.362 + return this._strBundle.formatStringFromName( 1.363 + key, formatArgs, formatArgs.length); 1.364 + else 1.365 + return this._strBundle.GetStringFromName(key); 1.366 + }, 1.367 + 1.368 + 1.369 + /* 1.370 + * _sanitizeUsername 1.371 + * 1.372 + * Sanitizes the specified username, by stripping quotes and truncating if 1.373 + * it's too long. This helps prevent an evil site from messing with the 1.374 + * "save password?" prompt too much. 1.375 + */ 1.376 + _sanitizeUsername : function (username) { 1.377 + if (username.length > 30) { 1.378 + username = username.substring(0, 30); 1.379 + username += this._ellipsis; 1.380 + } 1.381 + return username.replace(/['"]/g, ""); 1.382 + }, 1.383 + 1.384 + 1.385 + /* 1.386 + * _getFormattedHostname 1.387 + * 1.388 + * The aURI parameter may either be a string uri, or an nsIURI instance. 1.389 + * 1.390 + * Returns the hostname to use in a nsILoginInfo object (for example, 1.391 + * "http://example.com"). 1.392 + */ 1.393 + _getFormattedHostname : function (aURI) { 1.394 + var uri; 1.395 + if (aURI instanceof Ci.nsIURI) { 1.396 + uri = aURI; 1.397 + } else { 1.398 + uri = Services.io.newURI(aURI, null, null); 1.399 + } 1.400 + var scheme = uri.scheme; 1.401 + 1.402 + var hostname = scheme + "://" + uri.host; 1.403 + 1.404 + // If the URI explicitly specified a port, only include it when 1.405 + // it's not the default. (We never want "http://foo.com:80") 1.406 + port = uri.port; 1.407 + if (port != -1) { 1.408 + var handler = Services.io.getProtocolHandler(scheme); 1.409 + if (port != handler.defaultPort) 1.410 + hostname += ":" + port; 1.411 + } 1.412 + 1.413 + return hostname; 1.414 + }, 1.415 + 1.416 + 1.417 + /* 1.418 + * _getShortDisplayHost 1.419 + * 1.420 + * Converts a login's hostname field (a URL) to a short string for 1.421 + * prompting purposes. Eg, "http://foo.com" --> "foo.com", or 1.422 + * "ftp://www.site.co.uk" --> "site.co.uk". 1.423 + */ 1.424 + _getShortDisplayHost: function (aURIString) { 1.425 + var displayHost; 1.426 + 1.427 + var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. 1.428 + getService(Ci.nsIEffectiveTLDService); 1.429 + var idnService = Cc["@mozilla.org/network/idn-service;1"]. 1.430 + getService(Ci.nsIIDNService); 1.431 + try { 1.432 + var uri = Services.io.newURI(aURIString, null, null); 1.433 + var baseDomain = eTLDService.getBaseDomain(uri); 1.434 + displayHost = idnService.convertToDisplayIDN(baseDomain, {}); 1.435 + } catch (e) { 1.436 + this.log("_getShortDisplayHost couldn't process " + aURIString); 1.437 + } 1.438 + 1.439 + if (!displayHost) 1.440 + displayHost = aURIString; 1.441 + 1.442 + return displayHost; 1.443 + }, 1.444 + 1.445 +}; // end of LoginManagerPrompter implementation 1.446 + 1.447 + 1.448 +var component = [LoginManagerPrompter]; 1.449 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); 1.450 +