mobile/android/components/LoginManagerPrompter.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial