summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm')
-rw-r--r--mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm335
1 files changed, 335 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm b/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm
new file mode 100644
index 0000000000..f3483726fb
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm
@@ -0,0 +1,335 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = [
+ "GeckoViewAutocomplete",
+ "LoginEntry",
+ "SelectOption",
+];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { GeckoViewUtils } = ChromeUtils.import(
+ "resource://gre/modules/GeckoViewUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ EventDispatcher: "resource://gre/modules/Messaging.jsm",
+ GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "LoginInfo", () =>
+ Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo",
+ "init"
+ )
+);
+
+class LoginEntry {
+ constructor({
+ origin,
+ formActionOrigin,
+ httpRealm,
+ username,
+ password,
+ guid,
+ timeCreated,
+ timeLastUsed,
+ timePasswordChanged,
+ timesUsed,
+ }) {
+ this.origin = origin ?? null;
+ this.formActionOrigin = formActionOrigin ?? null;
+ this.httpRealm = httpRealm ?? null;
+ this.username = username ?? null;
+ this.password = password ?? null;
+
+ // Metadata.
+ this.guid = guid ?? null;
+ // TODO: Not supported by GV.
+ this.timeCreated = timeCreated ?? null;
+ this.timeLastUsed = timeLastUsed ?? null;
+ this.timePasswordChanged = timePasswordChanged ?? null;
+ this.timesUsed = timesUsed ?? null;
+ }
+
+ toLoginInfo() {
+ const info = new LoginInfo(
+ this.origin,
+ this.formActionOrigin,
+ this.httpRealm,
+ this.username,
+ this.password
+ );
+
+ // Metadata.
+ info.QueryInterface(Ci.nsILoginMetaInfo);
+ info.guid = this.guid;
+ info.timeCreated = this.timeCreated;
+ info.timeLastUsed = this.timeLastUsed;
+ info.timePasswordChanged = this.timePasswordChanged;
+ info.timesUsed = this.timesUsed;
+
+ return info;
+ }
+
+ static parse(aObj) {
+ const entry = new LoginEntry({});
+ Object.assign(entry, aObj);
+
+ return entry;
+ }
+
+ static fromLoginInfo(aInfo) {
+ const entry = new LoginEntry({});
+ entry.origin = aInfo.origin;
+ entry.formActionOrigin = aInfo.formActionOrigin;
+ entry.httpRealm = aInfo.httpRealm;
+ entry.username = aInfo.username;
+ entry.password = aInfo.password;
+
+ // Metadata.
+ aInfo.QueryInterface(Ci.nsILoginMetaInfo);
+ entry.guid = aInfo.guid;
+ entry.timeCreated = aInfo.timeCreated;
+ entry.timeLastUsed = aInfo.timeLastUsed;
+ entry.timePasswordChanged = aInfo.timePasswordChanged;
+ entry.timesUsed = aInfo.timesUsed;
+
+ return entry;
+ }
+}
+
+class SelectOption {
+ // Sync with Autocomplete.LoginSelectOption.Hint in Autocomplete.java.
+ static Hint = {
+ NONE: 0,
+ GENERATED: 1 << 0,
+ INSECURE_FORM: 1 << 1,
+ DUPLICATE_USERNAME: 1 << 2,
+ MATCHING_ORIGIN: 1 << 3,
+ };
+
+ constructor({ value, hint }) {
+ this.value = value ?? null;
+ this.hint = hint ?? SelectOption.Hint.NONE;
+ }
+}
+
+// Sync with Autocomplete.UsedField in Autocomplete.java.
+const UsedField = { PASSWORD: 1 };
+
+const GeckoViewAutocomplete = {
+ /**
+ * Delegates login entry fetching for the given domain to the attached
+ * LoginStorage GeckoView delegate.
+ *
+ * @param aDomain
+ * The domain string to fetch login entries for.
+ * @return {Promise}
+ * Resolves with an array of login objects or null.
+ * Rejected if no delegate is attached.
+ * Login object string properties:
+ * { guid, origin, formActionOrigin, httpRealm, username, password }
+ */
+ fetchLogins(aDomain) {
+ debug`fetchLogins for ${aDomain}`;
+
+ return EventDispatcher.instance.sendRequestForResult({
+ type: "GeckoView:Autocomplete:Fetch:Login",
+ domain: aDomain,
+ });
+ },
+
+ /**
+ * Delegates login entry saving to the attached LoginStorage GeckoView delegate.
+ * Call this when a new login entry or a new password for an existing login
+ * entry has been submitted.
+ *
+ * @param aLogin The {LoginEntry} to be saved.
+ */
+ onLoginSave(aLogin) {
+ debug`onLoginSave ${aLogin}`;
+
+ EventDispatcher.instance.sendRequest({
+ type: "GeckoView:Autocomplete:Save:Login",
+ login: aLogin,
+ });
+ },
+
+ /**
+ * Delegates login entry password usage to the attached LoginStorage GeckoView
+ * delegate.
+ * Call this when the password of an existing login entry, as returned by
+ * fetchLogins, has been used for autofill.
+ *
+ * @param aLogin The {LoginEntry} whose password was used.
+ */
+ onLoginPasswordUsed(aLogin) {
+ debug`onLoginUsed ${aLogin}`;
+
+ EventDispatcher.instance.sendRequest({
+ type: "GeckoView:Autocomplete:Used:Login",
+ usedFields: UsedField.PASSWORD,
+ login: aLogin,
+ });
+ },
+
+ _numActiveOnLoginSelect: 0,
+ /**
+ * Delegates login entry selection.
+ * Call this when there are multiple login entry option for a form to delegate
+ * the selection.
+ *
+ * @param aBrowser The browser instance the triggered the selection.
+ * @param aOptions The list of {SelectOption} depicting viable options.
+ */
+ onLoginSelect(aBrowser, aOptions) {
+ debug`onLoginSelect ${aOptions}`;
+
+ return new Promise((resolve, reject) => {
+ if (!aBrowser || !aOptions) {
+ debug`onLoginSelect Rejecting - no browser or options provided`;
+ reject();
+ return;
+ }
+
+ const prompt = new GeckoViewPrompter(aBrowser.ownerGlobal);
+ prompt.asyncShowPrompt(
+ {
+ type: "Autocomplete:Select:Login",
+ options: aOptions,
+ },
+ result => {
+ if (!result || !result.selection) {
+ reject();
+ return;
+ }
+
+ const option = new SelectOption({
+ value: LoginEntry.parse(result.selection.value),
+ hint: result.selection.hint,
+ });
+ resolve(option);
+ }
+ );
+ });
+ },
+
+ async delegateSelection({
+ browsingContext,
+ options,
+ inputElementIdentifier,
+ formOrigin,
+ }) {
+ debug`delegateSelection ${options}`;
+
+ if (!options.length) {
+ return;
+ }
+
+ let insecureHint = SelectOption.Hint.NONE;
+ let loginStyle = null;
+
+ const selectOptions = [];
+
+ for (const option of options) {
+ switch (option.style) {
+ case "insecureWarning": {
+ // We depend on the insecure warning to be the first option.
+ insecureHint = SelectOption.Hint.INSECURE_FORM;
+ break;
+ }
+ case "generatedPassword": {
+ const comment = JSON.parse(option.comment);
+ selectOptions.push(
+ new SelectOption({
+ value: new LoginEntry({
+ password: comment.generatedPassword,
+ }),
+ hint: SelectOption.Hint.GENERATED | insecureHint,
+ })
+ );
+ break;
+ }
+ case "login":
+ // Fallthrough.
+ case "loginWithOrigin": {
+ loginStyle = option.style;
+ const comment = JSON.parse(option.comment);
+
+ let hint = SelectOption.Hint.NONE | insecureHint;
+ if (comment.isDuplicateUsername) {
+ hint |= SelectOption.Hint.DUPLICATE_USERNAME;
+ }
+ if (comment.isOriginMatched) {
+ hint |= SelectOption.Hint.MATCHING_ORIGIN;
+ }
+
+ selectOptions.push(
+ new SelectOption({
+ value: LoginEntry.parse(comment.login),
+ hint,
+ })
+ );
+ break;
+ }
+ }
+ }
+
+ if (selectOptions.length < 1) {
+ debug`Abort delegateSelection - no valid options provided`;
+ return;
+ }
+
+ if (this._numActiveOnLoginSelect > 0) {
+ debug`Abort delegateSelection - there is already one delegation active`;
+ return;
+ }
+
+ ++this._numActiveOnLoginSelect;
+
+ const browser = browsingContext.top.embedderElement;
+ const selectedOption = await this.onLoginSelect(
+ browser,
+ selectOptions
+ ).catch(_ => {
+ debug`No GV delegate attached`;
+ });
+
+ --this._numActiveOnLoginSelect;
+
+ debug`delegateSelection selected option: ${selectedOption}`;
+ const selectedLogin = selectedOption?.value?.toLoginInfo();
+
+ if (!selectedLogin) {
+ debug`Abort delegateSelection - no login entry selected`;
+ return;
+ }
+
+ debug`delegateSelection - filling form`;
+
+ const actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
+
+ await actor.fillForm({
+ browser,
+ inputElementIdentifier,
+ loginFormOrigin: formOrigin,
+ login: selectedLogin,
+ style:
+ selectedOption.hint & SelectOption.Hint.GENERATED
+ ? "generatedPassword"
+ : loginStyle,
+ });
+
+ debug`delegateSelection - form filled`;
+ },
+};
+
+const { debug } = GeckoViewUtils.initLogging("GeckoViewAutocomplete");