Bug 1149975 - Part 1 of 2 - Handle visibility of the login fill doorhanger anchor. r=MattN

--HG--
extra : rebase_source : fdc290d5179bf79af1198bf62b5aac98e30aa894
This commit is contained in:
Paolo Amadini 2015-05-13 15:34:14 +01:00
Родитель f66879ad04
Коммит 80dddf5864
13 изменённых файлов: 376 добавлений и 52 удалений

Просмотреть файл

@ -773,8 +773,8 @@ window[chromehidden~="toolbar"] toolbar:not(#nav-bar):not(#TabsToolbar):not(#pri
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification"); -moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification");
} }
#password-fill-notification { #login-fill-notification {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#password-fill-notification"); -moz-binding: url("chrome://browser/content/urlbarBindings.xml#login-fill-notification");
} }
.login-fill-item { .login-fill-item {

Просмотреть файл

@ -783,6 +783,7 @@
<image id="push-notification-icon" class="notification-anchor-icon" role="button"/> <image id="push-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="addons-notification-icon" class="notification-anchor-icon" role="button"/> <image id="addons-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/> <image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="login-fill-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="password-notification-icon" class="notification-anchor-icon" role="button"/> <image id="password-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="webapps-notification-icon" class="notification-anchor-icon" role="button"/> <image id="webapps-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/> <image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/>

Просмотреть файл

@ -49,9 +49,15 @@ addMessageListener("ContextMenu:DoCustomCommand", function(message) {
PageMenuChild.executeMenu(message.data); PageMenuChild.executeMenu(message.data);
}); });
addMessageListener("RemoteLogins:fillForm", function(message) {
LoginManagerContent.receiveMessage(message, content);
});
addEventListener("DOMFormHasPassword", function(event) { addEventListener("DOMFormHasPassword", function(event) {
LoginManagerContent.onDOMFormHasPassword(event, content);
InsecurePasswordUtils.checkForInsecurePasswords(event.target); InsecurePasswordUtils.checkForInsecurePasswords(event.target);
LoginManagerContent.onFormPassword(event); });
addEventListener("pageshow", function(event) {
LoginManagerContent.onPageShow(event, content);
}); });
addEventListener("DOMAutoComplete", function(event) { addEventListener("DOMAutoComplete", function(event) {
LoginManagerContent.onUsernameInput(event); LoginManagerContent.onUsernameInput(event);

Просмотреть файл

@ -2781,7 +2781,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
which is empty because the actual panel is not implemented inside an XBL which is empty because the actual panel is not implemented inside an XBL
binding, but made of elements added to the notification panel. This binding, but made of elements added to the notification panel. This
allows accessing the full structure while the panel is hidden. --> allows accessing the full structure while the panel is hidden. -->
<binding id="password-fill-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> <binding id="login-fill-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
<content> <content>
<children/> <children/>
</content> </content>

Просмотреть файл

@ -8,11 +8,25 @@
max-height: 20em; max-height: 20em;
} }
.login-fill-item[disabled] {
color: #888;
background-color: #fff;
}
.login-fill-item[disabled][selected] {
background-color: #eef;
}
.login-hostname { .login-hostname {
margin: 4px; margin: 4px;
font-weight: bold; font-weight: bold;
} }
.login-fill-item.different-hostname > .login-hostname {
color: #888;
font-style: italic;
}
.login-username { .login-username {
margin: 4px; margin: 4px;
color: #888; color: #888;

Просмотреть файл

@ -140,6 +140,12 @@
list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16.png); list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16.png);
} }
#login-fill-notification-icon {
/* Temporary icon until the capture and fill doorhangers are unified. */
list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16.png);
transform: scaleX(-1);
}
.webapps-notification-icon, .webapps-notification-icon,
#webapps-notification-icon { #webapps-notification-icon {
list-style-image: url(chrome://global/skin/icons/webapps-16.png); list-style-image: url(chrome://global/skin/icons/webapps-16.png);
@ -311,6 +317,7 @@
list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png); list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
} }
#login-fill-notification-icon,
#password-notification-icon { #password-notification-icon {
list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16@2x.png); list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16@2x.png);
} }

Просмотреть файл

@ -4223,7 +4223,8 @@ Tab.prototype = {
} }
case "DOMFormHasPassword": { case "DOMFormHasPassword": {
LoginManagerContent.onFormPassword(aEvent); LoginManagerContent.onDOMFormHasPassword(aEvent,
this.browser.contentWindow);
break; break;
} }
@ -4365,7 +4366,9 @@ Tab.prototype = {
} }
case "pageshow": { case "pageshow": {
// only send pageshow for the top-level document LoginManagerContent.onPageShow(aEvent, this.browser.contentWindow);
// The rest of this only handles pageshow for the top-level document.
if (aEvent.originalTarget.defaultView != this.browser.contentWindow) if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
return; return;

Просмотреть файл

@ -10,6 +10,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", "resource://gre/modules/AboutReader.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", "resource://gre/modules/AboutReader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
let dump = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "Content"); let dump = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "Content");
@ -87,3 +88,7 @@ let AboutReaderListener = {
} }
}; };
AboutReaderListener.init(); AboutReaderListener.init();
addMessageListener("RemoteLogins:fillForm", function(message) {
LoginManagerContent.receiveMessage(message, content);
});

Просмотреть файл

@ -3812,6 +3812,7 @@ pref("signon.rememberSignons", true);
pref("signon.autofillForms", true); pref("signon.autofillForms", true);
pref("signon.autologin.proxy", false); pref("signon.autologin.proxy", false);
pref("signon.storeWhenAutocompleteOff", true); pref("signon.storeWhenAutocompleteOff", true);
pref("signon.ui.experimental", false);
pref("signon.debug", false); pref("signon.debug", false);
// Satchel (Form Manager) prefs // Satchel (Form Manager) prefs

Просмотреть файл

@ -11,6 +11,7 @@ this.EXPORTED_SYMBOLS = [
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/LoginManagerParent.jsm");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@ -27,10 +28,8 @@ this.LoginDoorhangers.FillDoorhanger = function (properties) {
this.onListDblClick = this.onListDblClick.bind(this); this.onListDblClick = this.onListDblClick.bind(this);
this.onListKeyPress = this.onListKeyPress.bind(this); this.onListKeyPress = this.onListKeyPress.bind(this);
this.filterString = properties.filterString; for (let name of Object.getOwnPropertyNames(properties)) {
this[name] = properties[name];
if (properties.browser) {
this.browser = properties.browser;
} }
}; };
@ -48,20 +47,25 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
* web page is moved to a different chrome window by the swapDocShells method. * web page is moved to a different chrome window by the swapDocShells method.
*/ */
set browser(browser) { set browser(browser) {
const MAX_DATE_VALUE = new Date(8640000000000000);
this._browser = browser; this._browser = browser;
let doorhanger = this; let doorhanger = this;
let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications; let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications;
let notification = PopupNotifications.show( let notification = PopupNotifications.show(
browser, browser,
"password-fill", "login-fill",
"", "",
"password-notification-icon", "login-fill-notification-icon",
null, null,
null, null,
{ {
dismissed: true, dismissed: true,
persistWhileVisible: true, // This will make the anchor persist forever even if the popup is not
// visible. We'll remove the notification manually when the page
// changes, after we had time to check its final state asynchronously.
timeout: MAX_DATE_VALUE,
eventCallback: function (topic, otherBrowser) { eventCallback: function (topic, otherBrowser) {
switch (topic) { switch (topic) {
case "shown": case "shown":
@ -69,6 +73,8 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
// be called after the "show" method returns, so the reference to // be called after the "show" method returns, so the reference to
// "this.notification" will be available at this point. // "this.notification" will be available at this point.
doorhanger.bound = true; doorhanger.bound = true;
doorhanger.promiseHidden =
new Promise(resolve => doorhanger.onUnbind = resolve);
doorhanger.bind(); doorhanger.bind();
break; break;
@ -76,11 +82,12 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
case "removed": case "removed":
if (doorhanger.bound) { if (doorhanger.bound) {
doorhanger.unbind(); doorhanger.unbind();
doorhanger.onUnbind();
} }
break; break;
case "swapping": case "swapping":
this._browser = otherBrowser; doorhanger._browser = otherBrowser;
return true; return true;
} }
return false; return false;
@ -116,6 +123,11 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
} }
}, },
/**
* Promise resolved as soon as the notification is hidden.
*/
promiseHidden: Promise.resolve(),
/** /**
* Removes the doorhanger from the browser. * Removes the doorhanger from the browser.
*/ */
@ -159,6 +171,18 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
this.chomeDocument.getElementById("mainPopupSet").appendChild(this.element); this.chomeDocument.getElementById("mainPopupSet").appendChild(this.element);
}, },
/**
* Origin for which the manual fill UI should be displayed, for example
* "http://www.example.com".
*/
loginFormOrigin: "",
/**
* When no login form is present on the page, we may still display a list of
* logins, but we cannot offer manual filling.
*/
loginFormPresent: false,
/** /**
* User-editable string used to filter the list of all logins. * User-editable string used to filter the list of all logins.
*/ */
@ -192,6 +216,12 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
item.classList.add("login-fill-item"); item.classList.add("login-fill-item");
item.setAttribute("hostname", hostname); item.setAttribute("hostname", hostname);
item.setAttribute("username", username); item.setAttribute("username", username);
if (hostname != this.loginFormOrigin) {
item.classList.add("different-hostname");
}
if (!this.loginFormPresent) {
item.setAttribute("disabled", "true");
}
this.list.appendChild(item); this.list.appendChild(item);
} }
}, },
@ -222,6 +252,23 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
this.fillLogin(); this.fillLogin();
}, },
fillLogin() { fillLogin() {
if (this.list.selectedItem.hasAttribute("disabled")) {
return;
}
let formLogins = Services.logins.findLogins({}, "", "", null);
let login = formLogins.find(login => {
return login.hostname == this.list.selectedItem.getAttribute("hostname") &&
login.username == this.list.selectedItem.getAttribute("username");
});
if (login) {
LoginManagerParent.fillForm({
browser: this.browser,
loginFormOrigin: this.loginFormOrigin,
login,
}).catch(Cu.reportError);
} else {
Cu.reportError("The selected login has been removed in the meantime.");
}
this.hide(); this.hide();
}, },
}; };
@ -238,7 +285,7 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
*/ */
this.LoginDoorhangers.FillDoorhanger.find = function ({ browser }) { this.LoginDoorhangers.FillDoorhanger.find = function ({ browser }) {
let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications; let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications;
let notification = PopupNotifications.getNotification("password-fill", let notification = PopupNotifications.getNotification("login-fill",
browser); browser);
return notification && notification.doorhanger; return notification && notification.doorhanger;
}; };

Просмотреть файл

@ -164,7 +164,7 @@ var LoginManagerContent = {
return deferred.promise; return deferred.promise;
}, },
receiveMessage: function (msg) { receiveMessage: function (msg, window) {
// Convert an array of logins in simple JS-object form to an array of // Convert an array of logins in simple JS-object form to an array of
// nsILoginInfo objects. // nsILoginInfo objects.
function jsLoginsToXPCOM(logins) { function jsLoginsToXPCOM(logins) {
@ -179,6 +179,16 @@ var LoginManagerContent = {
}); });
} }
if (msg.name == "RemoteLogins:fillForm") {
this.fillForm({
topDocument: window.document,
loginFormOrigin: msg.data.loginFormOrigin,
loginsFound: jsLoginsToXPCOM(msg.data.logins),
recipes: msg.data.recipes,
});
return;
}
let request = this._takeRequest(msg); let request = this._takeRequest(msg);
switch (msg.name) { switch (msg.name) {
case "RemoteLogins:loginsFound": { case "RemoteLogins:loginsFound": {
@ -253,35 +263,145 @@ var LoginManagerContent = {
messageData); messageData);
}, },
/* onDOMFormHasPassword(event, window) {
* onFormPassword if (!event.isTrusted) {
*
* Called when an <input type="password"> element is added to the page
*/
onFormPassword: function (event) {
if (!event.isTrusted)
return; return;
}
let form = event.target; let form = event.target;
let doc = form.ownerDocument; // Always record the most recently added form with a password field.
let win = doc.defaultView; this.stateForDocument(form.ownerDocument).loginForm = form;
let messageManager = messageManagerFromWindow(win);
this._updateLoginFormPresence(window);
let messageManager = messageManagerFromWindow(window);
messageManager.sendAsyncMessage("LoginStats:LoginEncountered"); messageManager.sendAsyncMessage("LoginStats:LoginEncountered");
if (!gEnabled) if (!gEnabled) {
return; return;
}
log("onFormPassword for", form.ownerDocument.documentURI); log("onDOMFormHasPassword for", form.ownerDocument.documentURI);
this._getLoginDataFromParent(form, { showMasterPassword: true }) this._getLoginDataFromParent(form, { showMasterPassword: true })
.then(this.loginsFound.bind(this)) .then(this.loginsFound.bind(this))
.then(null, Cu.reportError); .then(null, Cu.reportError);
}, },
onPageShow(event, window) {
this._updateLoginFormPresence(window);
},
/**
* Maps all DOM content documents in this content process, including those in
* frames, to the current state used by the Login Manager.
*/
loginFormStateByDocument: new WeakMap(),
/**
* Retrieves a reference to the state object associated with the given
* document. This is initialized to an empty object.
*/
stateForDocument(document) {
let loginFormState = this.loginFormStateByDocument.get(document);
if (!loginFormState) {
loginFormState = {};
this.loginFormStateByDocument.set(document, loginFormState);
}
return loginFormState;
},
/**
* Compute whether there is a login form on any frame of the current page, and
* notify the parent process. This is one of the factors used to control the
* visibility of the password fill doorhanger anchor.
*/
_updateLoginFormPresence(topWindow) {
// For the login form presence notification, we currently support only one
// origin for each browser, so the form origin will always match the origin
// of the top level document.
let loginFormOrigin =
LoginUtils._getPasswordOrigin(topWindow.document.documentURI);
// Returns the first known loginForm present in this window or in any
// same-origin subframes. Returns null if no loginForm is currently present.
let getFirstLoginForm = thisWindow => {
let loginForm = this.stateForDocument(thisWindow.document).loginForm;
if (loginForm) {
return loginForm;
}
for (let i = 0; i < thisWindow.frames.length; i++) {
let frame = thisWindow.frames[i];
if (LoginUtils._getPasswordOrigin(frame.document.documentURI) !=
loginFormOrigin) {
continue;
}
let loginForm = getFirstLoginForm(frame);
if (loginForm) {
return loginForm;
}
}
return null;
};
// Store the actual form to use on the state for the top-level document.
let topState = this.stateForDocument(topWindow.document);
topState.loginFormForFill = getFirstLoginForm(topWindow);
// Determine whether to show the anchor icon for the current tab.
let messageManager = messageManagerFromWindow(topWindow);
messageManager.sendAsyncMessage("RemoteLogins:updateLoginFormPresence", {
loginFormOrigin,
loginFormPresent: !!topState.loginFormForFill,
});
},
/**
* Perform a password fill upon user request coming from the parent process.
* The fill will be in the form previously identified during page navigation.
*
* @param An object with the following properties:
* {
* topDocument:
* DOM document currently associated to the the top-level window
* for which the fill is requested. This may be different from the
* document that originally caused the login UI to be displayed.
* loginFormOrigin:
* String with the origin for which the login UI was displayed.
* This must match the origin of the form used for the fill.
* loginsFound:
* Array containing the login to fill. While other messages may
* have more logins, for this use case this is expected to have
* exactly one element. The origin of the login may be different
* from the origin of the form used for the fill.
* recipes:
* Fill recipes transmitted together with the original message.
* }
*/
fillForm({ topDocument, loginFormOrigin, loginsFound, recipes }) {
let topState = this.stateForDocument(topDocument);
if (!topState.loginFormForFill) {
log("fillForm: There is no login form anymore. The form may have been",
"removed or the document may have changed.");
return;
}
if (LoginUtils._getPasswordOrigin(topDocument.documentURI) !=
loginFormOrigin) {
log("fillForm: The requested origin doesn't match the one form the",
"document. This may mean we navigated to a document from a different",
"site before we had a chance to indicate this change in the user",
"interface.");
return;
}
this._fillForm(topState.loginFormForFill, true, true, true, true,
loginsFound, recipes);
},
loginsFound: function({ form, loginsFound, recipes }) { loginsFound: function({ form, loginsFound, recipes }) {
let doc = form.ownerDocument; let doc = form.ownerDocument;
let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView); let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView);
this._fillForm(form, autofillForm, false, false, loginsFound, recipes); this._fillForm(form, autofillForm, false, false, false, loginsFound, recipes);
}, },
/* /*
@ -324,7 +444,7 @@ var LoginManagerContent = {
if (usernameField == acInputField && passwordField) { if (usernameField == acInputField && passwordField) {
this._getLoginDataFromParent(acForm, { showMasterPassword: false }) this._getLoginDataFromParent(acForm, { showMasterPassword: false })
.then(({ form, loginsFound, recipes }) => { .then(({ form, loginsFound, recipes }) => {
this._fillForm(form, true, true, true, loginsFound, recipes); this._fillForm(form, true, false, true, true, loginsFound, recipes);
}) })
.then(null, Cu.reportError); .then(null, Cu.reportError);
} else { } else {
@ -626,6 +746,8 @@ var LoginManagerContent = {
* *
* @param {HTMLFormElement} form * @param {HTMLFormElement} form
* @param {bool} autofillForm denotes if we should fill the form in automatically * @param {bool} autofillForm denotes if we should fill the form in automatically
* @param {bool} clobberUsername controls if an existing username can be
* overwritten
* @param {bool} clobberPassword controls if an existing password value can be * @param {bool} clobberPassword controls if an existing password value can be
* overwritten * overwritten
* @param {bool} userTriggered is an indication of whether this filling was triggered by * @param {bool} userTriggered is an indication of whether this filling was triggered by
@ -633,7 +755,7 @@ var LoginManagerContent = {
* @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form * @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form
* @param {Set} recipes that could be used to affect how the form is filled * @param {Set} recipes that could be used to affect how the form is filled
*/ */
_fillForm : function (form, autofillForm, clobberPassword, _fillForm : function (form, autofillForm, clobberUsername, clobberPassword,
userTriggered, foundLogins, recipes) { userTriggered, foundLogins, recipes) {
let ignoreAutocomplete = true; let ignoreAutocomplete = true;
const AUTOFILL_RESULT = { const AUTOFILL_RESULT = {
@ -737,7 +859,9 @@ var LoginManagerContent = {
// Select a login to use for filling in the form. // Select a login to use for filling in the form.
var selectedLogin; var selectedLogin;
if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) { if (!clobberUsername && usernameField && (usernameField.value ||
usernameField.disabled ||
usernameField.readOnly)) {
// If username was specified in the field, it's disabled or it's readOnly, only fill in the // If username was specified in the field, it's disabled or it's readOnly, only fill in the
// password if we find a matching login. // password if we find a matching login.
var username = usernameField.value.toLowerCase(); var username = usernameField.value.toLowerCase();

Просмотреть файл

@ -15,6 +15,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "UserAutoCompleteResult",
"resource://gre/modules/LoginManagerContent.jsm"); "resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AutoCompleteE10S", XPCOMUtils.defineLazyModuleGetter(this, "AutoCompleteE10S",
"resource://gre/modules/AutoCompleteE10S.jsm"); "resource://gre/modules/AutoCompleteE10S.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
"resource://gre/modules/DeferredTask.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginDoorhangers",
"resource://gre/modules/LoginDoorhangers.jsm");
this.EXPORTED_SYMBOLS = [ "LoginManagerParent", "PasswordsMetricsProvider" ]; this.EXPORTED_SYMBOLS = [ "LoginManagerParent", "PasswordsMetricsProvider" ];
@ -168,6 +172,7 @@ var LoginManagerParent = {
mm.addMessageListener("RemoteLogins:findLogins", this); mm.addMessageListener("RemoteLogins:findLogins", this);
mm.addMessageListener("RemoteLogins:onFormSubmit", this); mm.addMessageListener("RemoteLogins:onFormSubmit", this);
mm.addMessageListener("RemoteLogins:autoCompleteLogins", this); mm.addMessageListener("RemoteLogins:autoCompleteLogins", this);
mm.addMessageListener("RemoteLogins:updateLoginFormPresence", this);
mm.addMessageListener("LoginStats:LoginEncountered", this); mm.addMessageListener("LoginStats:LoginEncountered", this);
mm.addMessageListener("LoginStats:LoginFillSuccessful", this); mm.addMessageListener("LoginStats:LoginFillSuccessful", this);
Services.obs.addObserver(this, "LoginStats:NewSavedPassword", false); Services.obs.addObserver(this, "LoginStats:NewSavedPassword", false);
@ -216,6 +221,11 @@ var LoginManagerParent = {
break; break;
} }
case "RemoteLogins:updateLoginFormPresence": {
this.updateLoginFormPresence(msg.target, data);
break;
}
case "RemoteLogins:autoCompleteLogins": { case "RemoteLogins:autoCompleteLogins": {
this.doAutocompleteSearch(data, msg.target); this.doAutocompleteSearch(data, msg.target);
break; break;
@ -241,6 +251,33 @@ var LoginManagerParent = {
} }
}, },
/**
* Trigger a login form fill and send relevant data (e.g. logins and recipes)
* to the child process (LoginManagerContent).
*/
fillForm: Task.async(function* ({ browser, loginFormOrigin, login }) {
let recipes = [];
if (loginFormOrigin) {
let formHost;
try {
formHost = (new URL(loginFormOrigin)).host;
let recipeManager = yield this.recipeParentPromise;
recipes = recipeManager.getRecipesForHost(formHost);
} catch (ex) {
// Some schemes e.g. chrome aren't supported by URL
}
}
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
// doesn't support structured cloning.
let jsLogins = JSON.parse(JSON.stringify([login]));
browser.messageManager.sendAsyncMessage("RemoteLogins:fillForm", {
loginFormOrigin,
logins: jsLogins,
recipes,
});
}),
/** /**
* Send relevant data (e.g. logins and recipes) to the child process (LoginManagerContent). * Send relevant data (e.g. logins and recipes) to the child process (LoginManagerContent).
*/ */
@ -281,14 +318,14 @@ var LoginManagerParent = {
// If we're currently displaying a master password prompt, defer // If we're currently displaying a master password prompt, defer
// processing this form until the user handles the prompt. // processing this form until the user handles the prompt.
if (Services.logins.uiBusy) { if (Services.logins.uiBusy) {
log("deferring onFormPassword for", formOrigin); log("deferring sendLoginDataToChild for", formOrigin);
let self = this; let self = this;
let observer = { let observer = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]), Ci.nsISupportsWeakReference]),
observe: function (subject, topic, data) { observe: function (subject, topic, data) {
log("Got deferred onFormPassword notification:", topic); log("Got deferred sendLoginDataToChild notification:", topic);
// Only run observer once. // Only run observer once.
Services.obs.removeObserver(this, "passwordmgr-crypto-login"); Services.obs.removeObserver(this, "passwordmgr-crypto-login");
Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled"); Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
@ -507,5 +544,101 @@ var LoginManagerParent = {
// Prompt user to save login (via dialog or notification bar) // Prompt user to save login (via dialog or notification bar)
prompter = getPrompter(); prompter = getPrompter();
prompter.promptToSavePassword(formLogin); prompter.promptToSavePassword(formLogin);
} },
/**
* Maps all the <browser> elements for tabs in the parent process to the
* current state used to display tab-specific UI.
*
* This mapping is not updated in case a web page is moved to a different
* chrome window by the swapDocShells method. In this case, it is possible
* that a UI update just requested for the login fill doorhanger and then
* delayed by a few hundred milliseconds will be lost. Later requests would
* use the new browser reference instead.
*
* Given that the case above is rare, and it would not cause any origin
* mismatch at the time of filling because the origin is checked later in the
* content process, this case is left unhandled.
*/
loginFormStateByBrowser: new WeakMap(),
/**
* Retrieves a reference to the state object associated with the given
* browser. This is initialized to an empty object.
*/
stateForBrowser(browser) {
let loginFormState = this.loginFormStateByBrowser.get(browser);
if (!loginFormState) {
loginFormState = {};
this.loginFormStateByBrowser.set(browser, loginFormState);
}
return loginFormState;
},
/**
* Called to indicate whether a login form on the currently loaded page is
* present or not. This is one of the factors used to control the visibility
* of the password fill doorhanger.
*/
updateLoginFormPresence(browser, { loginFormOrigin, loginFormPresent }) {
const ANCHOR_DELAY_MS = 200;
let state = this.stateForBrowser(browser);
// Update the data to use to the latest known values. Since messages are
// processed in order, this will always be the latest version to use.
state.loginFormOrigin = loginFormOrigin;
state.loginFormPresent = loginFormPresent;
// Apply the data to the currently displayed icon later.
if (!state.anchorDeferredTask) {
state.anchorDeferredTask = new DeferredTask(
() => this.updateLoginAnchor(browser),
ANCHOR_DELAY_MS
);
}
state.anchorDeferredTask.arm();
},
updateLoginAnchor: Task.async(function* (browser) {
// Copy the state to use for this execution of the task. These will not
// change during this execution of the asynchronous function, but in case a
// change happens in the state, the function will be retriggered.
let { loginFormOrigin, loginFormPresent } = this.stateForBrowser(browser);
yield Services.logins.initializationPromise;
// Check if there are form logins for the site, ignoring formSubmitURL.
let hasLogins = loginFormOrigin &&
Services.logins.countLogins(loginFormOrigin, "", null) > 0;
// Once this preference is removed, this version of the fill doorhanger
// should be enabled for Desktop only, and not for Android or B2G.
if (!Services.prefs.getBoolPref("signon.ui.experimental")) {
return;
}
let showLoginAnchor = loginFormPresent || hasLogins;
let fillDoorhanger = LoginDoorhangers.FillDoorhanger.find({ browser });
if (fillDoorhanger) {
if (!showLoginAnchor) {
fillDoorhanger.remove();
return;
}
// We should only update the state of the doorhanger while it is hidden.
yield fillDoorhanger.promiseHidden;
fillDoorhanger.loginFormPresent = loginFormPresent;
fillDoorhanger.loginFormOrigin = loginFormOrigin;
fillDoorhanger.filterString = loginFormOrigin;
return;
}
if (showLoginAnchor) {
fillDoorhanger = new LoginDoorhangers.FillDoorhanger({
browser,
loginFormPresent,
loginFormOrigin,
filterString: loginFormOrigin,
});
}
}),
}; };

Просмотреть файл

@ -120,8 +120,6 @@ LoginManager.prototype = {
// Form submit observer checks forms for new logins and pw changes. // Form submit observer checks forms for new logins and pw changes.
Services.obs.addObserver(this._observer, "xpcom-shutdown", false); Services.obs.addObserver(this._observer, "xpcom-shutdown", false);
// TODO: Make this class useful in the child process (in addition to
// autoCompleteSearchAsync and fillForm).
if (Services.appinfo.processType === if (Services.appinfo.processType ===
Services.appinfo.PROCESS_TYPE_DEFAULT) { Services.appinfo.PROCESS_TYPE_DEFAULT) {
Services.obs.addObserver(this._observer, "passwordmgr-storage-replace", Services.obs.addObserver(this._observer, "passwordmgr-storage-replace",
@ -577,21 +575,6 @@ LoginManager.prototype = {
return this._getPasswordOrigin(uriString, true); return this._getPasswordOrigin(uriString, true);
}, },
/*
* fillForm
*
* Fill the form with login information if we can find it.
*/
fillForm : function (form) {
log("fillForm processing form[ id:", form.id, "]");
return LoginManagerContent._asyncFindLogins(form, { showMasterPassword: true })
.then(function({ form, loginsFound }) {
return LoginManagerContent._fillForm(form, true, false, false, loginsFound)[0];
});
},
}; // end of LoginManager implementation }; // end of LoginManager implementation
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]); this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);