mirror of
https://github.com/bitwarden/browser
synced 2026-02-05 19:23:19 +00:00
[WIP] [spike] Allows XPath to override input selection by domain.
This commit is contained in:
@@ -7,6 +7,12 @@ import {
|
||||
InlineMenuFillType,
|
||||
} from "../enums/autofill-overlay.enum";
|
||||
|
||||
export type DomainMatch = {
|
||||
domain: string;
|
||||
fieldType: AutofillFieldQualifierType;
|
||||
xpathResult: XPathResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a single field that is collected from the page source and is potentially autofilled.
|
||||
*/
|
||||
@@ -126,6 +132,8 @@ export default class AutofillField {
|
||||
|
||||
accountCreationFieldType?: InlineMenuAccountCreationFieldTypes;
|
||||
|
||||
domainMatch?: DomainMatch;
|
||||
|
||||
/**
|
||||
* used for totp multiline calculations
|
||||
*/
|
||||
|
||||
@@ -199,18 +199,30 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
autofillFieldData: AutofillField,
|
||||
pageDetails: AutofillPageDetails,
|
||||
) {
|
||||
console.log("SETUP OVERLAY LISTENERS");
|
||||
|
||||
console.log("currentlyInSandboxedIframe()", currentlyInSandboxedIframe());
|
||||
console.log(
|
||||
"this.formFieldElements.has(formFieldElement)",
|
||||
this.formFieldElements.has(formFieldElement),
|
||||
);
|
||||
console.log(
|
||||
"this.isIgnoredField(autofillFieldData, pageDetails)",
|
||||
this.isIgnoredField(autofillFieldData, pageDetails),
|
||||
);
|
||||
|
||||
if (
|
||||
currentlyInSandboxedIframe() ||
|
||||
this.formFieldElements.has(formFieldElement) ||
|
||||
this.isIgnoredField(autofillFieldData, pageDetails)
|
||||
this.isIgnoredField(autofillFieldData, pageDetails).result
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("IS IT HIDDEN");
|
||||
if (this.isHiddenField(formFieldElement, autofillFieldData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("SETUP ON QUALIFIED FIELD");
|
||||
await this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData);
|
||||
}
|
||||
|
||||
@@ -1048,16 +1060,26 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
private isIgnoredField(
|
||||
autofillFieldData: AutofillField,
|
||||
pageDetails: AutofillPageDetails,
|
||||
): boolean {
|
||||
if (this.ignoredFieldTypes.has(autofillFieldData.type)) {
|
||||
return true;
|
||||
): { result: boolean; message: string } {
|
||||
const message = "isIgnoredField";
|
||||
|
||||
console.log(message, { autofillFieldData });
|
||||
|
||||
const ignoredTypeResult = Array.from(this.ignoredFieldTypes).find(
|
||||
(v) => v === autofillFieldData.type,
|
||||
);
|
||||
if (ignoredTypeResult) {
|
||||
return {
|
||||
result: true,
|
||||
message: `${message} // field type is ignored type: ${ignoredTypeResult}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
this.inlineMenuFieldQualificationService.isFieldForLoginForm(autofillFieldData, pageDetails)
|
||||
) {
|
||||
void this.setQualifiedLoginFillType(autofillFieldData);
|
||||
return false;
|
||||
return { result: false, message: `${message} // field is for login form` };
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1068,7 +1090,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
)
|
||||
) {
|
||||
autofillFieldData.inlineMenuFillType = CipherType.Card;
|
||||
return false;
|
||||
return {
|
||||
result: false,
|
||||
message: `${message} // field is for credit card form & inline menu cards shown`,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1078,7 +1103,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
)
|
||||
) {
|
||||
this.setQualifiedAccountCreationFillType(autofillFieldData);
|
||||
return false;
|
||||
return { result: false, message: `${message} // field is for account creation form` };
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1089,10 +1114,16 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
)
|
||||
) {
|
||||
autofillFieldData.inlineMenuFillType = CipherType.Identity;
|
||||
return false;
|
||||
return {
|
||||
result: false,
|
||||
message: `${message} // field is for identity form & inline menu identities shown`,
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
return {
|
||||
result: true,
|
||||
message: `${message} // field ignored by default — not in any unignorable condition`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1101,6 +1132,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||
*/
|
||||
private async setQualifiedLoginFillType(autofillFieldData: AutofillField) {
|
||||
console.log("setQualifiedLoginFillType");
|
||||
autofillFieldData.inlineMenuFillType = CipherType.Login;
|
||||
autofillFieldData.showPasskeys = autofillFieldData.autoCompleteType.includes("webauthn");
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { AutofillFieldQualifier } from "../enums/autofill-field.enums";
|
||||
import AutofillField, { DomainMatch } from "../models/autofill-field";
|
||||
import AutofillForm from "../models/autofill-form";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
} from "./abstractions/collect-autofill-content.service";
|
||||
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
|
||||
import { DomQueryService } from "./abstractions/dom-query.service";
|
||||
import { P } from "@angular/cdk/portal-directives.d-BoG39gYN";
|
||||
|
||||
export class CollectAutofillContentService implements CollectAutofillContentServiceInterface {
|
||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||
@@ -168,6 +171,13 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
[...this.autofillFieldElements].sort((a, b) => a[1].elementNumber - b[1].elementNumber),
|
||||
);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns string (href)
|
||||
*/
|
||||
private getSafeDocumentUrl() {
|
||||
return (document.defaultView || globalThis).location.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats and returns the AutofillPageDetails object
|
||||
@@ -181,7 +191,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
): AutofillPageDetails {
|
||||
return {
|
||||
title: document.title,
|
||||
url: (document.defaultView || globalThis).location.href,
|
||||
url: this.getSafeDocumentUrl(),
|
||||
documentUrl: document.location.href,
|
||||
forms: autofillFormsData,
|
||||
fields: autofillFieldsData,
|
||||
@@ -398,7 +408,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
}
|
||||
|
||||
const fieldFormElement = (element as ElementWithOpId<FillableFormFieldElement>).form;
|
||||
const autofillField = {
|
||||
const autofillField: AutofillField = {
|
||||
...autofillFieldBase,
|
||||
...autofillFieldLabels,
|
||||
rel: this.getPropertyOrAttribute(element, "rel"),
|
||||
@@ -416,12 +426,101 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
"aria-disabled": this.getAttributeBoolean(element, "aria-disabled", true),
|
||||
"aria-haspopup": this.getAttributeBoolean(element, "aria-haspopup", true),
|
||||
"data-stripe": this.getPropertyOrAttribute(element, "data-stripe"),
|
||||
domainMatch: this.matchesXPathForDomain(element) || null,
|
||||
};
|
||||
|
||||
this.cacheAutofillFieldElement(index, element, autofillField);
|
||||
return autofillField;
|
||||
};
|
||||
|
||||
matchesXPathForDomain(element: ElementWithOpId<FormFieldElement>): DomainMatch {
|
||||
const url = this.getSafeDocumentUrl();
|
||||
const domain = Utils.getDomain(url);
|
||||
const matchers = [
|
||||
{
|
||||
domain: "cnn.com",
|
||||
xpathQualifiers: [
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.identityEmail,
|
||||
xpath: '//*[@id="login-email-input"]',
|
||||
fullxpath:
|
||||
"/html/body/div[1]/section[2]/section/section/section/div[2]/div[2]/div/form/div[1]/div/div[1]/input",
|
||||
},
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.password,
|
||||
xpath: '//*[@id="login-password-input"]',
|
||||
fullxpath:
|
||||
"/html/body/div[1]/section[2]/section/section/section/div[2]/div[2]/div/form/div[2]/div/div[1]/input",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
domain: "bestbuy.com",
|
||||
xpathQualifiers: [
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.identityEmail,
|
||||
fullxpath:
|
||||
"/html/body/div[1]/div/section/main/div[2]/div/div/div[1]/div/div/div/div/div/form/div[1]/div/input",
|
||||
},
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.password,
|
||||
fullxpath:
|
||||
"/html/body/div[1]/div/section/main/div[2]/div/div/div[1]/div/div/div/div/div/form/fieldset/fieldset/div[5]/div[2]/div/div/input",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
domain: "samsclub.com",
|
||||
xpathQualifiers: [
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.identityEmail,
|
||||
fullxpath: "/html/body/div[1]/div/div[1]/div/div/div[1]/form/div[1]/div/input",
|
||||
},
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.password,
|
||||
fullxpath: "/html/body/div[1]/div/div[1]/div/div/div[1]/form/div[3]/div/div/input",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
domain: "samsung.com",
|
||||
xpathQualifiers: [
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.identityEmail,
|
||||
fullxpath: "/html/body/div[1]/div/div[2]/div/div[2]/div[1]/form/div/div/input",
|
||||
},
|
||||
{
|
||||
qualifierType: AutofillFieldQualifier.password,
|
||||
fullxpath: "/html/body/div[1]/div/div[2]/div[1]/div[3]/div[1]/form/div/div/input",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const matcher of matchers) {
|
||||
if (matcher.domain === domain) {
|
||||
for (const xpathQualifier of matcher.xpathQualifiers) {
|
||||
const xpathResult = document.evaluate(
|
||||
xpathQualifier.fullxpath,
|
||||
document,
|
||||
null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||
);
|
||||
|
||||
const { singleNodeValue: node } = xpathResult;
|
||||
|
||||
if (node && node.isSameNode(element)) {
|
||||
return {
|
||||
domain,
|
||||
fieldType: xpathQualifier.qualifierType,
|
||||
xpathResult,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the autofill field element and its data.
|
||||
* Will not cache the element if the index is less than 0.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AutofillFieldQualifier } from "../enums/autofill-field.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils";
|
||||
@@ -225,6 +226,7 @@ export class InlineMenuFieldQualificationService
|
||||
* @param pageDetails - The details of the page that the field is on.
|
||||
*/
|
||||
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
||||
console.log("isFieldForLoginForm", field);
|
||||
/**
|
||||
* Totp inline menu is available only for premium users.
|
||||
*/
|
||||
@@ -236,17 +238,22 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
}
|
||||
console.log("isFieldForLoginForm // premium disabled or not totp field w password type");
|
||||
|
||||
const isCurrentPasswordField = this.isCurrentPasswordField(field);
|
||||
if (isCurrentPasswordField) {
|
||||
return this.isPasswordFieldForLoginForm(field, pageDetails);
|
||||
console.log(
|
||||
"isFieldForLoginForm // current password delegated to isPasswordFieldForLoginForm",
|
||||
this.isPasswordFieldForLoginForm(field, pageDetails),
|
||||
);
|
||||
return this.isPasswordFieldForLoginForm(field, pageDetails).result;
|
||||
}
|
||||
|
||||
const isUsernameField = this.isUsernameField(field);
|
||||
if (!isUsernameField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("// was username field, delegated to isUsernameFieldForLoginForm");
|
||||
return this.isUsernameFieldForLoginForm(field, pageDetails);
|
||||
}
|
||||
|
||||
@@ -353,10 +360,21 @@ export class InlineMenuFieldQualificationService
|
||||
* @param _pageDetails - Currently unused, will likely be required in the future
|
||||
*/
|
||||
isFieldForIdentityForm(field: AutofillField, _pageDetails: AutofillPageDetails): boolean {
|
||||
console.log(
|
||||
"isFieldForIdentityForm // isExcludedFieldType // ",
|
||||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet),
|
||||
);
|
||||
|
||||
if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log({
|
||||
isFieldForIdentityEmail: this.isFieldForIdentityEmail(field),
|
||||
fieldContainsAutocompleteValues: this.fieldContainsAutocompleteValues(
|
||||
field,
|
||||
this.identityAutocompleteValues,
|
||||
),
|
||||
});
|
||||
return (
|
||||
// Recognize explicit identity email fields (like id="new-email")
|
||||
this.isFieldForIdentityEmail(field) ||
|
||||
@@ -373,23 +391,35 @@ export class InlineMenuFieldQualificationService
|
||||
private isPasswordFieldForLoginForm(
|
||||
field: AutofillField,
|
||||
pageDetails: AutofillPageDetails,
|
||||
): boolean {
|
||||
): { result: boolean; message: string } {
|
||||
console.log({ field });
|
||||
const ns = "isPasswordFieldForLoginForm //";
|
||||
|
||||
if (field.domainMatch && field.domainMatch.fieldType === AutofillFieldQualifier.password) {
|
||||
return { result: true, message: `${ns} Matched domain specific setting.` };
|
||||
}
|
||||
|
||||
const parentForm = pageDetails.forms[field.form];
|
||||
|
||||
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a password field for a login form.
|
||||
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
|
||||
if (!parentForm) {
|
||||
return (
|
||||
pageDetails.fields.filter(this.isNewPasswordField).filter((f) => f.viewable).length === 0
|
||||
);
|
||||
const result =
|
||||
pageDetails.fields.filter(this.isNewPasswordField).filter((f) => f.viewable).length === 0;
|
||||
return {
|
||||
result,
|
||||
message: `${ns} Field had current password autocomplete value, result = 1+ viewable new password fields`,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
pageDetails.fields
|
||||
.filter(this.isNewPasswordField)
|
||||
.filter((f) => f.viewable && f.form === field.form).length === 0
|
||||
);
|
||||
return {
|
||||
result:
|
||||
pageDetails.fields
|
||||
.filter(this.isNewPasswordField)
|
||||
.filter((f) => f.viewable && f.form === field.form).length === 0,
|
||||
message: `${ns} Field had current password autocomplete value, result = 1+ viewable new password fields in form`,
|
||||
};
|
||||
}
|
||||
|
||||
const usernameFieldsInPageDetails = pageDetails.fields.filter(this.isUsernameField);
|
||||
@@ -398,7 +428,7 @@ export class InlineMenuFieldQualificationService
|
||||
// If a single username and a single password field exists on the page, we
|
||||
// should assume that this field is part of a login form.
|
||||
if (usernameFieldsInPageDetails.length === 1 && passwordFieldsInPageDetails.length === 1) {
|
||||
return true;
|
||||
return { result: true, message: `${ns} single username and single password found` };
|
||||
}
|
||||
|
||||
// If the field is not structured within a form, we need to identify if the field is present on
|
||||
@@ -407,21 +437,27 @@ export class InlineMenuFieldQualificationService
|
||||
// If no parent form is found, and multiple password fields are present, we should assume that
|
||||
// the passed field belongs to a user account creation form.
|
||||
if (passwordFieldsInPageDetails.length > 1) {
|
||||
return false;
|
||||
return {
|
||||
result: false,
|
||||
message: `${ns} no parent form and multiple password fields present`,
|
||||
};
|
||||
}
|
||||
|
||||
// If multiple username fields exist on the page, we should assume that
|
||||
// the provided field is part of an account creation form.
|
||||
const visibleUsernameFields = usernameFieldsInPageDetails.filter((f) => f.viewable);
|
||||
if (visibleUsernameFields.length > 1) {
|
||||
return false;
|
||||
return {
|
||||
result: false,
|
||||
message: `${ns} no parent form and multiple username fields found`,
|
||||
};
|
||||
}
|
||||
|
||||
// If a single username field or less is present on the page, then we can assume that the
|
||||
// provided field is for a login form. This will only be the case if the field does not
|
||||
// explicitly have its autocomplete attribute set to "off" or "false".
|
||||
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
const result = !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
return { result, message: `${ns} this field contains disabled values: ${result}` };
|
||||
}
|
||||
|
||||
// If the field has a form parent and there are multiple visible password fields
|
||||
@@ -430,7 +466,10 @@ export class InlineMenuFieldQualificationService
|
||||
(f) => f.form === field.form && f.viewable,
|
||||
);
|
||||
if (visiblePasswordFieldsInPageDetails.length > 1) {
|
||||
return false;
|
||||
return {
|
||||
result: false,
|
||||
message: `${ns} Field has form parent and multiple password fields present`,
|
||||
};
|
||||
}
|
||||
|
||||
// If the form has any visible username fields, we should treat the field as part of a login form
|
||||
@@ -438,12 +477,16 @@ export class InlineMenuFieldQualificationService
|
||||
(f) => f.form === field.form && f.viewable,
|
||||
);
|
||||
if (visibleUsernameFields.length > 0) {
|
||||
return true;
|
||||
return { result: true, message: `${ns} Form has visible username fields` };
|
||||
}
|
||||
|
||||
// If the field has a form parent and no username field exists and the field has an
|
||||
// autocomplete attribute set to "off" or "false", this is not a password field
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
const result = !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
return {
|
||||
result,
|
||||
message: `${ns} Field has form parent, no username, and contains disabled autocomplete values`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -456,6 +499,12 @@ export class InlineMenuFieldQualificationService
|
||||
field: AutofillField,
|
||||
pageDetails: AutofillPageDetails,
|
||||
): boolean {
|
||||
console.log({ field });
|
||||
|
||||
if (field.domainMatch && field.domainMatch.fieldType === "identityEmail") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the provided field is set with an autocomplete of "username", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a username field.
|
||||
|
||||
|
||||
@@ -501,6 +501,25 @@ export function isInvalidResponseStatusCode(statusCode: number) {
|
||||
* Determines if the current context is within a sandboxed iframe.
|
||||
*/
|
||||
export function currentlyInSandboxedIframe(): boolean {
|
||||
// console.log(
|
||||
// 'String(self.origin).toLowerCase() === "null"',
|
||||
// String(self.origin).toLowerCase() === "null",
|
||||
// self,
|
||||
// );
|
||||
// console.log(
|
||||
// 'globalThis.frameElement?.hasAttribute("sandbox")',
|
||||
// globalThis.frameElement?.hasAttribute("sandbox"),
|
||||
// globalThis.frameElement,
|
||||
// );
|
||||
// console.log(
|
||||
// 'globalThis.location.hostname === ""',
|
||||
// globalThis.location.hostname === "",
|
||||
// globalThis.location,
|
||||
// );
|
||||
// console.log({
|
||||
// globalThis,
|
||||
// });
|
||||
|
||||
return (
|
||||
String(self.origin).toLowerCase() === "null" ||
|
||||
globalThis.frameElement?.hasAttribute("sandbox") ||
|
||||
|
||||
Reference in New Issue
Block a user