Bug 1691819 - [1.7] Extend the Autocomplete API to support credit cards. r=geckoview-reviewers,agi

Differential Revision: https://phabricator.services.mozilla.com/D106695
This commit is contained in:
Eugen Sawin 2021-04-12 21:57:15 +00:00
parent 33cc29d588
commit 434f165a88
9 changed files with 905 additions and 360 deletions

View File

@ -84,3 +84,5 @@ pref("toolkit.autocomplete.delegate", true);
// Android doesn't support the new sync storage yet, we will have our own in
// Bug 1625257.
pref("webextensions.storage.sync.kinto", true);
pref("browser.formfill.enable", true);

View File

@ -681,6 +681,26 @@ function startup() {
frameScript: "chrome://geckoview/content/GeckoViewMediaControlChild.js",
},
},
{
name: "GeckoViewAutocomplete",
onInit: {
actors: {
FormAutofill: {
parent: {
moduleURI: "resource://autofill/FormAutofillParent.jsm",
},
child: {
moduleURI: "resource://autofill/FormAutofillChild.jsm",
events: {
focusin: {},
DOMFormBeforeSubmit: {},
},
},
allFrames: true,
},
},
},
},
]);
if (!Services.appinfo.sessionHistoryInParent) {

View File

@ -24,7 +24,8 @@ import org.mozilla.geckoview.Autocomplete
import org.mozilla.geckoview.Autocomplete.LoginEntry
import org.mozilla.geckoview.Autocomplete.LoginSaveOption
import org.mozilla.geckoview.Autocomplete.LoginSelectOption
import org.mozilla.geckoview.Autocomplete.LoginStorageDelegate
import org.mozilla.geckoview.Autocomplete.SelectOption
import org.mozilla.geckoview.Autocomplete.StorageDelegate
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.util.Callbacks
@ -43,18 +44,18 @@ class AutocompleteTest : BaseSessionTest() {
"signon.autofillForms.http" to true))
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
val fetchHandled = GeckoResult<Void>()
sessionRule.addExternalDelegateDuringNextWait(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled(count = 1)
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -81,16 +82,16 @@ class AutocompleteTest : BaseSessionTest() {
"signon.userInputRequiredToCapture.enabled" to false))
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
sessionRule.addExternalDelegateDuringNextWait(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled(count = 1)
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -104,8 +105,8 @@ class AutocompleteTest : BaseSessionTest() {
mainSession.waitForPageStop()
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled(count = 0)
override fun onLoginSave(login: LoginEntry) {}
})
@ -152,11 +153,11 @@ class AutocompleteTest : BaseSessionTest() {
"signon.userInputRequiredToCapture.enabled" to false))
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
mainSession.loadTestPath(FORMS3_HTML_PATH)
@ -165,8 +166,8 @@ class AutocompleteTest : BaseSessionTest() {
val saveHandled = GeckoResult<Void>()
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginSave(login: LoginEntry) {
assertThat(
@ -229,11 +230,11 @@ class AutocompleteTest : BaseSessionTest() {
"signon.userInputRequiredToCapture.enabled" to false))
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
mainSession.loadTestPath(FORMS3_HTML_PATH)
@ -242,8 +243,8 @@ class AutocompleteTest : BaseSessionTest() {
val saveHandled = GeckoResult<Void>()
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginSave(login: LoginEntry) {
assertThat(
@ -314,11 +315,11 @@ class AutocompleteTest : BaseSessionTest() {
"signon.userInputRequiredToCapture.enabled" to false))
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
val saveHandled = GeckoResult<Void>()
@ -331,8 +332,8 @@ class AutocompleteTest : BaseSessionTest() {
val savedLogins = mutableListOf<LoginEntry>()
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -430,11 +431,11 @@ class AutocompleteTest : BaseSessionTest() {
"signon.userInputRequiredToCapture.enabled" to false))
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
val usedHandled = GeckoResult<Void>()
@ -454,8 +455,8 @@ class AutocompleteTest : BaseSessionTest() {
if (autofillEnabled) {
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -491,8 +492,8 @@ class AutocompleteTest : BaseSessionTest() {
})
} else {
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -547,11 +548,11 @@ class AutocompleteTest : BaseSessionTest() {
"signon.userInputRequiredToCapture.enabled" to false))
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
val user1 = "user1x"
@ -568,8 +569,8 @@ class AutocompleteTest : BaseSessionTest() {
val savedLogins = mutableListOf<LoginEntry>(savedLogin)
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -657,11 +658,11 @@ class AutocompleteTest : BaseSessionTest() {
// f. Ensure that onLoginUsed is called.
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
val user1 = "user1x"
@ -676,8 +677,8 @@ class AutocompleteTest : BaseSessionTest() {
val usedHandled = GeckoResult<Void>()
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -929,11 +930,11 @@ class AutocompleteTest : BaseSessionTest() {
// f. Ensure that onLoginUsed is not called.
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
val user1 = "user1x"
@ -949,8 +950,8 @@ class AutocompleteTest : BaseSessionTest() {
val selectHandled = GeckoResult<Void>()
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -1178,11 +1179,11 @@ class AutocompleteTest : BaseSessionTest() {
// a. Ensure onLoginSave is called with accordingly.
val runtime = sessionRule.runtime
val register = { delegate: LoginStorageDelegate ->
runtime.loginStorageDelegate = delegate
val register = { delegate: StorageDelegate ->
runtime.autocompleteStorageDelegate = delegate
}
val unregister = { _: LoginStorageDelegate ->
runtime.loginStorageDelegate = null
val unregister = { _: StorageDelegate ->
runtime.autocompleteStorageDelegate = null
}
val user1 = "user1x"
@ -1193,8 +1194,8 @@ class AutocompleteTest : BaseSessionTest() {
var numSelects = 0
sessionRule.addExternalDelegateUntilTestEnd(
LoginStorageDelegate::class, register, unregister,
object : LoginStorageDelegate {
StorageDelegate::class, register, unregister,
object : StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String)
: GeckoResult<Array<LoginEntry>>? {
@ -1244,7 +1245,7 @@ class AutocompleteTest : BaseSessionTest() {
assertThat(
"Hint should match",
option.hint,
equalTo(LoginSelectOption.Hint.GENERATED))
equalTo(SelectOption.Hint.GENERATED))
assertThat("Login should not be null", login, notNullValue())
assertThat(

View File

@ -62,7 +62,7 @@ import org.mozilla.gecko.util.GeckoBundle;
* <p>
* With the document parsed and the login input fields identified, GeckoView
* dispatches a
* <code>LoginStorageDelegate.onLoginFetch(&quot;example.com&quot;)</code>
* <code>StorageDelegate.onLoginFetch(&quot;example.com&quot;)</code>
* request to fetch logins for the given domain.
* </p>
* <p>
@ -82,14 +82,14 @@ import org.mozilla.gecko.util.GeckoBundle;
* <h3>Update API</h3>
* <p>
* When the user submits some login input fields, GeckoView dispatches another
* <code>LoginStorageDelegate.onLoginFetch(&quot;example.com&quot;)</code>
* <code>StorageDelegate.onLoginFetch(&quot;example.com&quot;)</code>
* request to check whether the submitted login exists or whether it's a new or
* updated login entry.
* </p>
* <p>
* If the submitted login is already contained as-is in the collection returned
* by <code>onLoginFetch</code>, then GeckoView dispatches
* <code>LoginStorageDelegate.onLoginUsed</code> with the submitted login
* <code>StorageDelegate.onLoginUsed</code> with the submitted login
* entry.
* </p>
* <p>
@ -120,12 +120,12 @@ import org.mozilla.gecko.util.GeckoBundle;
* <p>
* The login entry returned in a confirmed save prompt is used to request for
* saving in the runtime delegate via
* <code>LoginStorageDelegate.onLoginSave(login)</code>.
* <code>StorageDelegate.onLoginSave(login)</code>.
* If the app has already stored the entry during the prompt request handling,
* it may ignore this storage saving request.
* </p>
*
* <br>@see GeckoRuntime#setLoginStorageDelegate
* <br>@see GeckoRuntime#setAutocompleteStorageDelegate
* <br>@see GeckoSession#setPromptDelegate
* <br>@see GeckoSession.PromptDelegate#onLoginSave
* <br>@see GeckoSession.PromptDelegate#onLoginSelect
@ -136,6 +136,172 @@ public class Autocomplete {
protected Autocomplete() {}
/**
* Holds credit card information for a specific entry.
*/
public static class CreditCard {
private static final String GUID_KEY = "guid";
private static final String NAME_KEY = "name";
private static final String NUMBER_KEY = "number";
private static final String EXP_MONTH_KEY = "expMonth";
private static final String EXP_YEAR_KEY = "expYear";
/**
* The unique identifier for this login entry.
*/
public final @Nullable String guid;
/**
* The full name as it appears on the credit card.
*/
public final @NonNull String name;
/**
* The credit card number.
*/
public final @NonNull String number;
/**
* The expiration month.
*/
public final @NonNull String expirationMonth;
/**
* The expiration year.
*/
public final @NonNull String expirationYear;
// For tests only.
@AnyThread
protected CreditCard() {
guid = null;
name = "";
number = "";
expirationMonth = "";
expirationYear = "";
}
@AnyThread
/* package */ CreditCard(final @NonNull GeckoBundle bundle) {
guid = bundle.getString(GUID_KEY);
name = bundle.getString(NAME_KEY, "");
number = bundle.getString(NUMBER_KEY, "");
expirationMonth = bundle.getString(EXP_MONTH_KEY, "");
expirationYear = bundle.getString(EXP_YEAR_KEY, "");
}
@Override
@AnyThread
public String toString() {
final StringBuilder builder = new StringBuilder("CreditCard {");
builder
.append("guid=").append(guid)
.append(", name=").append(name)
.append(", number=").append(number)
.append(", expirationMonth=").append(expirationMonth)
.append(", expirationYear=").append(expirationYear)
.append("}");
return builder.toString();
}
@AnyThread
/* package */ @NonNull GeckoBundle toBundle() {
final GeckoBundle bundle = new GeckoBundle(7);
bundle.putString(GUID_KEY, guid);
bundle.putString(NAME_KEY, name);
bundle.putString(NUMBER_KEY, number);
bundle.putString(EXP_MONTH_KEY, expirationMonth);
bundle.putString(EXP_YEAR_KEY, expirationYear);
return bundle;
}
public static class Builder {
private final GeckoBundle mBundle;
@AnyThread
/* package */ Builder(final @NonNull GeckoBundle bundle) {
mBundle = new GeckoBundle(bundle);
}
@AnyThread
@SuppressWarnings("checkstyle:javadocmethod")
public Builder() {
mBundle = new GeckoBundle(7);
}
/**
* Finalize the {@link CreditCard} instance.
*
* @return The {@link CreditCard} instance.
*/
@AnyThread
public @NonNull CreditCard build() {
return new CreditCard(mBundle);
}
/**
* Set the unique identifier for this credit card entry.
*
* @param guid The unique identifier string.
* @return This {@link Builder} instance.
*/
@AnyThread
public @NonNull Builder guid(final @Nullable String guid) {
mBundle.putString(GUID_KEY, guid);
return this;
}
/**
* Set the name for this credit card entry.
*
* @param name The full name as it appears on the credit card.
* @return This {@link Builder} instance.
*/
@AnyThread
public @NonNull Builder name(final @Nullable String name) {
mBundle.putString(NAME_KEY, name);
return this;
}
/**
* Set the number for this credit card entry.
*
* @param number The credit card number string.
* @return This {@link Builder} instance.
*/
@AnyThread
public @NonNull Builder number(final @Nullable String number) {
mBundle.putString(NUMBER_KEY, number);
return this;
}
/**
* Set the expiration month for this credit card entry.
*
* @param expMonth The expiration month string.
* @return This {@link Builder} instance.
*/
@AnyThread
public @NonNull Builder expirationMonth(final @Nullable String expMonth) {
mBundle.putString(EXP_MONTH_KEY, expMonth);
return this;
}
/**
* Set the expiration year for this credit card entry.
*
* @param expYear The expiration year string.
* @return This {@link Builder} instance.
*/
@AnyThread
public @NonNull Builder expirationYear(final @Nullable String expYear) {
mBundle.putString(EXP_YEAR_KEY, expYear);
return this;
}
}
}
/**
* Holds login information for a specific entry.
*/
@ -337,7 +503,7 @@ public class Autocomplete {
// Sync with UsedField in GeckoViewAutocomplete.jsm.
/**
* Possible login entry field types for {@link LoginStorageDelegate#onLoginUsed}.
* Possible login entry field types for {@link StorageDelegate#onLoginUsed}.
*/
public static class UsedField {
/**
@ -353,9 +519,9 @@ public class Autocomplete {
* Login storage events include login entry requests for autofill and
* autocompletion of login input fields.
* This delegate is attached to the runtime via
* {@link GeckoRuntime#setLoginStorageDelegate}.
* {@link GeckoRuntime#setAutocompleteStorageDelegate}.
*/
public interface LoginStorageDelegate {
public interface StorageDelegate {
/**
* Request login entries for a given domain.
* While processing the web document, we have identified elements
@ -374,6 +540,21 @@ public class Autocomplete {
return null;
}
/**
* Request credit card entries.
* While processing the web document, we have identified elements
* resembling credit card input fields suitable for autofill.
* We will attempt to match the provided credit card information to the
* identified input fields.
*
* @return A {@link GeckoResult} that completes with an array of
* {@link CreditCard} containing the existing credit cards.
*/
@UiThread
default @Nullable GeckoResult<CreditCard[]> onCreditCardFetch() {
return null;
}
/**
* Request saving or updating of the given login entry.
* This is triggered by confirming a
@ -401,6 +582,13 @@ public class Autocomplete {
@LSUsedField final int usedFields) {}
}
/**
* @deprecated This API has been replaced by {@link StorageDelegate} and
* will be removed in GeckoView 93.
*/
@Deprecated @DeprecationSchedule(version = 93, id = "login-storage")
public interface LoginStorageDelegate extends StorageDelegate {}
/**
* Abstract base class for Autocomplete options.
* Extended by {@link Autocomplete.SaveOption} and
@ -428,44 +616,10 @@ public class Autocomplete {
* Extended by {@link Autocomplete.LoginSaveOption}.
*/
public abstract static class SaveOption<T> extends Option<T> {
@SuppressWarnings("checkstyle:javadocmethod")
public SaveOption(final @NonNull T value, final int hint) {
super(value, hint);
}
}
/**
* Abstract base class for saving options.
* Extended by {@link Autocomplete.LoginSelectOption}.
*/
public abstract static class SelectOption<T> extends Option<T> {
@SuppressWarnings("checkstyle:javadocmethod")
public SelectOption(
final @NonNull T value,
final int hint) {
super(value, hint);
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder("SelectOption {");
builder
.append("value=").append(value).append(", ")
.append("hint=").append(hint)
.append("}");
return builder.toString();
}
}
/**
* Holds information required to process login saving requests.
*/
public static class LoginSaveOption extends SaveOption<LoginEntry> {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
value = { Hint.NONE, Hint.GENERATED, Hint.LOW_CONFIDENCE })
/* package */ @interface LoginSaveHint {}
/* package */ @interface SaveOptionHint {}
/**
* Hint types for login saving requests.
@ -491,6 +645,84 @@ public class Autocomplete {
protected Hint() {}
}
@SuppressWarnings("checkstyle:javadocmethod")
public SaveOption(
final @NonNull T value,
final @SaveOptionHint int hint) {
super(value, hint);
}
}
/**
* Abstract base class for saving options.
* Extended by {@link Autocomplete.LoginSelectOption}.
*/
public abstract static class SelectOption<T> extends Option<T> {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
value = { Hint.NONE, Hint.GENERATED, Hint.INSECURE_FORM,
Hint.DUPLICATE_USERNAME, Hint.MATCHING_ORIGIN })
/* package */ @interface SelectOptionHint {}
/**
* Hint types for selection requests.
*/
public static class Hint {
public static final int NONE = 0;
/**
* Auto-generated password.
* A new password-only login entry containing a secure generated
* password.
*/
public static final int GENERATED = 1 << 0;
/**
* Insecure context.
* The form or transmission mechanics are considered insecure.
* This is the case when the form is served via http or submitted
* insecurely.
*/
public static final int INSECURE_FORM = 1 << 1;
/**
* The username is shared with another login entry.
* There are multiple login entries in the options that share the
* same username. You may have to disambiguate the login entry,
* e.g., using the last date of modification and its origin.
*/
public static final int DUPLICATE_USERNAME = 1 << 2;
/**
* The login entry's origin matches the login form origin.
* The login was saved from the same origin it is being requested
* for, rather than for a subdomain.
*/
public static final int MATCHING_ORIGIN = 1 << 3;
}
@SuppressWarnings("checkstyle:javadocmethod")
public SelectOption(
final @NonNull T value,
final @SelectOptionHint int hint) {
super(value, hint);
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder("SelectOption {");
builder
.append("value=").append(value).append(", ")
.append("hint=").append(hint)
.append("}");
return builder.toString();
}
}
/**
* Holds information required to process login saving requests.
*/
public static class LoginSaveOption extends SaveOption<LoginEntry> {
/**
* Construct a login save option.
*
@ -499,7 +731,7 @@ public class Autocomplete {
*/
/* package */ LoginSaveOption(
final @NonNull LoginEntry value,
final @LoginSaveHint int hint) {
final @SaveOptionHint int hint) {
super(value, hint);
}
@ -525,49 +757,6 @@ public class Autocomplete {
* Holds information required to process login selection requests.
*/
public static class LoginSelectOption extends SelectOption<LoginEntry> {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
value = { Hint.NONE, Hint.GENERATED, Hint.INSECURE_FORM,
Hint.DUPLICATE_USERNAME, Hint.MATCHING_ORIGIN })
/* package */ @interface LoginSelectHint {}
/**
* Hint types for login selection requests.
*/
public static class Hint {
public static final int NONE = 0;
/**
* Auto-generated password.
* A new password-only login entry containing a secure generated
* password.
*/
public static final int GENERATED = 1 << 0;
/**
* Insecure login.
* The login form or transmission mechanics are considered insecure.
* This is the case when the form is served via http or submitted
* insecurely.
*/
public static final int INSECURE_FORM = 1 << 1;
/**
* The username is shared with another login entry.
* There are multiple login entries in the options that share the
* same username. You may have to disambiguate the login entry,
* e.g., using the last date of modification and its origin.
*/
public static final int DUPLICATE_USERNAME = 1 << 2;
/**
* The login entry's origin matches the login form origin.
* The login was saved from the same origin it is being requested
* for, rather than for a subdomain.
*/
public static final int MATCHING_ORIGIN = 1 << 3;
}
/**
* Construct a login select option.
*
@ -576,7 +765,7 @@ public class Autocomplete {
*/
/* package */ LoginSelectOption(
final @NonNull LoginEntry value,
final @LoginSelectHint int hint) {
final @SelectOptionHint int hint) {
super(value, hint);
}
@ -606,24 +795,87 @@ public class Autocomplete {
}
}
/* package */ final static class LoginStorageProxy implements BundleEventListener {
private static final String LOGTAG = "LoginStorageProxy";
/**
* Holds information required to process credit card selection requests.
*/
public static class CreditCardSelectOption extends SelectOption<CreditCard> {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
value = { Hint.NONE, Hint.INSECURE_FORM })
/* package */ @interface CreditCardSelectHint {}
/**
* Hint types for credit card selection requests.
*/
public static class Hint {
public static final int NONE = 0;
/**
* Insecure context.
* The form or transmission mechanics are considered insecure.
* This is the case when the form is served via http or submitted
* insecurely.
*/
public static final int INSECURE_FORM = 1 << 1;
}
/**
* Construct a credit card select option.
*
* @param value The {@link LoginEntry} credit card entry selection option.
* @param hint The {@link Hint} detailing the type of the option.
*/
/* package */ CreditCardSelectOption(
final @NonNull CreditCard value,
final @CreditCardSelectHint int hint) {
super(value, hint);
}
/**
* Construct a credit card select option.
*
* @param value The {@link CreditCard} credit card entry selection option.
*/
public CreditCardSelectOption(final @NonNull CreditCard value) {
this(value, Hint.NONE);
}
/* package */ static @NonNull CreditCardSelectOption fromBundle(
final @NonNull GeckoBundle bundle) {
final int hint = bundle.getInt("hint");
final CreditCard value = new CreditCard(bundle.getBundle("value"));
return new CreditCardSelectOption(value, hint);
}
@Override
/* package */ @NonNull GeckoBundle toBundle() {
final GeckoBundle bundle = new GeckoBundle(2);
bundle.putBundle(VALUE_KEY, value.toBundle());
bundle.putInt(HINT_KEY, hint);
return bundle;
}
}
/* package */ final static class StorageProxy implements BundleEventListener {
private static final String FETCH_LOGIN_EVENT =
"GeckoView:Autocomplete:Fetch:Login";
private static final String FETCH_CREDIT_CARD_EVENT =
"GeckoView:Autocomplete:Fetch:CreditCard";
private static final String SAVE_LOGIN_EVENT =
"GeckoView:Autocomplete:Save:Login";
private static final String USED_LOGIN_EVENT =
"GeckoView:Autocomplete:Used:Login";
private @Nullable LoginStorageDelegate mDelegate;
private @Nullable StorageDelegate mDelegate;
public LoginStorageProxy() {}
public StorageProxy() {}
private void registerListener() {
EventDispatcher.getInstance().registerUiThreadListener(
this,
FETCH_LOGIN_EVENT,
FETCH_CREDIT_CARD_EVENT,
SAVE_LOGIN_EVENT,
USED_LOGIN_EVENT);
}
@ -632,22 +884,28 @@ public class Autocomplete {
EventDispatcher.getInstance().unregisterUiThreadListener(
this,
FETCH_LOGIN_EVENT,
FETCH_CREDIT_CARD_EVENT,
SAVE_LOGIN_EVENT,
USED_LOGIN_EVENT);
}
public synchronized void setDelegate(
final @Nullable LoginStorageDelegate delegate) {
if (mDelegate == null && delegate != null) {
registerListener();
} else if (mDelegate != null && delegate == null) {
final @Nullable StorageDelegate delegate) {
if (mDelegate == delegate) {
return;
}
if (mDelegate != null) {
unregisterListener();
}
mDelegate = delegate;
if (mDelegate != null) {
registerListener();
}
}
public synchronized @Nullable LoginStorageDelegate getDelegate() {
public synchronized @Nullable StorageDelegate getDelegate() {
return mDelegate;
}
@ -662,7 +920,7 @@ public class Autocomplete {
if (mDelegate == null) {
if (callback != null) {
callback.sendError("No LoginStorageDelegate attached");
callback.sendError("No StorageDelegate attached");
}
return;
}
@ -691,6 +949,29 @@ public class Autocomplete {
return loginBundles;
}));
} else if (FETCH_CREDIT_CARD_EVENT.equals(event)) {
final GeckoResult<Autocomplete.CreditCard[]> result =
mDelegate.onCreditCardFetch();
if (result == null) {
callback.sendSuccess(new GeckoBundle[0]);
return;
}
callback.resolveTo(result.map(creditCards -> {
if (creditCards == null) {
return new GeckoBundle[0];
}
// This is a one-liner with streams (API level 24).
final GeckoBundle[] creditCardBundles =
new GeckoBundle[creditCards.length];
for (int i = 0; i < creditCards.length; ++i) {
creditCardBundles[i] = creditCards[i].toBundle();
}
return creditCardBundles;
}));
} else if (SAVE_LOGIN_EVENT.equals(event)) {
final GeckoBundle loginBundle = message.getBundle("login");
final LoginEntry login = new LoginEntry(loginBundle);

View File

@ -173,13 +173,13 @@ public final class GeckoRuntime implements Parcelable {
private final WebExtensionController mWebExtensionController;
private WebPushController mPushController;
private final ContentBlockingController mContentBlockingController;
private final Autocomplete.LoginStorageProxy mLoginStorageProxy;
private final Autocomplete.StorageProxy mAutocompleteStorageProxy;
private final ProfilerController mProfilerController;
private GeckoRuntime() {
mWebExtensionController = new WebExtensionController(this);
mContentBlockingController = new ContentBlockingController();
mLoginStorageProxy = new Autocomplete.LoginStorageProxy();
mAutocompleteStorageProxy = new Autocomplete.StorageProxy();
mProfilerController = new ProfilerController();
if (sRuntime != null) {
@ -566,28 +566,63 @@ public final class GeckoRuntime implements Parcelable {
}
/**
* Set the {@link Autocomplete.LoginStorageDelegate} instance on this runtime.
* This delegate is required for handling login storage requests.
* Set the {@link Autocomplete.StorageDelegate} instance on this runtime.
* This delegate is required for handling autocomplete storage requests.
*
* @param delegate The {@link Autocomplete.LoginStorageDelegate} handling login storage
* requests.
* @param delegate The {@link Autocomplete.StorageDelegate} handling
* autocomplete storage requests.
*/
@UiThread
public void setAutocompleteStorageDelegate(
final @Nullable Autocomplete.StorageDelegate delegate) {
ThreadUtils.assertOnUiThread();
mAutocompleteStorageProxy.setDelegate(delegate);
}
/**
* Set the {@link Autocomplete.LoginStorageDelegate} instance on this runtime.
* This delegate is required for handling autocomplete storage requests.
*
* @param delegate The {@link Autocomplete.LoginStorageDelegate} handling
* autocomplete storage requests.
*
* @deprecated This API has been replaced by
* {@link #setAutocompleteStorageDelegate} and
* will be removed in GeckoView 93.
*/
@Deprecated @DeprecationSchedule(version = 93, id = "login-storage")
@UiThread
public void setLoginStorageDelegate(
final @Nullable Autocomplete.LoginStorageDelegate delegate) {
ThreadUtils.assertOnUiThread();
mLoginStorageProxy.setDelegate(delegate);
mAutocompleteStorageProxy.setDelegate(delegate);
}
/**
* Get the {@link Autocomplete.StorageDelegate} instance set on this runtime.
*
* @return The {@link Autocomplete.StorageDelegate} set on this runtime.
*/
@UiThread
public @Nullable Autocomplete.StorageDelegate getAutocompleteStorageDelegate() {
ThreadUtils.assertOnUiThread();
return mAutocompleteStorageProxy.getDelegate();
}
/**
* Get the {@link Autocomplete.LoginStorageDelegate} instance set on this runtime.
*
* @return The {@link Autocomplete.LoginStorageDelegate} set on this runtime.
*
* @deprecated This API has been replaced by
* {@link #getAutocompleteStorageDelegate} and
* will be removed in GeckoView 93.
*/
@Deprecated @DeprecationSchedule(version = 93, id = "login-storage")
@UiThread
public @Nullable Autocomplete.LoginStorageDelegate getLoginStorageDelegate() {
ThreadUtils.assertOnUiThread();
return mLoginStorageProxy.getDelegate();
return (Autocomplete.LoginStorageDelegate)mAutocompleteStorageProxy.getDelegate();
}
@UiThread

View File

@ -294,7 +294,7 @@ public final class GeckoRuntimeSettings extends RuntimeSettings {
/**
* Set whether login forms should be filled automatically if only one
* viable candidate is provided via
* {@link Autocomplete.LoginStorageDelegate#onLoginFetch onLoginFetch}.
* {@link Autocomplete.StorageDelegate#onLoginFetch onLoginFetch}.
*
* @param enabled A flag determining whether login autofill should be
* enabled.
@ -1181,7 +1181,7 @@ public final class GeckoRuntimeSettings extends RuntimeSettings {
/**
* Set whether login forms should be filled automatically if only one
* viable candidate is provided via
* {@link Autocomplete.LoginStorageDelegate#onLoginFetch onLoginFetch}.
* {@link Autocomplete.StorageDelegate#onLoginFetch onLoginFetch}.
*
* @param enabled A flag determining whether login autofill should be
* enabled.

View File

@ -2860,7 +2860,29 @@ public class GeckoSession {
new PromptDelegate.AutocompleteRequest<>(options);
res = delegate.onLoginSelect(session, request);
break;
}
case "Autocomplete:Select:CreditCard": {
final GeckoBundle[] optionBundles =
message.getBundleArray("options");
if (optionBundles == null) {
break;
}
final Autocomplete.CreditCardSelectOption[] options =
new Autocomplete.CreditCardSelectOption[optionBundles.length];
for (int i = 0; i < options.length; ++i) {
options[i] = Autocomplete.CreditCardSelectOption.fromBundle(
optionBundles[i]);
}
final PromptDelegate.AutocompleteRequest
<Autocomplete.CreditCardSelectOption> request =
new PromptDelegate.AutocompleteRequest<>(options);
res = delegate.onCreditCardSelect(session, request);
break;
}
default: {
@ -5143,7 +5165,7 @@ public class GeckoSession {
*
* Confirm the request with an {@link Autocomplete.Option}
* to trigger a
* {@link Autocomplete.LoginStorageDelegate#onLoginSave} request
* {@link Autocomplete.StorageDelegate#onLoginSave} request
* to save the given selection.
* The confirmed selection may be an entry out of the request's
* options, a modified option, or a freshly created login entry.
@ -5184,6 +5206,34 @@ public class GeckoSession {
request) {
return null;
}
/**
* Handle a credit card selection prompt request.
* This is triggered by the user focusing on a credit card input field.
*
* @param session The {@link GeckoSession} that triggered the request.
* @param request The {@link AutocompleteRequest} containing the request
* details.
*
* @return A {@link GeckoResult} resolving to a {@link PromptResponse}
*
* Confirm the request with an {@link Autocomplete.Option}
* to let GeckoView fill out the credit card forms with the given
* selection details.
* The confirmed selection may be an entry out of the request's
* options, a modified option, or a freshly created credit
* card entry.
*
* Dismiss the request to deny autocompletion for the detected
* form.
*/
@UiThread
default @Nullable GeckoResult<PromptResponse> onCreditCardSelect(
@NonNull final GeckoSession session,
@NonNull final AutocompleteRequest<Autocomplete.CreditCardSelectOption>
request) {
return null;
}
}
/**

View File

@ -7,6 +7,8 @@
const EXPORTED_SYMBOLS = [
"GeckoViewAutocomplete",
"LoginEntry",
"CreditCard",
"Address",
"SelectOption",
];
@ -106,8 +108,189 @@ class LoginEntry {
}
}
class Address {
constructor({
name,
givenName,
additionalName,
familyName,
organization,
streetAddress,
addressLevel1,
addressLevel2,
addressLevel3,
postalCode,
country,
tel,
email,
guid,
timeCreated,
timeLastUsed,
timeLastModified,
timesUsed,
version,
}) {
this.name = name ?? null;
this.givenName = givenName ?? null;
this.additionalName = additionalName ?? null;
this.familyName = familyName ?? null;
this.organization = organization ?? null;
this.streetAddress = streetAddress ?? null;
this.addressLevel1 = addressLevel1 ?? null;
this.addressLevel2 = addressLevel2 ?? null;
this.addressLevel3 = addressLevel3 ?? null;
this.postalCode = postalCode ?? null;
this.country = country ?? null;
this.tel = tel ?? null;
this.email = email ?? null;
// Metadata.
this.guid = guid ?? null;
// TODO: Not supported by GV.
this.timeCreated = timeCreated ?? null;
this.timeLastUsed = timeLastUsed ?? null;
this.timeLastModified = timeLastModified ?? null;
this.timesUsed = timesUsed ?? null;
this.version = version ?? null;
}
isValid() {
return (
(this.name ?? this.givenName ?? this.familyName) !== null &&
this.streetAddress !== null &&
this.postalCode !== null
);
}
static fromGecko(aObj) {
return new Address({
version: aObj.version,
name: aObj.name,
givenName: aObj["given-name"],
additionalName: aObj["additional-name"],
familyName: aObj["family-name"],
organization: aObj.organization,
streetAddress: aObj["street-address"],
addressLevel1: aObj["address-level1"],
addressLevel2: aObj["address-level2"],
addressLevel3: aObj["address-level3"],
postalCode: aObj["postal-code"],
country: aObj.country,
tel: aObj.tel,
email: aObj.email,
guid: aObj.guid,
timeCreated: aObj.timeCreated,
timeLastUsed: aObj.timeLastUsed,
timeLastModified: aObj.timeLastModified,
timesUsed: aObj.timesUsed,
});
}
static parse(aObj) {
const entry = new Address({});
Object.assign(entry, aObj);
return entry;
}
toGecko() {
return {
version: this.version,
name: this.name,
"given-name": this.givenName,
"additional-name": this.additionalName,
"family-name": this.familyName,
organization: this.organization,
"street-address": this.streetAddress,
"address-level1": this.addressLevel1,
"address-level2": this.addressLevel2,
"address-level3": this.addressLevel3,
"postal-code": this.postalCode,
country: this.country,
tel: this.tel,
email: this.email,
guid: this.guid,
};
}
}
class CreditCard {
constructor({
name,
number,
expMonth,
expYear,
type,
guid,
timeCreated,
timeLastUsed,
timeLastModified,
timesUsed,
version,
}) {
this.name = name ?? null;
this.number = number ?? null;
this.expMonth = expMonth ?? null;
this.expYear = expYear ?? null;
this.type = type ?? null;
// Metadata.
this.guid = guid ?? null;
// TODO: Not supported by GV.
this.timeCreated = timeCreated ?? null;
this.timeLastUsed = timeLastUsed ?? null;
this.timeLastModified = timeLastModified ?? null;
this.timesUsed = timesUsed ?? null;
this.version = version ?? null;
}
isValid() {
return (
this.name !== null &&
this.number !== null &&
this.expMonth !== null &&
this.expYear !== null
);
}
static fromGecko(aObj) {
return new CreditCard({
version: aObj.version,
name: aObj["cc-name"],
number: aObj["cc-number"],
expMonth: aObj["cc-exp-month"],
expYear: aObj["cc-exp-year"],
type: aObj["cc-type"],
guid: aObj.guid,
timeCreated: aObj.timeCreated,
timeLastUsed: aObj.timeLastUsed,
timeLastModified: aObj.timeLastModified,
timesUsed: aObj.timesUsed,
});
}
static parse(aObj) {
const entry = new CreditCard({});
Object.assign(entry, aObj);
return entry;
}
toGecko() {
return {
version: this.version,
"cc-name": this.name,
"cc-number": this.number,
"cc-exp-month": this.expMonth,
"cc-exp-year": this.expYear,
"cc-type": this.type,
guid: this.guid,
};
}
}
class SelectOption {
// Sync with Autocomplete.LoginSelectOption.Hint in Autocomplete.java.
// Sync with Autocomplete.SelectOption.Hint in Autocomplete.java.
static Hint = {
NONE: 0,
GENERATED: 1 << 0,
@ -160,7 +343,9 @@ const GeckoViewAutocomplete = {
fetchCreditCards() {
debug`fetchCreditCards`;
return Promise.resolve(null);
return EventDispatcher.instance.sendRequestForResult({
type: "GeckoView:Autocomplete:Fetch:CreditCard",
});
},
/**
@ -189,6 +374,11 @@ const GeckoViewAutocomplete = {
*/
onCreditCardSave(aCreditCard) {
debug`onLoginSave ${aCreditCard}`;
EventDispatcher.instance.sendRequest({
type: "GeckoView:Autocomplete:Save:CreditCard",
creditCard: aCreditCard,
});
},
/**
@ -235,7 +425,8 @@ const GeckoViewAutocomplete = {
});
},
_numActiveOnLoginSelect: 0,
_numActiveSelections: 0,
/**
* Delegates login entry selection.
* Call this when there are multiple login entry option for a form to delegate
@ -276,6 +467,60 @@ const GeckoViewAutocomplete = {
});
},
/**
* Delegates credit card entry selection.
* Call this when there are multiple credit card 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.
*/
onCreditCardSelect(aBrowser, aOptions) {
debug`onCreditCardSelect ${aOptions}`;
return new Promise((resolve, reject) => {
if (!aBrowser || !aOptions) {
debug`onCreditCardSelect Rejecting - no browser or options provided`;
reject();
return;
}
const prompt = new GeckoViewPrompter(aBrowser.ownerGlobal);
prompt.asyncShowPrompt(
{
type: "Autocomplete:Select:CreditCard",
options: aOptions,
},
result => {
if (!result || !result.selection) {
reject();
return;
}
const option = new SelectOption({
value: CreditCard.parse(result.selection.value),
hint: result.selection.hint,
});
resolve(option);
}
);
});
},
/**
* Delegates address entry selection.
* Call this when there are multiple address 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.
*/
onAddressSelect(aBrowser, aOptions) {
debug`onAddressSelect ${aOptions}`;
return Promise.resolve(null);
},
async delegateSelection({
browsingContext,
options,
@ -291,6 +536,8 @@ const GeckoViewAutocomplete = {
let insecureHint = SelectOption.Hint.NONE;
let loginStyle = null;
// TODO: Replace this string with more robust mechanics.
let selectionType = null;
const selectOptions = [];
for (const option of options) {
@ -301,6 +548,7 @@ const GeckoViewAutocomplete = {
break;
}
case "generatedPassword": {
selectionType = "login";
const comment = JSON.parse(option.comment);
selectOptions.push(
new SelectOption({
@ -315,6 +563,7 @@ const GeckoViewAutocomplete = {
case "login":
// Fallthrough.
case "loginWithOrigin": {
selectionType = "login";
loginStyle = option.style;
const comment = JSON.parse(option.comment);
@ -334,6 +583,32 @@ const GeckoViewAutocomplete = {
);
break;
}
case "autofill-profile": {
const comment = JSON.parse(option.comment);
debug`delegateSelection ${comment}`;
const creditCard = CreditCard.fromGecko(comment);
const address = Address.fromGecko(comment);
if (creditCard.isValid()) {
selectionType = "creditCard";
selectOptions.push(
new SelectOption({
value: creditCard,
hint: insecureHint,
})
);
} else if (address.isValid()) {
selectionType = "address";
selectOptions.push(
new SelectOption({
value: address,
hint: insecureHint,
})
);
}
break;
}
default:
debug`delegateSelection - ignoring unknown option style ${option.style}`;
}
}
@ -342,45 +617,79 @@ const GeckoViewAutocomplete = {
return;
}
if (this._numActiveOnLoginSelect > 0) {
if (this._numActiveSelections > 0) {
debug`Abort delegateSelection - there is already one delegation active`;
return;
}
++this._numActiveOnLoginSelect;
++this._numActiveSelections;
let selectedOption = null;
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;
if (selectionType === "login") {
selectedOption = await this.onLoginSelect(browser, selectOptions).catch(
_ => {
debug`No GV delegate attached`;
}
);
} else if (selectionType === "creditCard") {
selectedOption = await this.onCreditCardSelect(
browser,
selectOptions
).catch(_ => {
debug`No GV delegate attached`;
});
} else if (selectionType === "address") {
selectedOption = await this.onAddressSelect(browser, selectOptions).catch(
_ => {
debug`No GV delegate attached`;
}
);
}
debug`delegateSelection - filling form`;
--this._numActiveSelections;
const actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
debug`delegateSelection selected option: ${selectedOption}`;
await actor.fillForm({
browser,
inputElementIdentifier,
loginFormOrigin: formOrigin,
login: selectedLogin,
style:
selectedOption.hint & SelectOption.Hint.GENERATED
? "generatedPassword"
: loginStyle,
});
if (selectionType === "login") {
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,
});
} else if (selectionType === "creditCard") {
const selectedCreditCard = selectedOption?.value?.toGecko();
const actor = browsingContext.currentWindowGlobal.getActor(
"FormAutofill"
);
actor.sendAsyncMessage("FormAutofill:FillForm", selectedCreditCard);
} else if (selectionType === "address") {
const selectedAddress = selectedOption?.value?.toGecko();
const actor = browsingContext.currentWindowGlobal.getActor(
"FormAutofill"
);
actor.sendAsyncMessage("FormAutofill:FillForm", selectedAddress);
}
debug`delegateSelection - form filled`;
},

View File

@ -1,153 +0,0 @@
/* 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 = ["GeckoViewLoginStorage", "LoginEntry"];
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",
});
XPCOMUtils.defineLazyGetter(this, "LoginInfo", () =>
Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
"nsILoginInfo",
"init"
)
);
class LoginEntry {
constructor() {
this.origin = null;
this.formActionOrigin = null;
this.httpRealm = null;
this.username = null;
this.password = null;
// Metadata.
this.guid = null;
// TODO: Not supported by GV.
this.timeCreated = null;
this.timeLastUsed = null;
this.timePasswordChanged = null;
this.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 fromBundle(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;
}
}
// Sync with LoginStorage.Delegate.UsedField in LoginStorage.java.
const UsedField = { PASSWORD: 1 };
const GeckoViewLoginStorage = {
/**
* 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:LoginStorage:Fetch",
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:LoginStorage:Save",
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:LoginStorage:Used",
usedFields: UsedField.PASSWORD,
login: aLogin,
});
},
};
const { debug } = GeckoViewUtils.initLogging("GeckoViewLoginStorage");