зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f66879ad04
Коммит
80dddf5864
|
@ -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]);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче