/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = [ "LoginManagerContent", "LoginFormFactory", "UserAutoCompleteResult" ]; const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1; const AUTOCOMPLETE_AFTER_CONTEXTMENU_THRESHOLD_MS = 250; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); Cu.import("resource://gre/modules/InsecurePasswordUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Timer.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory", "resource://gre/modules/FormLikeFactory.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent", "resource://gre/modules/LoginRecipes.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", "resource://gre/modules/LoginHelper.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils", "resource://gre/modules/InsecurePasswordUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "gNetUtil", "@mozilla.org/network/util;1", "nsINetUtil"); XPCOMUtils.defineLazyGetter(this, "log", () => { let logger = LoginHelper.createLogger("LoginManagerContent"); return logger.log.bind(logger); }); // These mirror signon.* prefs. var gEnabled, gAutofillForms, gStoreWhenAutocompleteOff; var gLastContextMenuEventTimeStamp = Number.NEGATIVE_INFINITY; var observer = { QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIFormSubmitObserver, Ci.nsIWebProgressListener, Ci.nsIDOMEventListener, Ci.nsISupportsWeakReference]), // nsIFormSubmitObserver notify(formElement, aWindow, actionURI) { log("observer notified for form submission."); // We're invoked before the content's |onsubmit| handlers, so we // can grab form data before it might be modified (see bug 257781). try { let formLike = LoginFormFactory.createFromForm(formElement); LoginManagerContent._onFormSubmit(formLike); } catch (e) { log("Caught error in onFormSubmit(", e.lineNumber, "):", e.message); Cu.reportError(e); } return true; // Always return true, or form submit will be canceled. }, onPrefChange() { gEnabled = Services.prefs.getBoolPref("signon.rememberSignons"); gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms"); gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff"); }, // nsIWebProgressListener onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { // Only handle pushState/replaceState here. if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) || !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) { return; } log("onLocationChange handled:", aLocation.spec, aWebProgress.DOMWindow.document); LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document); }, onStateChange(aWebProgress, aRequest, aState, aStatus) { if (!(aState & Ci.nsIWebProgressListener.STATE_START)) { return; } // We only care about when a page triggered a load, not the user. For example: // clicking refresh/back/forward, typing a URL and hitting enter, and loading a bookmark aren't // likely to be when a user wants to save a login. let channel = aRequest.QueryInterface(Ci.nsIChannel); let triggeringPrincipal = channel.loadInfo.triggeringPrincipal; if (triggeringPrincipal.isNullPrincipal || triggeringPrincipal.equals(Services.scriptSecurityManager.getSystemPrincipal())) { return; } // Don't handle history navigation, reload, or pushState not triggered via chrome UI. // e.g. history.go(-1), location.reload(), history.replaceState() if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) { log("onStateChange: loadType isn't LOAD_CMD_NORMAL:", aWebProgress.loadType); return; } log("onStateChange handled:", channel); LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document); }, handleEvent(aEvent) { if (!aEvent.isTrusted) { return; } if (!gEnabled) { return; } switch (aEvent.type) { // Only used for username fields. case "focus": { LoginManagerContent._onUsernameFocus(aEvent); break; } case "contextmenu": { gLastContextMenuEventTimeStamp = Date.now(); break; } default: { throw new Error("Unexpected event"); } } }, }; Services.obs.addObserver(observer, "earlyformsubmit", false); var prefBranch = Services.prefs.getBranch("signon."); prefBranch.addObserver("", observer.onPrefChange, false); observer.onPrefChange(); // read initial values function messageManagerFromWindow(win) { return win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIContentFrameMessageManager); } // This object maps to the "child" process (even in the single-process case). var LoginManagerContent = { __formFillService : null, // FormFillController, for username autocompleting get _formFillService() { if (!this.__formFillService) this.__formFillService = Cc["@mozilla.org/satchel/form-fill-controller;1"]. getService(Ci.nsIFormFillController); return this.__formFillService; }, _getRandomId() { return Cc["@mozilla.org/uuid-generator;1"] .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); }, _messages: [ "RemoteLogins:loginsFound", "RemoteLogins:loginsAutoCompleted" ], /** * WeakMap of the root element of a FormLike to the FormLike representing its fields. * * This is used to be able to lookup an existing FormLike for a given root element since multiple * calls to LoginFormFactory won't give the exact same object. When batching fills we don't always * want to use the most recent list of elements for a FormLike since we may end up doing multiple * fills for the same set of elements when a field gets added between arming and running the * DeferredTask. * * @type {WeakMap} */ _formLikeByRootElement: new WeakMap(), /** * WeakMap of the root element of a WeakMap to the DeferredTask to fill its fields. * * This is used to be able to throttle fills for a FormLike since onDOMInputPasswordAdded gets * dispatched for each password field added to a document but we only want to fill once per * FormLike when multiple fields are added at once. * * @type {WeakMap} */ _deferredPasswordAddedTasksByRootElement: new WeakMap(), // Map from form login requests to information about that request. _requests: new Map(), // Number of outstanding requests to each manager. _managers: new Map(), _takeRequest(msg) { let data = msg.data; let request = this._requests.get(data.requestId); this._requests.delete(data.requestId); let count = this._managers.get(msg.target); if (--count === 0) { this._managers.delete(msg.target); for (let message of this._messages) msg.target.removeMessageListener(message, this); } else { this._managers.set(msg.target, count); } return request; }, _sendRequest(messageManager, requestData, name, messageData) { let count; if (!(count = this._managers.get(messageManager))) { this._managers.set(messageManager, 1); for (let message of this._messages) messageManager.addMessageListener(message, this); } else { this._managers.set(messageManager, ++count); } let requestId = this._getRandomId(); messageData.requestId = requestId; messageManager.sendAsyncMessage(name, messageData); let deferred = Promise.defer(); requestData.promise = deferred; this._requests.set(requestId, requestData); return deferred.promise; }, receiveMessage(msg, window) { if (msg.name == "RemoteLogins:fillForm") { this.fillForm({ topDocument: window.document, loginFormOrigin: msg.data.loginFormOrigin, loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins), recipes: msg.data.recipes, inputElement: msg.objects.inputElement, }); return; } let request = this._takeRequest(msg); switch (msg.name) { case "RemoteLogins:loginsFound": { let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins); request.promise.resolve({ form: request.form, loginsFound: loginsFound, recipes: msg.data.recipes, }); break; } case "RemoteLogins:loginsAutoCompleted": { let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins); // If we're in the parent process, don't pass a message manager so our // autocomplete result objects know they can remove the login from the // login manager directly. let messageManager = (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) ? msg.target : undefined; request.promise.resolve({ logins: loginsFound, messageManager }); break; } } }, /** * Get relevant logins and recipes from the parent * * @param {HTMLFormElement} form - form to get login data for * @param {Object} options * @param {boolean} options.showMasterPassword - whether to show a master password prompt */ _getLoginDataFromParent(form, options) { let doc = form.ownerDocument; let win = doc.defaultView; let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI); if (!formOrigin) { return Promise.reject("_getLoginDataFromParent: A form origin is required"); } let actionOrigin = LoginUtils._getActionOrigin(form); let messageManager = messageManagerFromWindow(win); // XXX Weak?? let requestData = { form: form }; let messageData = { formOrigin: formOrigin, actionOrigin: actionOrigin, options: options }; return this._sendRequest(messageManager, requestData, "RemoteLogins:findLogins", messageData); }, _autoCompleteSearchAsync(aSearchString, aPreviousResult, aElement, aRect) { let doc = aElement.ownerDocument; let form = LoginFormFactory.createFromField(aElement); let win = doc.defaultView; let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI); let actionOrigin = LoginUtils._getActionOrigin(form); let messageManager = messageManagerFromWindow(win); let remote = (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT); let previousResult = aPreviousResult ? { searchString: aPreviousResult.searchString, logins: LoginHelper.loginsToVanillaObjects(aPreviousResult.logins) } : null; let requestData = {}; let messageData = { formOrigin: formOrigin, actionOrigin: actionOrigin, searchString: aSearchString, previousResult: previousResult, rect: aRect, isSecure: InsecurePasswordUtils.isFormSecure(form), isPasswordField: aElement.type == "password", remote: remote }; return this._sendRequest(messageManager, requestData, "RemoteLogins:autoCompleteLogins", messageData); }, setupProgressListener(window) { if (!LoginHelper.formlessCaptureEnabled) { return; } try { let webProgress = window.QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIWebNavigation). QueryInterface(Ci.nsIDocShell). QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIWebProgress); webProgress.addProgressListener(observer, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT | Ci.nsIWebProgress.NOTIFY_LOCATION); } catch (ex) { // Ignore NS_ERROR_FAILURE if the progress listener was already added } }, onDOMFormHasPassword(event, window) { if (!event.isTrusted) { return; } let form = event.target; let formLike = LoginFormFactory.createFromForm(form); log("onDOMFormHasPassword:", form, formLike); this._fetchLoginsFromParentAndFillForm(formLike, window); }, onDOMInputPasswordAdded(event, window) { if (!event.isTrusted) { return; } let pwField = event.target; if (pwField.form) { // Fill is handled by onDOMFormHasPassword which is already throttled. return; } // Only setup the listener for formless inputs. // Capture within a