[PM-10138] Inline menu not properly validating as the top-most element within mutation observer (#10292)

* [PM-10138] Inline menu not properly validating as the top-most element within mutation observer

* [PM-10138] Identity inline menu appearing for invalid field types

* [PM-10138] Identity inline menu appearing for invalid field types

* [PM-10138] Fixing an issue present with references to card and identity ciphers

* [PM-10138] Fixing issues with initialization of the inline menu

* [PM-10138] Fixing issues with initialization of the inline menu

* [PM-10138] Removing inclusion of file protocol when injecting scripts
This commit is contained in:
Cesar Gonzalez 2024-07-31 12:51:41 -05:00 committed by GitHub
parent ea362a9ed9
commit 85c8ff04a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 106 additions and 28 deletions

View File

@ -190,7 +190,6 @@ export type OverlayBackgroundExtensionMessageHandlers = {
}: BackgroundOnMessageHandlerParams) => void; }: BackgroundOnMessageHandlerParams) => void;
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
unlockCompleted: ({ message }: BackgroundMessageParam) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void;
doFullSync: () => void;
addedCipher: () => void; addedCipher: () => void;
addEditCipherSubmitted: () => void; addEditCipherSubmitted: () => void;
editedCipher: () => void; editedCipher: () => void;

View File

@ -2014,7 +2014,6 @@ describe("OverlayBackground", () => {
describe("extension messages that trigger an update of the inline menu ciphers", () => { describe("extension messages that trigger an update of the inline menu ciphers", () => {
const extensionMessages = [ const extensionMessages = [
"doFullSync",
"addedCipher", "addedCipher",
"addEditCipherSubmitted", "addEditCipherSubmitted",
"editedCipher", "editedCipher",

View File

@ -120,7 +120,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
unlockCompleted: ({ message }) => this.unlockCompleted(message), unlockCompleted: ({ message }) => this.unlockCompleted(message),
doFullSync: () => this.updateOverlayCiphers(),
addedCipher: () => this.updateOverlayCiphers(), addedCipher: () => this.updateOverlayCiphers(),
addEditCipherSubmitted: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(),
editedCipher: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(),
@ -273,7 +272,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "") await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "")
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
return cipherViews.concat(...this.cardAndIdentityCiphers); return this.cardAndIdentityCiphers
? cipherViews.concat(...this.cardAndIdentityCiphers)
: cipherViews;
} }
/** /**

View File

@ -399,6 +399,11 @@ describe("AutofillInlineMenuContentService", () => {
}); });
it("sets the z-index of to a lower value", async () => { it("sets the z-index of to a lower value", async () => {
autofillInlineMenuContentService["handlePersistentLastChildOverrideTimeout"] = setTimeout(
jest.fn(),
1000,
);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
await waitForIdleCallback(); await waitForIdleCallback();
@ -411,8 +416,9 @@ describe("AutofillInlineMenuContentService", () => {
}); });
globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild); globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); await autofillInlineMenuContentService["verifyInlineMenuIsNotObscured"](
await waitForIdleCallback(); persistentLastChild,
);
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", { expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button, overlayElement: AutofillOverlayElement.Button,
@ -425,8 +431,9 @@ describe("AutofillInlineMenuContentService", () => {
}); });
globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild); globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild);
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); await autofillInlineMenuContentService["verifyInlineMenuIsNotObscured"](
await waitForIdleCallback(); persistentLastChild,
);
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", { expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,

View File

@ -33,6 +33,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private bodyElementMutationObserver: MutationObserver; private bodyElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0; private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout;
private lastElementOverrides: WeakMap<Element, number> = new WeakMap(); private lastElementOverrides: WeakMap<Element, number> = new WeakMap();
private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = { private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
all: "initial", all: "initial",
@ -405,7 +406,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
} }
if (this.lastElementOverrides.get(lastChild) >= 3) { if (this.lastElementOverrides.get(lastChild) >= 3) {
await this.handlePersistentLastChildOverride(lastChild); this.handlePersistentLastChildOverride(lastChild);
return; return;
} }
@ -430,6 +431,26 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
globalThis.document.body.insertBefore(lastChild, this.buttonElement); globalThis.document.body.insertBefore(lastChild, this.buttonElement);
}; };
/**
* Handles the behavior of a persistent child element that is forcing itself to
* the bottom of the body element. This method will ensure that the inline menu
* elements are not obscured by the persistent child element.
*
* @param lastChild - The last child of the body element.
*/
private handlePersistentLastChildOverride(lastChild: Element) {
const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex);
if (lastChildZIndex >= 2147483647) {
(lastChild as HTMLElement).style.zIndex = "2147483646";
}
this.clearPersistentLastChildOverrideTimeout();
this.handlePersistentLastChildOverrideTimeout = globalThis.setTimeout(
() => this.verifyInlineMenuIsNotObscured(lastChild),
500,
);
}
/** /**
* Verifies if the last child of the body element is overlaying the inline menu elements. * Verifies if the last child of the body element is overlaying the inline menu elements.
* This is triggered when the last child of the body is being forced by some script to * This is triggered when the last child of the body is being forced by some script to
@ -437,12 +458,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* *
* @param lastChild - The last child of the body element. * @param lastChild - The last child of the body element.
*/ */
private async handlePersistentLastChildOverride(lastChild: Element) { private verifyInlineMenuIsNotObscured = async (lastChild: Element) => {
const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex);
if (lastChildZIndex >= 2147483647) {
(lastChild as HTMLElement).style.zIndex = "2147483646";
}
const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage( const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage(
"getAutofillInlineMenuPosition", "getAutofillInlineMenuPosition",
); );
@ -456,7 +472,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
if (!!list && this.elementAtCenterOfInlineMenuPosition(list) === lastChild) { if (!!list && this.elementAtCenterOfInlineMenuPosition(list) === lastChild) {
this.closeInlineMenu(); this.closeInlineMenu();
} }
} };
/** /**
* Returns the element present at the center of the inline menu position. * Returns the element present at the center of the inline menu position.
@ -470,6 +486,16 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
); );
} }
/**
* Clears the timeout that is used to verify that the last child of the body element
* is not overlaying the inline menu elements.
*/
private clearPersistentLastChildOverrideTimeout() {
if (this.handlePersistentLastChildOverrideTimeout) {
globalThis.clearTimeout(this.handlePersistentLastChildOverrideTimeout);
}
}
/** /**
* Identifies if the mutation observer is triggering excessive iterations. * Identifies if the mutation observer is triggering excessive iterations.
* Will trigger a blur of the most recently focused field and remove the * Will trigger a blur of the most recently focused field and remove the
@ -503,5 +529,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
*/ */
destroy() { destroy() {
this.closeInlineMenu(); this.closeInlineMenu();
this.clearPersistentLastChildOverrideTimeout();
} }
} }

View File

@ -377,6 +377,21 @@ describe("AutofillInlineMenuIframeService", () => {
autofillInlineMenuIframeService["ariaAlertElement"], autofillInlineMenuIframeService["ariaAlertElement"],
); );
}); });
it("resets the fade in timeout if it is set", () => {
autofillInlineMenuIframeService["fadeInTimeout"] = setTimeout(jest.fn, 100);
const styles = { top: "100px", left: "100px" };
jest.spyOn(autofillInlineMenuIframeService as any, "handleFadeInInlineMenuIframe");
sendPortMessage(portSpy, {
command: "updateAutofillInlineMenuPosition",
styles,
});
expect(
autofillInlineMenuIframeService["handleFadeInInlineMenuIframe"],
).toHaveBeenCalled();
});
}); });
it("updates the visibility of the iframe", () => { it("updates the visibility of the iframe", () => {

View File

@ -260,10 +260,13 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
return; return;
} }
const styles = this.fadeInTimeout ? Object.assign(position, { opacity: "0" }) : position;
this.updateElementStyles(this.iframe, styles);
if (this.fadeInTimeout) { if (this.fadeInTimeout) {
this.handleFadeInInlineMenuIframe(); this.handleFadeInInlineMenuIframe();
} }
this.updateElementStyles(this.iframe, position);
this.announceAriaAlert(); this.announceAriaAlert();
} }
@ -320,10 +323,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
*/ */
private handleFadeInInlineMenuIframe() { private handleFadeInInlineMenuIframe() {
this.clearFadeInTimeout(); this.clearFadeInTimeout();
this.fadeInTimeout = globalThis.setTimeout( this.fadeInTimeout = globalThis.setTimeout(() => {
() => this.updateElementStyles(this.iframe, { display: "block", opacity: "1" }), this.updateElementStyles(this.iframe, { display: "block", opacity: "1" });
10, this.clearFadeInTimeout();
); }, 10);
} }
/** /**

View File

@ -16,7 +16,7 @@ export class InlineMenuFieldQualificationService
implements InlineMenuFieldQualificationServiceInterface implements InlineMenuFieldQualificationServiceInterface
{ {
private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes); private excludedAutofillFieldTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
private usernameFieldTypes = new Set(["text", "email", "number", "tel"]); private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);
private usernameAutocompleteValue = "username"; private usernameAutocompleteValue = "username";
private emailAutocompleteValue = "email"; private emailAutocompleteValue = "email";
@ -244,6 +244,10 @@ export class InlineMenuFieldQualificationService
* @param pageDetails - The details of the page that the field is on. * @param pageDetails - The details of the page that the field is on.
*/ */
isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean { isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) {
return false;
}
if (!this.isUsernameField(field) && !this.isPasswordField(field)) { if (!this.isUsernameField(field) && !this.isPasswordField(field)) {
return false; return false;
} }
@ -286,6 +290,10 @@ export class InlineMenuFieldQualificationService
* @param _pageDetails - Currently unused, will likely be required in the future * @param _pageDetails - Currently unused, will likely be required in the future
*/ */
isFieldForIdentityForm(field: AutofillField, _pageDetails: AutofillPageDetails): boolean { isFieldForIdentityForm(field: AutofillField, _pageDetails: AutofillPageDetails): boolean {
if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) {
return false;
}
if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) { if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) {
return true; return true;
} }
@ -833,7 +841,7 @@ export class InlineMenuFieldQualificationService
isUsernameField = (field: AutofillField): boolean => { isUsernameField = (field: AutofillField): boolean => {
if ( if (
!this.usernameFieldTypes.has(field.type) || !this.usernameFieldTypes.has(field.type) ||
this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)
) { ) {
return false; return false;
} }
@ -852,7 +860,7 @@ export class InlineMenuFieldQualificationService
} }
return ( return (
!this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) && !this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) &&
this.keywordsFoundInFieldData(field, AutoFillConstants.EmailFieldNames) this.keywordsFoundInFieldData(field, AutoFillConstants.EmailFieldNames)
); );
}; };
@ -898,7 +906,7 @@ export class InlineMenuFieldQualificationService
const isInputPasswordType = field.type === "password"; const isInputPasswordType = field.type === "password";
if ( if (
(!isInputPasswordType && (!isInputPasswordType &&
this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)) || this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) ||
this.fieldHasDisqualifyingAttributeValue(field) this.fieldHasDisqualifyingAttributeValue(field)
) { ) {
return false; return false;

View File

@ -1245,6 +1245,13 @@ export default class MainBackground {
} }
} }
async updateOverlayCiphers() {
// overlayBackground null in popup only contexts
if (this.overlayBackground) {
await this.overlayBackground.updateOverlayCiphers();
}
}
/** /**
* Switch accounts to indicated userId -- null is no active user * Switch accounts to indicated userId -- null is no active user
*/ */
@ -1273,7 +1280,7 @@ export default class MainBackground {
if (userId == null) { if (userId == null) {
await this.refreshBadge(); await this.refreshBadge();
await this.refreshMenu(); await this.refreshMenu();
await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts await this.updateOverlayCiphers();
this.messagingService.send("goHome"); this.messagingService.send("goHome");
return; return;
} }
@ -1296,7 +1303,7 @@ export default class MainBackground {
this.messagingService.send("unlocked", { userId: userId }); this.messagingService.send("unlocked", { userId: userId });
await this.refreshBadge(); await this.refreshBadge();
await this.refreshMenu(); await this.refreshMenu();
await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts await this.updateOverlayCiphers();
await this.syncService.fullSync(false); await this.syncService.fullSync(false);
} }
} finally { } finally {
@ -1480,7 +1487,17 @@ export default class MainBackground {
* Temporary solution to handle initialization of the overlay background behind a feature flag. * Temporary solution to handle initialization of the overlay background behind a feature flag.
* Will be reverted to instantiation within the constructor once the feature flag is removed. * Will be reverted to instantiation within the constructor once the feature flag is removed.
*/ */
private async initOverlayAndTabsBackground() { async initOverlayAndTabsBackground() {
if (
this.popupOnlyContext ||
this.overlayBackground ||
this.tabsBackground ||
(await firstValueFrom(this.authService.activeAccountStatus$)) ===
AuthenticationStatus.LoggedOut
) {
return;
}
const inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag( const inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.InlineMenuPositioningImprovements, FeatureFlag.InlineMenuPositioningImprovements,
); );

View File

@ -200,6 +200,7 @@ export default class RuntimeBackground {
let item: LockedVaultPendingNotificationsData; let item: LockedVaultPendingNotificationsData;
if (msg.command === "loggedIn") { if (msg.command === "loggedIn") {
await this.main.initOverlayAndTabsBackground();
await this.sendBwInstalledMessageToVault(); await this.sendBwInstalledMessageToVault();
await this.autofillService.reloadAutofillScripts(); await this.autofillService.reloadAutofillScripts();
} }
@ -246,6 +247,7 @@ export default class RuntimeBackground {
await this.main.refreshMenu(); await this.main.refreshMenu();
}, 2000); }, 2000);
await this.configService.ensureConfigFetched(); await this.configService.ensureConfigFetched();
await this.main.updateOverlayCiphers();
} }
break; break;
case "openPopup": case "openPopup":