browser/metro/components/LoginManagerPrompter.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

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://browser/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 __brandBundle : null, // String bundle for L10N
michael@0 66 get _brandBundle() {
michael@0 67 if (!this.__brandBundle) {
michael@0 68 var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
michael@0 69 getService(Ci.nsIStringBundleService);
michael@0 70 this.__brandBundle = bunService.createBundle(
michael@0 71 "chrome://branding/locale/brand.properties");
michael@0 72 if (!this.__brandBundle)
michael@0 73 throw "Branding string bundle not present!";
michael@0 74 }
michael@0 75
michael@0 76 return this.__brandBundle;
michael@0 77 },
michael@0 78
michael@0 79
michael@0 80 __ellipsis : null,
michael@0 81 get _ellipsis() {
michael@0 82 if (!this.__ellipsis) {
michael@0 83 this.__ellipsis = "\u2026";
michael@0 84 try {
michael@0 85 this.__ellipsis = Services.prefs.getComplexValue(
michael@0 86 "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
michael@0 87 } catch (e) { }
michael@0 88 }
michael@0 89 return this.__ellipsis;
michael@0 90 },
michael@0 91
michael@0 92
michael@0 93 /*
michael@0 94 * log
michael@0 95 *
michael@0 96 * Internal function for logging debug messages to the Error Console window.
michael@0 97 */
michael@0 98 log : function (message) {
michael@0 99 if (!this._debug)
michael@0 100 return;
michael@0 101
michael@0 102 dump("Pwmgr Prompter: " + message + "\n");
michael@0 103 Services.console.logStringMessage("Pwmgr Prompter: " + message);
michael@0 104 },
michael@0 105
michael@0 106
michael@0 107 /* ---------- nsILoginManagerPrompter prompts ---------- */
michael@0 108
michael@0 109
michael@0 110
michael@0 111
michael@0 112 /*
michael@0 113 * init
michael@0 114 *
michael@0 115 */
michael@0 116 init : function (aWindow, aFactory) {
michael@0 117 this._window = aWindow;
michael@0 118 this._factory = aFactory || null;
michael@0 119
michael@0 120 var prefBranch = Services.prefs.getBranch("signon.");
michael@0 121 this._debug = prefBranch.getBoolPref("debug");
michael@0 122 this.log("===== initialized =====");
michael@0 123 },
michael@0 124
michael@0 125
michael@0 126 /*
michael@0 127 * promptToSavePassword
michael@0 128 *
michael@0 129 */
michael@0 130 promptToSavePassword : function (aLogin) {
michael@0 131 var notifyBox = this._getNotifyBox();
michael@0 132 if (notifyBox)
michael@0 133 this._showSaveLoginNotification(notifyBox, aLogin);
michael@0 134 },
michael@0 135
michael@0 136
michael@0 137 /*
michael@0 138 * _showLoginNotification
michael@0 139 *
michael@0 140 * Displays a notification bar.
michael@0 141 *
michael@0 142 */
michael@0 143 _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
michael@0 144 var oldBar = aNotifyBox.getNotificationWithValue(aName);
michael@0 145 const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
michael@0 146
michael@0 147 this.log("Adding new " + aName + " notification bar");
michael@0 148 var newBar = aNotifyBox.appendNotification(
michael@0 149 aText, aName,
michael@0 150 "chrome://browser/skin/images/infobar-key.png",
michael@0 151 priority, aButtons);
michael@0 152
michael@0 153 // The page we're going to hasn't loaded yet, so we want to persist
michael@0 154 // across the first location change.
michael@0 155 newBar.persistence++;
michael@0 156
michael@0 157 // Sites like Gmail perform a funky redirect dance before you end up
michael@0 158 // at the post-authentication page. I don't see a good way to
michael@0 159 // heuristically determine when to ignore such location changes, so
michael@0 160 // we'll try ignoring location changes based on a time interval.
michael@0 161 newBar.timeout = Date.now() + 20000; // 20 seconds
michael@0 162
michael@0 163 if (oldBar) {
michael@0 164 this.log("(...and removing old " + aName + " notification bar)");
michael@0 165 aNotifyBox.removeNotification(oldBar);
michael@0 166 }
michael@0 167 },
michael@0 168
michael@0 169
michael@0 170 /*
michael@0 171 * _showSaveLoginNotification
michael@0 172 *
michael@0 173 * Displays a notification bar (rather than a popup), to allow the user to
michael@0 174 * save the specified login. This allows the user to see the results of
michael@0 175 * their login, and only save a login which they know worked.
michael@0 176 *
michael@0 177 */
michael@0 178 _showSaveLoginNotification : function (aNotifyBox, aLogin) {
michael@0 179 // Ugh. We can't use the strings from the popup window, because they
michael@0 180 // have the access key marked in the string (eg "Mo&zilla"), along
michael@0 181 // with some weird rules for handling access keys that do not occur
michael@0 182 // in the string, for L10N. See commonDialog.js's setLabelForNode().
michael@0 183 var neverButtonText =
michael@0 184 this._getLocalizedString("notifyBarNotForThisSiteButtonText");
michael@0 185 var neverButtonAccessKey =
michael@0 186 this._getLocalizedString("notifyBarNotForThisSiteButtonAccessKey");
michael@0 187 var rememberButtonText =
michael@0 188 this._getLocalizedString("notifyBarRememberPasswordButtonText");
michael@0 189 var rememberButtonAccessKey =
michael@0 190 this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
michael@0 191
michael@0 192 var brandShortName =
michael@0 193 this._brandBundle.GetStringFromName("brandShortName");
michael@0 194 var displayHost = this._getShortDisplayHost(aLogin.hostname);
michael@0 195 var notificationText;
michael@0 196 if (aLogin.username) {
michael@0 197 var displayUser = this._sanitizeUsername(aLogin.username);
michael@0 198 notificationText = this._getLocalizedString(
michael@0 199 "saveLoginText",
michael@0 200 [brandShortName, displayUser, displayHost]);
michael@0 201 } else {
michael@0 202 notificationText = this._getLocalizedString(
michael@0 203 "saveLoginTextNoUsername",
michael@0 204 [brandShortName, displayHost]);
michael@0 205 }
michael@0 206
michael@0 207 // The callbacks in |buttons| have a closure to access the variables
michael@0 208 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
michael@0 209 // without a getService() call.
michael@0 210 var pwmgr = this._pwmgr;
michael@0 211
michael@0 212
michael@0 213 var buttons = [
michael@0 214 // "Remember" button
michael@0 215 {
michael@0 216 label: rememberButtonText,
michael@0 217 accessKey: rememberButtonAccessKey,
michael@0 218 popup: null,
michael@0 219 callback: function(aNotificationBar, aButton) {
michael@0 220 pwmgr.addLogin(aLogin);
michael@0 221 }
michael@0 222 },
michael@0 223
michael@0 224 // "Never for this site" button
michael@0 225 {
michael@0 226 label: neverButtonText,
michael@0 227 accessKey: neverButtonAccessKey,
michael@0 228 popup: null,
michael@0 229 callback: function(aNotificationBar, aButton) {
michael@0 230 pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
michael@0 231 }
michael@0 232 }
michael@0 233 ];
michael@0 234
michael@0 235 this._showLoginNotification(aNotifyBox, "password-save",
michael@0 236 notificationText, buttons);
michael@0 237 },
michael@0 238
michael@0 239
michael@0 240 /*
michael@0 241 * promptToChangePassword
michael@0 242 *
michael@0 243 * Called when we think we detect a password change for an existing
michael@0 244 * login, when the form being submitted contains multiple password
michael@0 245 * fields.
michael@0 246 *
michael@0 247 */
michael@0 248 promptToChangePassword : function (aOldLogin, aNewLogin) {
michael@0 249 var notifyBox = this._getNotifyBox();
michael@0 250 if (notifyBox)
michael@0 251 this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin.password);
michael@0 252 },
michael@0 253
michael@0 254 /*
michael@0 255 * _showChangeLoginNotification
michael@0 256 *
michael@0 257 * Shows the Change Password notification bar.
michael@0 258 *
michael@0 259 */
michael@0 260 _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewPassword) {
michael@0 261 var notificationText;
michael@0 262 if (aOldLogin.username)
michael@0 263 notificationText = this._getLocalizedString(
michael@0 264 "passwordChangeText",
michael@0 265 [aOldLogin.username]);
michael@0 266 else
michael@0 267 notificationText = this._getLocalizedString(
michael@0 268 "passwordChangeTextNoUser");
michael@0 269
michael@0 270 var changeButtonText =
michael@0 271 this._getLocalizedString("notifyBarChangeButtonText");
michael@0 272 var changeButtonAccessKey =
michael@0 273 this._getLocalizedString("notifyBarChangeButtonAccessKey");
michael@0 274 var dontChangeButtonText =
michael@0 275 this._getLocalizedString("notifyBarDontChangeButtonText2");
michael@0 276 var dontChangeButtonAccessKey =
michael@0 277 this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
michael@0 278
michael@0 279 // The callbacks in |buttons| have a closure to access the variables
michael@0 280 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
michael@0 281 // without a getService() call.
michael@0 282 var self = this;
michael@0 283
michael@0 284 var buttons = [
michael@0 285 // "Yes" button
michael@0 286 {
michael@0 287 label: changeButtonText,
michael@0 288 accessKey: changeButtonAccessKey,
michael@0 289 popup: null,
michael@0 290 callback: function(aNotificationBar, aButton) {
michael@0 291 self._updateLogin(aOldLogin, aNewPassword);
michael@0 292 }
michael@0 293 },
michael@0 294
michael@0 295 // "No" button
michael@0 296 {
michael@0 297 label: dontChangeButtonText,
michael@0 298 accessKey: dontChangeButtonAccessKey,
michael@0 299 popup: null,
michael@0 300 callback: function(aNotificationBar, aButton) {
michael@0 301 // do nothing
michael@0 302 }
michael@0 303 }
michael@0 304 ];
michael@0 305
michael@0 306 this._showLoginNotification(aNotifyBox, "password-change",
michael@0 307 notificationText, buttons);
michael@0 308 },
michael@0 309
michael@0 310 /*
michael@0 311 * promptToChangePasswordWithUsernames
michael@0 312 *
michael@0 313 * Called when we detect a password change in a form submission, but we
michael@0 314 * don't know which existing login (username) it's for. Asks the user
michael@0 315 * to select a username and confirm the password change.
michael@0 316 *
michael@0 317 * Note: The caller doesn't know the username for aNewLogin, so this
michael@0 318 * function fills in .username and .usernameField with the values
michael@0 319 * from the login selected by the user.
michael@0 320 *
michael@0 321 * Note; XPCOM stupidity: |count| is just |logins.length|.
michael@0 322 */
michael@0 323 promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
michael@0 324 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
michael@0 325
michael@0 326 var usernames = logins.map(function (l) l.username);
michael@0 327 var dialogText = this._getLocalizedString("userSelectText");
michael@0 328 var dialogTitle = this._getLocalizedString("passwordChangeTitle");
michael@0 329 var selectedIndex = { value: null };
michael@0 330
michael@0 331 // If user selects ok, outparam.value is set to the index
michael@0 332 // of the selected username.
michael@0 333 var ok = this._promptService.select(null,
michael@0 334 dialogTitle, dialogText,
michael@0 335 usernames.length, usernames,
michael@0 336 selectedIndex);
michael@0 337 if (ok) {
michael@0 338 // Now that we know which login to use, modify its password.
michael@0 339 var selectedLogin = logins[selectedIndex.value];
michael@0 340 this.log("Updating password for user " + selectedLogin.username);
michael@0 341 this._updateLogin(selectedLogin, aNewLogin.password);
michael@0 342 }
michael@0 343 },
michael@0 344
michael@0 345
michael@0 346 /* ---------- Internal Methods ---------- */
michael@0 347
michael@0 348 /*
michael@0 349 * _updateLogin
michael@0 350 */
michael@0 351 _updateLogin : function (login, newPassword) {
michael@0 352 var now = Date.now();
michael@0 353 var propBag = Cc["@mozilla.org/hash-property-bag;1"].
michael@0 354 createInstance(Ci.nsIWritablePropertyBag);
michael@0 355 if (newPassword) {
michael@0 356 propBag.setProperty("password", newPassword);
michael@0 357 // Explicitly set the password change time here (even though it would
michael@0 358 // be changed automatically), to ensure that it's exactly the same
michael@0 359 // value as timeLastUsed.
michael@0 360 propBag.setProperty("timePasswordChanged", now);
michael@0 361 }
michael@0 362 propBag.setProperty("timeLastUsed", now);
michael@0 363 propBag.setProperty("timesUsedIncrement", 1);
michael@0 364 this._pwmgr.modifyLogin(login, propBag);
michael@0 365 },
michael@0 366
michael@0 367 /*
michael@0 368 * _getNotifyWindow
michael@0 369 */
michael@0 370 _getNotifyWindow: function () {
michael@0 371 try {
michael@0 372 // Get topmost window, in case we're in a frame.
michael@0 373 var notifyWin = this._window.top;
michael@0 374
michael@0 375 // Some sites pop up a temporary login window, when disappears
michael@0 376 // upon submission of credentials. We want to put the notification
michael@0 377 // bar in the opener window if this seems to be happening.
michael@0 378 if (notifyWin.opener) {
michael@0 379 var chromeDoc = this._getChromeWindow(notifyWin).
michael@0 380 document.documentElement;
michael@0 381 var webnav = notifyWin.
michael@0 382 QueryInterface(Ci.nsIInterfaceRequestor).
michael@0 383 getInterface(Ci.nsIWebNavigation);
michael@0 384
michael@0 385 // Check to see if the current window was opened with chrome
michael@0 386 // disabled, and if so use the opener window. But if the window
michael@0 387 // has been used to visit other pages (ie, has a history),
michael@0 388 // assume it'll stick around and *don't* use the opener.
michael@0 389 if (chromeDoc.getAttribute("chromehidden") &&
michael@0 390 webnav.sessionHistory.count == 1) {
michael@0 391 this.log("Using opener window for notification bar.");
michael@0 392 notifyWin = notifyWin.opener;
michael@0 393 }
michael@0 394 }
michael@0 395
michael@0 396 return notifyWin;
michael@0 397
michael@0 398 } catch (e) {
michael@0 399 // If any errors happen, just assume no notification box.
michael@0 400 this.log("Unable to get notify window");
michael@0 401 return null;
michael@0 402 }
michael@0 403 },
michael@0 404
michael@0 405 /*
michael@0 406 * _getChromeWindow
michael@0 407 *
michael@0 408 * Given a content DOM window, returns the chrome window it's in.
michael@0 409 */
michael@0 410 _getChromeWindow: function (aWindow) {
michael@0 411 var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 412 .getInterface(Ci.nsIWebNavigation)
michael@0 413 .QueryInterface(Ci.nsIDocShell)
michael@0 414 .chromeEventHandler.ownerDocument.defaultView;
michael@0 415 return chromeWin;
michael@0 416 },
michael@0 417
michael@0 418 /*
michael@0 419 * _getNotifyBox
michael@0 420 *
michael@0 421 * Returns the notification box to this prompter, or null if there isn't
michael@0 422 * a notification box available.
michael@0 423 */
michael@0 424 _getNotifyBox : function () {
michael@0 425 let notifyBox = null;
michael@0 426
michael@0 427 try {
michael@0 428 let notifyWin = this._getNotifyWindow();
michael@0 429 let windowID = notifyWin.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 430 .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
michael@0 431
michael@0 432 // Get the chrome window for the content window we're using.
michael@0 433 // .wrappedJSObject needed here -- see bug 422974 comment 5.
michael@0 434 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
michael@0 435 let browser = chromeWin.Browser.getBrowserForWindowId(windowID);
michael@0 436
michael@0 437 notifyBox = chromeWin.getNotificationBox(browser);
michael@0 438 } catch (e) {
michael@0 439 Cu.reportError(e);
michael@0 440 }
michael@0 441
michael@0 442 return notifyBox;
michael@0 443 },
michael@0 444
michael@0 445 /*
michael@0 446 * _getLocalizedString
michael@0 447 *
michael@0 448 * Can be called as:
michael@0 449 * _getLocalizedString("key1");
michael@0 450 * _getLocalizedString("key2", ["arg1"]);
michael@0 451 * _getLocalizedString("key3", ["arg1", "arg2"]);
michael@0 452 * (etc)
michael@0 453 *
michael@0 454 * Returns the localized string for the specified key,
michael@0 455 * formatted if required.
michael@0 456 *
michael@0 457 */
michael@0 458 _getLocalizedString : function (key, formatArgs) {
michael@0 459 if (formatArgs)
michael@0 460 return this._strBundle.formatStringFromName(
michael@0 461 key, formatArgs, formatArgs.length);
michael@0 462 else
michael@0 463 return this._strBundle.GetStringFromName(key);
michael@0 464 },
michael@0 465
michael@0 466
michael@0 467 /*
michael@0 468 * _sanitizeUsername
michael@0 469 *
michael@0 470 * Sanitizes the specified username, by stripping quotes and truncating if
michael@0 471 * it's too long. This helps prevent an evil site from messing with the
michael@0 472 * "save password?" prompt too much.
michael@0 473 */
michael@0 474 _sanitizeUsername : function (username) {
michael@0 475 if (username.length > 30) {
michael@0 476 username = username.substring(0, 30);
michael@0 477 username += this._ellipsis;
michael@0 478 }
michael@0 479 return username.replace(/['"]/g, "");
michael@0 480 },
michael@0 481
michael@0 482
michael@0 483 /*
michael@0 484 * _getShortDisplayHost
michael@0 485 *
michael@0 486 * Converts a login's hostname field (a URL) to a short string for
michael@0 487 * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
michael@0 488 * "ftp://www.site.co.uk" --> "site.co.uk".
michael@0 489 */
michael@0 490 _getShortDisplayHost: function (aURIString) {
michael@0 491 var displayHost;
michael@0 492
michael@0 493 var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
michael@0 494 getService(Ci.nsIEffectiveTLDService);
michael@0 495 var idnService = Cc["@mozilla.org/network/idn-service;1"].
michael@0 496 getService(Ci.nsIIDNService);
michael@0 497 try {
michael@0 498 var uri = Services.io.newURI(aURIString, null, null);
michael@0 499 var baseDomain = eTLDService.getBaseDomain(uri);
michael@0 500 displayHost = idnService.convertToDisplayIDN(baseDomain, {});
michael@0 501 } catch (e) {
michael@0 502 this.log("_getShortDisplayHost couldn't process " + aURIString);
michael@0 503 }
michael@0 504
michael@0 505 if (!displayHost)
michael@0 506 displayHost = aURIString;
michael@0 507
michael@0 508 return displayHost;
michael@0 509 },
michael@0 510
michael@0 511 }; // end of LoginManagerPrompter implementation
michael@0 512
michael@0 513
michael@0 514 var component = [LoginManagerPrompter];
michael@0 515 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
michael@0 516

mercurial