1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

[PM-2481] [PM-3465] Integrate Shadow DOM Support Into Autofill v2 and Optimize Collection of Page Details (special thanks to @RafaelKr) (#6141)

* [PM-2100] Create Unit Test Suite for Autofill.service.ts

* [PM-2100] Finishing out tests for the getFormsWithPasswordFields method

* [PM-2100] Implementing tests for the doAutofill method within the autofill service

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service

* [PM-2100] Finishing implementation of doAutoFill method within autofill service

* [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service

* [PM-2100] Working through tests for generateFillScript

* split up autofill.ts, first pass

* remove modification tracking comments

* lessen and localize eslint disables

* additional typing and formatting

* update autofill v2 with PR #5364 changes (update/i18n confirm dialogs)

* update autofill v2 with PR #4155 changes (add autofill support for textarea)

Co-Authored-By: Manuel <mr-manuel@outlook.it>

* move commonly used string values to constants

* ts cleanup

* [PM-2100] Finalizing generateFillScript method testing

* [PM-2100] Starting implementation of generateLoginFillScript

* [PM-2100] Working through tests for generateLoginFillScript

* [PM-2100] Finalizing generateLoginFillScript method testing

* [PM-2130] Starting work to re-architect autofillv2.ts

* [PM-2130] Starting work to re-architect autofillv2.ts

* [PM-2130] Working through autofill collect method

* [PM-2130] Marking Removal of documentUUID as dead code

* [PM-2130] Refining the implementation of collect and moving broken out utils back into class implementation

* [PM-2130] Applying small refactors to AutofillCollect

* [PM-2130] Refining the implementation of getAutofillFieldLabelTag to help with readability of the method

* [PM-2130] Implementing jest tests for AutofillCollect methods

* [PM-2130] Refining implementation for AutofillCollect

* [PM-2200] Unit tests for autofill content script utilities with slight refactors (#5544)

* add unit tests for urlNotSecure

* add test coverage command

* add unit tests for canSeeElementToStyle

* canSeeElementToStyle should not return true if `animateTheFilling` or `currentEl` is false

* add tests for selectAllFromDoc and getElementByOpId

* clean up getElementByOpId

* address some typing issues

* add tests for setValueForElementByEvent, setValueForElement, and doSimpleSetByQuery

* clean up setValueForElement and setValueForElementByEvent

* more typescript cleanup

* add tests for doClickByOpId and touchAllPasswordFields

* add tests for doFocusByOpId and doClickByQuery

* misc fill cleanup

* move functions between collect and fill utils and replace getElementForOPID for duplicate getElementByOpId

* add tests for isKnownTag and isElementVisible

* rename addProp and remove redundant focusElement in favor of doFocusElement

* cleanup

* fix checkNodeType

* add tests for shiftForLeftLabel

* clean up and rename checkNodeType, isKnownTag, and shiftForLeftLabel

* add tests for getFormElements

* clean up getFormElements

* add tests for getElementAttrValue, getElementValue, getSelectElementOptions, getLabelTop, and queryDoc

* clean up and rename queryDoc to queryDocument

* misc cleanup and rename getElementAttrValue to getPropertyOrAttribute

* rebase cleanup

* prettier formatting

* [PM-2130] Fixing linting issues

* [PM-2130] Fixing linting issues

* [PM-2130] Migrating implementation for collect methods and tests for those methods into AutofillCollect context

* [PM-2130] Migrating getPropertyOrAttribute method from utils to AutofillCollect

* [PM-2130] Continuing migration of methods from collect utils into AutofillCollect

* [PM-2130] Rework of isViewable method to better handle behavior for how we identify if an element is currently within the viewport

* [PM-2130] Filling out implementation of autofill-insert

* [PM-2130] Refining AutofillInsert

* [PM-2130] Implementing jest tests for AutofillCollect methods and breaking out visibility related logic to a separate service

* [PM-2130] Fixing jest tests for AutofillCollect

* [PM-2130] Fixing jest tests for AutofillInit

* [PM-2130] Adjusting how the AutofillFieldVisibilityService class is used in AutofillCollect

* [PM-2130] Working through AutofillInsert implementation

* [PM-2130] Migrating methods from fill.ts to AutofillInsert

* [PM-2130] Migrating methods from fill.ts to AutofillInsert

* [PM-2130] Applying fix for IntersectionObserver when triggering behavior in Safari and fixing issue with how we trigger an input event shortly after filling in a field

* [PM-2130] Refactoring AutofillCollect to service CollectAutofillContentService

* [PM-2130] Refactoring AutofillInsert to service InsertAutofillContentService

* [PM-2130] Further organization of implementation

* [PM-2130] Filling out missing jest test for AutofillInit.fillForm method

* [PM-2130] Migrating the last of the collect jest tests to InsertAutofillContentService

* [PM-2130] Further refactoring of elements including typing information

* [PM-2130] Implementing jest tests for InsertAutofillContentService

* [PM-2130] Implementing jest tests for InsertAutofillContentService

* [PM-2130] Organization and refactoring of methods within InsertAutofillContent

* [PM-2130] Implementation of jest tests for InsertAutofillContentService

* [PM-2130] Implementation of Jest Test for IntertAutofillContentService

* [PM-2130] Finalizing migration of methods and jest tests from util files into Autofill serivces

* [PM-2130] Cleaning up dead code comments

* [PM-2130] Removing unnecessary constants

* [PM-2130] Finalizing jest tests for InsertAutofillContentService

* [PM-2130] Refactoring FieldVisibiltyService to DomElementVisibilityService to allow service to act in a more general manner

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Breaking out the callback method used to resolve the IntersectionObserver promise

* [PM-2100] Removing unnecessary jest config file

* [PM-2100] Fixing jest tests based on changes implemented within PM-2130

* [PM-2100] Fixing autofill mocks

* [PM-2100] Fixing AutofillService jest tests

* [PM-2100] Handling missing tests within coverage of AutofillService

* [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript

* [PM-2100] Writing tests for AutofillService.generateCardFillScript

* [PM-2100] Finalizing tests for AutofillService.generateCardFillScript

* [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR

* [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript

* [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript

* [PM-2100] Implementing tests for AutofillService

* [PM-2130] Adding a comment explaining a fix for Safari

* [PM-2130] Adding a comment explaining a fix for Safari

* [PM-2100] Implementing tests for AutofillService.loadPasswordFields

* [PM-2100] Implementing tests for AutofillService.findUsernameField

* [PM-2100] Implementing tests for AutofillService.findTotpField

* [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch

* [PM-2100] Finalizing tests for AutofillService

* [PM-2747] Add Support for Feature Flag of Autofill Version

* [PM-2747] Adding Support for Manifest v3 within the implementation

* [PM-2747] Modifying how the feature flag for autofill is named

* [PM-2747] Modifying main.background.ts to load the ConfigApiService correctly

* [PM-2747] Refactoring trigger of autofill scripts to be a simple immediately invoked function

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Removal of jest transform declaration

* [PM-2130] Applying changes required for PM-2762 to implementation, and ensuring jest tests exist to validate the behavior

* [PM-2747] Modifying how we inject the autofill scripts to ensure we are injecting into all frames within a page

* [PM-2130] Removing usage of IntersectionObserver when identifying element visibility due to broken interactions with React Components

* [PM-2130] Fixing issue found when attempting to capture the elementAtCenterPoint in determining file visibility

* Refining implementation for ShadowDOM fix

* update tests

* cleanup

* [PM-3285] Autofill v2 Feature Branch

* [PM-2100] Create Unit Test Suite for autofill.service.ts (#5371)

* [PM-2100] Create Unit Test Suite for Autofill.service.ts

* [PM-2100] Finishing out tests for the getFormsWithPasswordFields method

* [PM-2100] Implementing tests for the doAutofill method within the autofill service

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service

* [PM-2100] Finishing implementation of doAutoFill method within autofill service

* [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service

* [PM-2100] Working through tests for generateFillScript

* [PM-2100] Finalizing generateFillScript method testing

* [PM-2100] Starting implementation of generateLoginFillScript

* [PM-2100] Working through tests for generateLoginFillScript

* [PM-2100] Finalizing generateLoginFillScript method testing

* [PM-2100] Removing unnecessary jest config file

* [PM-2100] Fixing jest tests based on changes implemented within PM-2130

* [PM-2100] Fixing autofill mocks

* [PM-2100] Fixing AutofillService jest tests

* [PM-2100] Handling missing tests within coverage of AutofillService

* [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript

* [PM-2100] Writing tests for AutofillService.generateCardFillScript

* [PM-2100] Finalizing tests for AutofillService.generateCardFillScript

* [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR

* [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript

* [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript

* [PM-2100] Implementing tests for AutofillService

* [PM-2100] Implementing tests for AutofillService.loadPasswordFields

* [PM-2100] Implementing tests for AutofillService.findUsernameField

* [PM-2100] Implementing tests for AutofillService.findTotpField

* [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch

* [PM-2100] Finalizing tests for AutofillService

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Removal of jest transform declaration

* [PM-2747] Applying a fix for a race condition that can occur when loading the notification bar and autofiller script login

* [PM-2747] Reverting removal of autofill npm action. Now this will force usage of autofill-v2 regardless of whether a feature flag is set or not

* [PM-2747] Fixing logic error incorporated when merging in master

* [PM-2130] Fixing issue with autofill service unit tests

* [PM-2130] Fixing issue with autofill service unit tests

* [PM-2747] Fixing issue present with notification bar merge

* [PM-2130] Fixing test test for when we need to handle a password reprompt

* [PM-3465] Optimization of CollectPageDetails Message within Autofill

* [PM-3465] Implementing caching for CollectPage details call

* [PM-3465] Implementing caching for CollectPage details call

* [PM-3465] Implementing method for ensuring that getPageDetails is not called when no fields appear within a frame

* [PM-3465] Implementing Mutation Observer to handle updating autofill fields when DOM updates

* [PM-2747] Fixing wording for webpack script

* [PM-2130] - Audit, Modularize, and Refactor Core autofill.js File (#5453)

* split up autofill.ts, first pass

* remove modification tracking comments

* lessen and localize eslint disables

* additional typing and formatting

* update autofill v2 with PR #5364 changes (update/i18n confirm dialogs)

* update autofill v2 with PR #4155 changes (add autofill support for textarea)

Co-Authored-By: Manuel <mr-manuel@outlook.it>

* move commonly used string values to constants

* ts cleanup

* [PM-2130] Starting work to re-architect autofillv2.ts

* [PM-2130] Starting work to re-architect autofillv2.ts

* [PM-2130] Working through autofill collect method

* [PM-2130] Marking Removal of documentUUID as dead code

* [PM-2130] Refining the implementation of collect and moving broken out utils back into class implementation

* [PM-2130] Applying small refactors to AutofillCollect

* [PM-2130] Refining the implementation of getAutofillFieldLabelTag to help with readability of the method

* [PM-2130] Implementing jest tests for AutofillCollect methods

* [PM-2130] Refining implementation for AutofillCollect

* [PM-2200] Unit tests for autofill content script utilities with slight refactors (#5544)

* add unit tests for urlNotSecure

* add test coverage command

* add unit tests for canSeeElementToStyle

* canSeeElementToStyle should not return true if `animateTheFilling` or `currentEl` is false

* add tests for selectAllFromDoc and getElementByOpId

* clean up getElementByOpId

* address some typing issues

* add tests for setValueForElementByEvent, setValueForElement, and doSimpleSetByQuery

* clean up setValueForElement and setValueForElementByEvent

* more typescript cleanup

* add tests for doClickByOpId and touchAllPasswordFields

* add tests for doFocusByOpId and doClickByQuery

* misc fill cleanup

* move functions between collect and fill utils and replace getElementForOPID for duplicate getElementByOpId

* add tests for isKnownTag and isElementVisible

* rename addProp and remove redundant focusElement in favor of doFocusElement

* cleanup

* fix checkNodeType

* add tests for shiftForLeftLabel

* clean up and rename checkNodeType, isKnownTag, and shiftForLeftLabel

* add tests for getFormElements

* clean up getFormElements

* add tests for getElementAttrValue, getElementValue, getSelectElementOptions, getLabelTop, and queryDoc

* clean up and rename queryDoc to queryDocument

* misc cleanup and rename getElementAttrValue to getPropertyOrAttribute

* rebase cleanup

* prettier formatting

* [PM-2130] Fixing linting issues

* [PM-2130] Fixing linting issues

* [PM-2130] Migrating implementation for collect methods and tests for those methods into AutofillCollect context

* [PM-2130] Migrating getPropertyOrAttribute method from utils to AutofillCollect

* [PM-2130] Continuing migration of methods from collect utils into AutofillCollect

* [PM-2130] Rework of isViewable method to better handle behavior for how we identify if an element is currently within the viewport

* [PM-2130] Filling out implementation of autofill-insert

* [PM-2130] Refining AutofillInsert

* [PM-2130] Implementing jest tests for AutofillCollect methods and breaking out visibility related logic to a separate service

* [PM-2130] Fixing jest tests for AutofillCollect

* [PM-2130] Fixing jest tests for AutofillInit

* [PM-2130] Adjusting how the AutofillFieldVisibilityService class is used in AutofillCollect

* [PM-2130] Working through AutofillInsert implementation

* [PM-2130] Migrating methods from fill.ts to AutofillInsert

* [PM-2130] Migrating methods from fill.ts to AutofillInsert

* [PM-2130] Applying fix for IntersectionObserver when triggering behavior in Safari and fixing issue with how we trigger an input event shortly after filling in a field

* [PM-2130] Refactoring AutofillCollect to service CollectAutofillContentService

* [PM-2130] Refactoring AutofillInsert to service InsertAutofillContentService

* [PM-2130] Further organization of implementation

* [PM-2130] Filling out missing jest test for AutofillInit.fillForm method

* [PM-2130] Migrating the last of the collect jest tests to InsertAutofillContentService

* [PM-2130] Further refactoring of elements including typing information

* [PM-2130] Implementing jest tests for InsertAutofillContentService

* [PM-2130] Implementing jest tests for InsertAutofillContentService

* [PM-2130] Organization and refactoring of methods within InsertAutofillContent

* [PM-2130] Implementation of jest tests for InsertAutofillContentService

* [PM-2130] Implementation of Jest Test for IntertAutofillContentService

* [PM-2130] Finalizing migration of methods and jest tests from util files into Autofill serivces

* [PM-2130] Cleaning up dead code comments

* [PM-2130] Removing unnecessary constants

* [PM-2130] Finalizing jest tests for InsertAutofillContentService

* [PM-2130] Refactoring FieldVisibiltyService to DomElementVisibilityService to allow service to act in a more general manner

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Breaking out the callback method used to resolve the IntersectionObserver promise

* [PM-2130] Adding a comment explaining a fix for Safari

* [PM-2130] Adding a comment explaining a fix for Safari

* [PM-2130] Applying changes required for PM-2762 to implementation, and ensuring jest tests exist to validate the behavior

* [PM-2130] Removing usage of IntersectionObserver when identifying element visibility due to broken interactions with React Components

* [PM-2130] Fixing issue found when attempting to capture the elementAtCenterPoint in determining file visibility

* [PM-2100] Create Unit Test Suite for autofill.service.ts (#5371)

* [PM-2100] Create Unit Test Suite for Autofill.service.ts

* [PM-2100] Finishing out tests for the getFormsWithPasswordFields method

* [PM-2100] Implementing tests for the doAutofill method within the autofill service

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service

* [PM-2100] Finishing implementation of doAutoFill method within autofill service

* [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service

* [PM-2100] Working through tests for generateFillScript

* [PM-2100] Finalizing generateFillScript method testing

* [PM-2100] Starting implementation of generateLoginFillScript

* [PM-2100] Working through tests for generateLoginFillScript

* [PM-2100] Finalizing generateLoginFillScript method testing

* [PM-2100] Removing unnecessary jest config file

* [PM-2100] Fixing jest tests based on changes implemented within PM-2130

* [PM-2100] Fixing autofill mocks

* [PM-2100] Fixing AutofillService jest tests

* [PM-2100] Handling missing tests within coverage of AutofillService

* [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript

* [PM-2100] Writing tests for AutofillService.generateCardFillScript

* [PM-2100] Finalizing tests for AutofillService.generateCardFillScript

* [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR

* [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript

* [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript

* [PM-2100] Implementing tests for AutofillService

* [PM-2100] Implementing tests for AutofillService.loadPasswordFields

* [PM-2100] Implementing tests for AutofillService.findUsernameField

* [PM-2100] Implementing tests for AutofillService.findTotpField

* [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch

* [PM-2100] Finalizing tests for AutofillService

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Removal of jest transform declaration

* [PM-2130] Fixing issue with autofill service unit tests

* [PM-2130] Fixing issue with autofill service unit tests

* [PM-2130] Fixing test test for when we need to handle a password reprompt

---------

Co-authored-by: Manuel <mr-manuel@outlook.it>
Co-authored-by: Cesar Gonzalez <cgonzalez@bitwarden.com>
Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>

* [PM-2747] Finanlizing implementation of attribute updates on cached values

* [PM-3465] Finalizing implementation of mutation observer behavior and CollectPageDetails optimization

* [PM-3465] Adding jest tests for introduced functionality

* [PM-3465] Finalizing jest tests and comments within implementation

* [PM-3465] Removing a TODO by incorrporating a method for deep querying for a password field element

* [PM-3465] Removing a TODO by incorrporating a method for deep querying for a password field element

* [PM-3285] Migrating Changes from PM-1407 into autofill v2 refactor implementation

* [PM-2747] Addressing stylistic changes requested from code review

* [PM-2747] Add Support for Feature Flag of Autofill Version (#5695)

* [PM-2100] Create Unit Test Suite for Autofill.service.ts

* [PM-2100] Finishing out tests for the getFormsWithPasswordFields method

* [PM-2100] Implementing tests for the doAutofill method within the autofill service

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service

* [PM-2100] Finishing implementation of doAutoFill method within autofill service

* [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service

* [PM-2100] Working through tests for generateFillScript

* split up autofill.ts, first pass

* remove modification tracking comments

* lessen and localize eslint disables

* additional typing and formatting

* update autofill v2 with PR #5364 changes (update/i18n confirm dialogs)

* update autofill v2 with PR #4155 changes (add autofill support for textarea)

Co-Authored-By: Manuel <mr-manuel@outlook.it>

* move commonly used string values to constants

* ts cleanup

* [PM-2100] Finalizing generateFillScript method testing

* [PM-2100] Starting implementation of generateLoginFillScript

* [PM-2100] Working through tests for generateLoginFillScript

* [PM-2100] Finalizing generateLoginFillScript method testing

* [PM-2130] Starting work to re-architect autofillv2.ts

* [PM-2130] Starting work to re-architect autofillv2.ts

* [PM-2130] Working through autofill collect method

* [PM-2130] Marking Removal of documentUUID as dead code

* [PM-2130] Refining the implementation of collect and moving broken out utils back into class implementation

* [PM-2130] Applying small refactors to AutofillCollect

* [PM-2130] Refining the implementation of getAutofillFieldLabelTag to help with readability of the method

* [PM-2130] Implementing jest tests for AutofillCollect methods

* [PM-2130] Refining implementation for AutofillCollect

* [PM-2200] Unit tests for autofill content script utilities with slight refactors (#5544)

* add unit tests for urlNotSecure

* add test coverage command

* add unit tests for canSeeElementToStyle

* canSeeElementToStyle should not return true if `animateTheFilling` or `currentEl` is false

* add tests for selectAllFromDoc and getElementByOpId

* clean up getElementByOpId

* address some typing issues

* add tests for setValueForElementByEvent, setValueForElement, and doSimpleSetByQuery

* clean up setValueForElement and setValueForElementByEvent

* more typescript cleanup

* add tests for doClickByOpId and touchAllPasswordFields

* add tests for doFocusByOpId and doClickByQuery

* misc fill cleanup

* move functions between collect and fill utils and replace getElementForOPID for duplicate getElementByOpId

* add tests for isKnownTag and isElementVisible

* rename addProp and remove redundant focusElement in favor of doFocusElement

* cleanup

* fix checkNodeType

* add tests for shiftForLeftLabel

* clean up and rename checkNodeType, isKnownTag, and shiftForLeftLabel

* add tests for getFormElements

* clean up getFormElements

* add tests for getElementAttrValue, getElementValue, getSelectElementOptions, getLabelTop, and queryDoc

* clean up and rename queryDoc to queryDocument

* misc cleanup and rename getElementAttrValue to getPropertyOrAttribute

* rebase cleanup

* prettier formatting

* [PM-2130] Fixing linting issues

* [PM-2130] Fixing linting issues

* [PM-2130] Migrating implementation for collect methods and tests for those methods into AutofillCollect context

* [PM-2130] Migrating getPropertyOrAttribute method from utils to AutofillCollect

* [PM-2130] Continuing migration of methods from collect utils into AutofillCollect

* [PM-2130] Rework of isViewable method to better handle behavior for how we identify if an element is currently within the viewport

* [PM-2130] Filling out implementation of autofill-insert

* [PM-2130] Refining AutofillInsert

* [PM-2130] Implementing jest tests for AutofillCollect methods and breaking out visibility related logic to a separate service

* [PM-2130] Fixing jest tests for AutofillCollect

* [PM-2130] Fixing jest tests for AutofillInit

* [PM-2130] Adjusting how the AutofillFieldVisibilityService class is used in AutofillCollect

* [PM-2130] Working through AutofillInsert implementation

* [PM-2130] Migrating methods from fill.ts to AutofillInsert

* [PM-2130] Migrating methods from fill.ts to AutofillInsert

* [PM-2130] Applying fix for IntersectionObserver when triggering behavior in Safari and fixing issue with how we trigger an input event shortly after filling in a field

* [PM-2130] Refactoring AutofillCollect to service CollectAutofillContentService

* [PM-2130] Refactoring AutofillInsert to service InsertAutofillContentService

* [PM-2130] Further organization of implementation

* [PM-2130] Filling out missing jest test for AutofillInit.fillForm method

* [PM-2130] Migrating the last of the collect jest tests to InsertAutofillContentService

* [PM-2130] Further refactoring of elements including typing information

* [PM-2130] Implementing jest tests for InsertAutofillContentService

* [PM-2130] Implementing jest tests for InsertAutofillContentService

* [PM-2130] Organization and refactoring of methods within InsertAutofillContent

* [PM-2130] Implementation of jest tests for InsertAutofillContentService

* [PM-2130] Implementation of Jest Test for IntertAutofillContentService

* [PM-2130] Finalizing migration of methods and jest tests from util files into Autofill serivces

* [PM-2130] Cleaning up dead code comments

* [PM-2130] Removing unnecessary constants

* [PM-2130] Finalizing jest tests for InsertAutofillContentService

* [PM-2130] Refactoring FieldVisibiltyService to DomElementVisibilityService to allow service to act in a more general manner

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Implementing jest tests for DomElementVisibilityService

* [PM-2130] Breaking out the callback method used to resolve the IntersectionObserver promise

* [PM-2100] Removing unnecessary jest config file

* [PM-2100] Fixing jest tests based on changes implemented within PM-2130

* [PM-2100] Fixing autofill mocks

* [PM-2100] Fixing AutofillService jest tests

* [PM-2100] Handling missing tests within coverage of AutofillService

* [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript

* [PM-2100] Writing tests for AutofillService.generateCardFillScript

* [PM-2100] Finalizing tests for AutofillService.generateCardFillScript

* [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR

* [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript

* [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript

* [PM-2100] Implementing tests for AutofillService

* [PM-2130] Adding a comment explaining a fix for Safari

* [PM-2130] Adding a comment explaining a fix for Safari

* [PM-2100] Implementing tests for AutofillService.loadPasswordFields

* [PM-2100] Implementing tests for AutofillService.findUsernameField

* [PM-2100] Implementing tests for AutofillService.findTotpField

* [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch

* [PM-2100] Finalizing tests for AutofillService

* [PM-2747] Add Support for Feature Flag of Autofill Version

* [PM-2747] Adding Support for Manifest v3 within the implementation

* [PM-2747] Modifying how the feature flag for autofill is named

* [PM-2747] Modifying main.background.ts to load the ConfigApiService correctly

* [PM-2747] Refactoring trigger of autofill scripts to be a simple immediately invoked function

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Removal of jest transform declaration

* [PM-2130] Applying changes required for PM-2762 to implementation, and ensuring jest tests exist to validate the behavior

* [PM-2747] Modifying how we inject the autofill scripts to ensure we are injecting into all frames within a page

* [PM-2130] Removing usage of IntersectionObserver when identifying element visibility due to broken interactions with React Components

* [PM-2130] Fixing issue found when attempting to capture the elementAtCenterPoint in determining file visibility

* [PM-2100] Create Unit Test Suite for autofill.service.ts (#5371)

* [PM-2100] Create Unit Test Suite for Autofill.service.ts

* [PM-2100] Finishing out tests for the getFormsWithPasswordFields method

* [PM-2100] Implementing tests for the doAutofill method within the autofill service

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Working through implementation of doAutofill method

* [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service

* [PM-2100] Finishing implementation of doAutoFill method within autofill service

* [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service

* [PM-2100] Working through tests for generateFillScript

* [PM-2100] Finalizing generateFillScript method testing

* [PM-2100] Starting implementation of generateLoginFillScript

* [PM-2100] Working through tests for generateLoginFillScript

* [PM-2100] Finalizing generateLoginFillScript method testing

* [PM-2100] Removing unnecessary jest config file

* [PM-2100] Fixing jest tests based on changes implemented within PM-2130

* [PM-2100] Fixing autofill mocks

* [PM-2100] Fixing AutofillService jest tests

* [PM-2100] Handling missing tests within coverage of AutofillService

* [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript

* [PM-2100] Writing tests for AutofillService.generateCardFillScript

* [PM-2100] Finalizing tests for AutofillService.generateCardFillScript

* [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR

* [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript

* [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript

* [PM-2100] Implementing tests for AutofillService

* [PM-2100] Implementing tests for AutofillService.loadPasswordFields

* [PM-2100] Implementing tests for AutofillService.findUsernameField

* [PM-2100] Implementing tests for AutofillService.findTotpField

* [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch

* [PM-2100] Finalizing tests for AutofillService

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Modyfing placement of autofill-mocks

* [PM-2100] Removal of jest transform declaration

* [PM-2747] Applying a fix for a race condition that can occur when loading the notification bar and autofiller script login

* [PM-2747] Reverting removal of autofill npm action. Now this will force usage of autofill-v2 regardless of whether a feature flag is set or not

* [PM-2747] Fixing logic error incorporated when merging in master

* [PM-2130] Fixing issue with autofill service unit tests

* [PM-2130] Fixing issue with autofill service unit tests

* [PM-2747] Fixing issue present with notification bar merge

* [PM-2130] Fixing test test for when we need to handle a password reprompt

* [PM-2747] Fixing wording for webpack script

* [PM-2747] Addressing stylistic changes requested from code review

* [PM-2747] Addressing stylistic changes requested from code review

---------

Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
Co-authored-by: Manuel <mr-manuel@outlook.it>
Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>

* [PM-3285] Applying stylistic changes suggested by code review for the feature flag implementation

* [PM-3285] Adding temporary console log to validate which version is being used

* [PM-3465] Implementing a methodology for sorting the autofill field elements after awaiting the results of each element

* [PM-3465] Implementing a methodology for sorting the autofill field elements after awaiting the results of each element

* [PM-3465] Implementing a methodology for using cached field values when requerying DOM for elements

* [PM-3285] Removing temporary console log indicating which version of autofill the user is currently loading

* [PM-3465] Adding scripting api reference to the manifest v3 json file

* [PM-3465] Updating implementation to deal with cache invalidation issues

* [PM-3465] Implementing jest tests for added collect autofill content class elements

* [PM-3465] Removing scripting API permissiong within manifest v3 json file

* [PM-3465] Implementing method for removing cached page details if the window location has updated

* [PM-3465] Fixing issue found with query selector generated while collecting page details

* [PM-3465] Fixing jest tests

* [PM-3465] Fixing jest tests

---------

Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
Co-authored-by: Manuel <mr-manuel@outlook.it>
Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>
This commit is contained in:
Cesar Gonzalez 2023-09-21 17:42:49 -05:00 committed by GitHub
parent c2877523c8
commit c02fc7abe1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1490 additions and 69 deletions

View File

@ -2,6 +2,7 @@
* Represents an HTML form whose elements can be autofilled
*/
export default class AutofillForm {
[key: string]: any;
/**
* The unique identifier assigned to this field during collection of the page details
*/

View File

@ -1,8 +1,32 @@
import AutofillField from "../../models/autofill-field";
import AutofillForm from "../../models/autofill-form";
import AutofillPageDetails from "../../models/autofill-page-details";
import { ElementWithOpId, FormFieldElement } from "../../types";
type AutofillFormElements = Map<ElementWithOpId<HTMLFormElement>, AutofillForm>;
type AutofillFieldElements = Map<ElementWithOpId<FormFieldElement>, AutofillField>;
type UpdateAutofillDataAttributeParams = {
element: ElementWithOpId<HTMLFormElement | FormFieldElement>;
attributeName: string;
dataTarget?: AutofillForm | AutofillField;
dataTargetKey?: string;
};
interface CollectAutofillContentService {
getPageDetails(): Promise<AutofillPageDetails>;
getAutofillFieldElementByOpid(opid: string): HTMLElement | null;
queryAllTreeWalkerNodes(
rootNode: Node,
filterCallback: CallableFunction,
isObservingShadowRoot?: boolean
): Node[];
}
export { CollectAutofillContentService };
export {
AutofillFormElements,
AutofillFieldElements,
UpdateAutofillDataAttributeParams,
CollectAutofillContentService,
};

View File

@ -1,3 +1,7 @@
import { mock } from "jest-mock-extended";
import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form";
import {
ElementWithOpId,
FillableFormFieldElement,
@ -32,7 +36,128 @@ describe("CollectAutofillContentService", () => {
});
describe("getPageDetails", () => {
it("returns an object containing information about the curren page as well as autofill data for the forms and fields of the page", async () => {
beforeEach(() => {
jest
.spyOn(collectAutofillContentService as any, "setupMutationObserver")
.mockImplementationOnce(() => {
collectAutofillContentService["mutationObserver"] = mock<MutationObserver>();
});
});
it("sets up the mutation observer the first time getPageDetails is called", async () => {
await collectAutofillContentService.getPageDetails();
await collectAutofillContentService.getPageDetails();
expect(collectAutofillContentService["setupMutationObserver"]).toHaveBeenCalledTimes(1);
});
it("returns an object with empty forms and fields if no fields were found on a previous iteration", async () => {
collectAutofillContentService["domRecentlyMutated"] = false;
collectAutofillContentService["noFieldsFound"] = true;
jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails");
jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements");
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
await collectAutofillContentService.getPageDetails();
expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalledWith({}, []);
expect(
collectAutofillContentService["queryAutofillFormAndFieldElements"]
).not.toHaveBeenCalled();
expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled();
expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled();
});
it("returns an object with cached form and field data values", async () => {
const formId = "validFormId";
const formAction = "https://example.com/";
const formMethod = "post";
const formName = "validFormName";
const usernameFieldId = "usernameField";
const usernameFieldName = "username";
const usernameFieldLabel = "User Name";
const passwordFieldId = "passwordField";
const passwordFieldName = "password";
const passwordFieldLabel = "Password";
document.body.innerHTML = `
<form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
<label for="${usernameFieldId}">${usernameFieldLabel}</label>
<input type="text" id="${usernameFieldId}" name="${usernameFieldName}" />
<label for="${passwordFieldId}">${passwordFieldLabel}</label>
<input type="password" id="${passwordFieldId}" name="${passwordFieldName}" />
</form>
`;
const formElement = document.getElementById(formId) as ElementWithOpId<HTMLFormElement>;
const autofillForm: AutofillForm = {
opid: "__form__0",
htmlAction: formAction,
htmlName: formName,
htmlID: formId,
htmlMethod: formMethod,
};
const fieldElement = document.getElementById(
usernameFieldId
) as ElementWithOpId<FormFieldElement>;
const autofillField: AutofillField = {
opid: "__0",
elementNumber: 0,
maxLength: 999,
viewable: true,
htmlID: usernameFieldId,
htmlName: usernameFieldName,
htmlClass: null,
tabindex: null,
title: "",
tagName: "input",
"label-tag": usernameFieldLabel,
"label-data": null,
"label-aria": null,
"label-top": null,
"label-right": passwordFieldLabel,
"label-left": usernameFieldLabel,
placeholder: "",
rel: null,
type: "text",
value: "",
checked: false,
autoCompleteType: "",
disabled: false,
readonly: false,
selectInfo: null,
form: "__form__0",
"aria-hidden": false,
"aria-disabled": false,
"aria-haspopup": false,
"data-stripe": null,
};
collectAutofillContentService["domRecentlyMutated"] = false;
collectAutofillContentService["autofillFormElements"] = new Map([
[formElement, autofillForm],
]);
collectAutofillContentService["autofillFieldElements"] = new Map([
[fieldElement, autofillField],
]);
jest.spyOn(collectAutofillContentService as any, "getFormattedPageDetails");
jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFormsData");
jest.spyOn(collectAutofillContentService as any, "getFormattedAutofillFieldsData");
jest.spyOn(collectAutofillContentService as any, "queryAutofillFormAndFieldElements");
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
await collectAutofillContentService.getPageDetails();
expect(collectAutofillContentService["getFormattedPageDetails"]).toHaveBeenCalled();
expect(collectAutofillContentService["getFormattedAutofillFormsData"]).toHaveBeenCalled();
expect(collectAutofillContentService["getFormattedAutofillFieldsData"]).toHaveBeenCalled();
expect(
collectAutofillContentService["queryAutofillFormAndFieldElements"]
).not.toHaveBeenCalled();
expect(collectAutofillContentService["buildAutofillFormsData"]).not.toHaveBeenCalled();
expect(collectAutofillContentService["buildAutofillFieldsData"]).not.toHaveBeenCalled();
});
it("returns an object containing information about the current page as well as autofill data for the forms and fields of the page", async () => {
const documentTitle = "Test Page";
const formId = "validFormId";
const formAction = "https://example.com/";
@ -145,6 +270,19 @@ describe("CollectAutofillContentService", () => {
collectedTimestamp: expect.any(Number),
});
});
it("sets the noFieldsFond property to true if the page has no forms or fields", async function () {
document.body.innerHTML = "";
collectAutofillContentService["noFieldsFound"] = false;
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData");
await collectAutofillContentService.getPageDetails();
expect(collectAutofillContentService["buildAutofillFormsData"]).toHaveBeenCalled();
expect(collectAutofillContentService["buildAutofillFieldsData"]).toHaveBeenCalled();
expect(collectAutofillContentService["noFieldsFound"]).toBe(true);
});
});
describe("getAutofillFieldElementByOpid", () => {
@ -213,6 +351,44 @@ describe("CollectAutofillContentService", () => {
});
describe("buildAutofillFormsData", () => {
it("will not attempt to gather data from a cached form element", () => {
const documentTitle = "Test Page";
const formId = "validFormId";
const formAction = "https://example.com/";
const formMethod = "post";
const formName = "validFormName";
document.title = documentTitle;
document.body.innerHTML = `
<form id="${formId}" action="${formAction}" method="${formMethod}" name="${formName}">
<label for="usernameFieldId">usernameFieldLabel</label>
<input type="text" id="usernameFieldId" name="usernameFieldName" />
<label for="passwordFieldId">passwordFieldLabel</label>
<input type="password" id="passwordFieldId" name="passwordFieldName" />
</form>
`;
const formElement = document.getElementById(formId) as ElementWithOpId<HTMLFormElement>;
const existingAutofillForm: AutofillForm = {
opid: "__form__0",
htmlAction: formAction,
htmlName: formName,
htmlID: formId,
htmlMethod: formMethod,
};
collectAutofillContentService["autofillFormElements"] = new Map([
[formElement, existingAutofillForm],
]);
const formElements = Array.from(document.querySelectorAll("form"));
jest.spyOn(collectAutofillContentService as any, "getFormActionAttribute");
const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"](
formElements as Node[]
);
expect(collectAutofillContentService["getFormActionAttribute"]).not.toHaveBeenCalled();
expect(autofillFormsData).toStrictEqual({ __form__0: existingAutofillForm });
});
it("returns an object of AutofillForm objects with the form id as a key", () => {
const documentTitle = "Test Page";
const formId1 = "validFormId";
@ -237,7 +413,9 @@ describe("CollectAutofillContentService", () => {
</form>
`;
const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"]();
const { formElements } = collectAutofillContentService["queryAutofillFormAndFieldElements"]();
const autofillFormsData =
collectAutofillContentService["buildAutofillFormsData"](formElements);
expect(autofillFormsData).toStrictEqual({
__form__0: {
@ -266,10 +444,17 @@ describe("CollectAutofillContentService", () => {
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
.mockResolvedValue(true);
const autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"]();
const { formFieldElements } =
collectAutofillContentService["queryAutofillFormAndFieldElements"]();
const autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"](
formFieldElements as FormFieldElement[]
);
const autofillFieldsData = await Promise.resolve(autofillFieldsPromise);
expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith(50);
expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith(
100,
formFieldElements
);
expect(collectAutofillContentService["buildAutofillFieldItem"]).toHaveBeenCalledTimes(2);
expect(autofillFieldsPromise).toBeInstanceOf(Promise);
expect(autofillFieldsData).toStrictEqual([
@ -372,9 +557,6 @@ describe("CollectAutofillContentService", () => {
const formElements: FormFieldElement[] =
collectAutofillContentService["getAutofillFieldElements"]();
expect(document.querySelectorAll).toHaveBeenCalledWith(
'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill]'
);
expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
expect(formElements).toEqual([
usernameInput,
@ -538,6 +720,105 @@ describe("CollectAutofillContentService", () => {
});
describe("buildAutofillFieldItem", () => {
it("returns an existing autofill field item if it exists", async () => {
const index = 0;
const usernameField = {
labelText: "Username",
id: "username-id",
classes: "username input classes",
name: "username",
type: "text",
maxLength: 42,
tabIndex: 0,
title: "Username Input Title",
autocomplete: "username-autocomplete",
dataLabel: "username-data-label",
ariaLabel: "username-aria-label",
placeholder: "username-placeholder",
rel: "username-rel",
value: "username-value",
dataStripe: "data-stripe",
};
document.body.innerHTML = `
<form>
<label for="${usernameField.id}">${usernameField.labelText}</label>
<input
id="${usernameField.id}"
class="${usernameField.classes}"
name="${usernameField.name}"
type="${usernameField.type}"
maxlength="${usernameField.maxLength}"
tabindex="${usernameField.tabIndex}"
title="${usernameField.title}"
autocomplete="${usernameField.autocomplete}"
data-label="${usernameField.dataLabel}"
aria-label="${usernameField.ariaLabel}"
placeholder="${usernameField.placeholder}"
rel="${usernameField.rel}"
value="${usernameField.value}"
data-stripe="${usernameField.dataStripe}"
/>
</form>
`;
document.body.innerHTML = `
<form>
<label for="${usernameField.id}">${usernameField.labelText}</label>
<input
id="${usernameField.id}"
class="${usernameField.classes}"
name="${usernameField.name}"
type="${usernameField.type}"
maxlength="${usernameField.maxLength}"
tabindex="${usernameField.tabIndex}"
title="${usernameField.title}"
autocomplete="${usernameField.autocomplete}"
data-label="${usernameField.dataLabel}"
aria-label="${usernameField.ariaLabel}"
placeholder="${usernameField.placeholder}"
rel="${usernameField.rel}"
value="${usernameField.value}"
data-stripe="${usernameField.dataStripe}"
/>
</form>
`;
const existingFieldData: AutofillField = {
elementNumber: index,
htmlClass: usernameField.classes,
htmlID: usernameField.id,
htmlName: usernameField.name,
maxLength: usernameField.maxLength,
opid: `__${index}`,
tabindex: String(usernameField.tabIndex),
tagName: "input",
title: usernameField.title,
viewable: true,
};
const usernameInput = document.getElementById(
usernameField.id
) as ElementWithOpId<FillableFormFieldElement>;
usernameInput.opid = "__0";
collectAutofillContentService["autofillFieldElements"].set(usernameInput, existingFieldData);
jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength");
jest
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
.mockResolvedValue(true);
jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute");
jest.spyOn(collectAutofillContentService as any, "getElementValue");
const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
usernameInput,
0
);
expect(collectAutofillContentService["getAutofillFieldMaxLength"]).not.toHaveBeenCalled();
expect(
collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable
).not.toHaveBeenCalled();
expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled();
expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled();
expect(autofillFieldItem).toEqual(existingFieldData);
});
it("returns the AutofillField base data values without the field labels or input values if the passed element is a span element", async () => {
const index = 0;
const spanElementId = "span-element";
@ -958,6 +1239,20 @@ describe("CollectAutofillContentService", () => {
expect(labels).toEqual(document.querySelectorAll("label[for='username']"));
});
it("removes any new lines generated for the query selector", () => {
document.body.innerHTML = `
<label for="username-
id">Username</label>
<input type="text" id="username-
id">
`;
const element = document.querySelector("input") as FillableFormFieldElement;
const labels = collectAutofillContentService["queryElementLabels"](element);
expect(labels).toEqual(document.querySelectorAll("label[for='username-id']"));
});
});
describe("createLabelElementsTag", () => {
@ -1585,4 +1880,466 @@ describe("CollectAutofillContentService", () => {
expect(selectWithoutOptionsOptions).toEqual({ options: [] });
});
});
describe("getShadowRoot", () => {
it("returns null if the passed node is not an HTMLElement instance", () => {
const textNode = document.createTextNode("Hello, world!");
const shadowRoot = collectAutofillContentService["getShadowRoot"](textNode);
expect(shadowRoot).toEqual(null);
});
it("returns a value provided by Chrome's openOrClosedShadowRoot API", () => {
// eslint-disable-next-line
// @ts-ignore
globalThis.chrome.dom = {
openOrClosedShadowRoot: jest.fn(),
};
const element = document.createElement("div");
collectAutofillContentService["getShadowRoot"](element);
// eslint-disable-next-line
// @ts-ignore
expect(chrome.dom.openOrClosedShadowRoot).toBeCalled();
});
});
describe("buildTreeWalkerNodesQueryResults", () => {
it("will recursively call itself if a shadowDOM element is found and will observe the element for mutations", () => {
collectAutofillContentService["mutationObserver"] = mock<MutationObserver>({
observe: jest.fn(),
});
jest.spyOn(collectAutofillContentService as any, "buildTreeWalkerNodesQueryResults");
const shadowRoot = document.createElement("div");
jest
.spyOn(collectAutofillContentService as any, "getShadowRoot")
.mockReturnValueOnce(shadowRoot);
const callbackFilter = jest.fn();
collectAutofillContentService["buildTreeWalkerNodesQueryResults"](
document.body,
[],
callbackFilter,
true
);
expect(collectAutofillContentService["buildTreeWalkerNodesQueryResults"]).toBeCalledTimes(2);
expect(collectAutofillContentService["mutationObserver"].observe).toBeCalled();
});
it("will not observe the shadowDOM element if required to skip", () => {
collectAutofillContentService["mutationObserver"] = mock<MutationObserver>({
observe: jest.fn(),
});
const shadowRoot = document.createElement("div");
jest
.spyOn(collectAutofillContentService as any, "getShadowRoot")
.mockReturnValueOnce(shadowRoot);
const callbackFilter = jest.fn();
collectAutofillContentService["buildTreeWalkerNodesQueryResults"](
document.body,
[],
callbackFilter,
false
);
expect(collectAutofillContentService["mutationObserver"].observe).not.toBeCalled();
});
});
describe("setupMutationObserver", () => {
it("sets up a mutation observer and observes the document element", () => {
jest.spyOn(MutationObserver.prototype, "observe");
collectAutofillContentService["setupMutationObserver"]();
expect(collectAutofillContentService["mutationObserver"]).toBeInstanceOf(MutationObserver);
expect(collectAutofillContentService["mutationObserver"].observe).toBeCalled();
});
});
describe("handleMutationObserverMutation", () => {
it("will set the domRecentlyMutated value to true and the noFieldsFound value to false if a form or field node has been added ", () => {
const form = document.createElement("form");
document.body.appendChild(form);
const addedNodes = document.querySelectorAll("form");
const removedNodes = document.querySelectorAll("li");
const mutationRecord: MutationRecord = {
type: "childList",
addedNodes: addedNodes,
attributeName: null,
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: removedNodes,
target: document.body,
};
collectAutofillContentService["domRecentlyMutated"] = false;
collectAutofillContentService["noFieldsFound"] = true;
collectAutofillContentService["currentLocationHref"] = window.location.href;
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true);
expect(collectAutofillContentService["noFieldsFound"]).toEqual(false);
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).toBeCalledWith(
removedNodes,
true
);
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).toBeCalledWith(
addedNodes
);
});
it("will handle updating the autofill element if any attribute mutations are encountered", () => {
const mutationRecord: MutationRecord = {
type: "attributes",
addedNodes: null,
attributeName: "value",
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: null,
target: document.body,
};
collectAutofillContentService["domRecentlyMutated"] = false;
collectAutofillContentService["noFieldsFound"] = true;
collectAutofillContentService["currentLocationHref"] = window.location.href;
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
jest.spyOn(collectAutofillContentService as any, "handleAutofillElementAttributeMutation");
collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(false);
expect(collectAutofillContentService["noFieldsFound"]).toEqual(true);
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled();
expect(collectAutofillContentService["handleAutofillElementAttributeMutation"]).toBeCalled();
});
it("will handle window location mutations", () => {
const mutationRecord: MutationRecord = {
type: "attributes",
addedNodes: null,
attributeName: "value",
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: null,
target: document.body,
};
collectAutofillContentService["currentLocationHref"] = "https://someotherurl.com";
jest.spyOn(collectAutofillContentService as any, "handleWindowLocationMutation");
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
jest.spyOn(collectAutofillContentService as any, "handleAutofillElementAttributeMutation");
collectAutofillContentService["handleMutationObserverMutation"]([mutationRecord]);
expect(collectAutofillContentService["handleWindowLocationMutation"]).toBeCalled();
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled();
expect(
collectAutofillContentService["handleAutofillElementAttributeMutation"]
).not.toBeCalled();
});
});
describe("deleteCachedAutofillElement", () => {
it("removes the autofill form element from the map of elements", () => {
const formElement = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
const autofillForm: AutofillForm = {
opid: "1234",
htmlName: "formEl",
htmlID: "formEl-id",
htmlAction: "https://example.com",
htmlMethod: "POST",
};
collectAutofillContentService["autofillFormElements"] = new Map([
[formElement, autofillForm],
]);
collectAutofillContentService["deleteCachedAutofillElement"](formElement);
expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0);
});
it("removes the autofill field element form the map of elements", () => {
const fieldElement = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
const autofillField: AutofillField = {
elementNumber: 0,
htmlClass: "",
tabindex: "",
title: "",
viewable: false,
opid: "1234",
htmlName: "username",
htmlID: "username-id",
htmlType: "text",
htmlAutocomplete: "username",
htmlAutofocus: false,
htmlDisabled: false,
htmlMaxLength: 999,
htmlReadonly: false,
htmlRequired: false,
htmlValue: "jsmith",
};
collectAutofillContentService["autofillFieldElements"] = new Map([
[fieldElement, autofillField],
]);
collectAutofillContentService["deleteCachedAutofillElement"](fieldElement);
expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0);
});
});
describe("handleWindowLocationMutation", () => {
it("will set the current location to the global location href, set the dom recently mutated flag and the no fields found flag, clear out the autofill form and field maps, and update the autofill elements after mutation", () => {
collectAutofillContentService["currentLocationHref"] = "https://example.com/login";
collectAutofillContentService["domRecentlyMutated"] = false;
collectAutofillContentService["noFieldsFound"] = true;
jest.spyOn(collectAutofillContentService as any, "updateAutofillElementsAfterMutation");
collectAutofillContentService["handleWindowLocationMutation"]();
expect(collectAutofillContentService["currentLocationHref"]).toEqual(window.location.href);
expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true);
expect(collectAutofillContentService["noFieldsFound"]).toEqual(false);
expect(collectAutofillContentService["updateAutofillElementsAfterMutation"]).toBeCalled();
expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0);
expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0);
});
});
describe("handleAutofillElementAttributeMutation", () => {
it("returns early if the target node is not an HTMLElement instance", () => {
const mutationRecord: MutationRecord = {
type: "attributes",
addedNodes: null,
attributeName: "value",
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: null,
target: document.createTextNode("Hello, world!"),
};
jest.spyOn(collectAutofillContentService as any, "isAutofillElementNodeMutated");
collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord);
expect(collectAutofillContentService["isAutofillElementNodeMutated"]).not.toBeCalled();
});
it("will update the autofill form element data if the target node can be found in the autofillFormElements map", () => {
const targetNode = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
targetNode.setAttribute("name", "username");
targetNode.setAttribute("value", "jsmith");
const autofillForm: AutofillForm = {
opid: "1234",
htmlName: "formEl",
htmlID: "formEl-id",
htmlAction: "https://example.com",
htmlMethod: "POST",
};
const mutationRecord: MutationRecord = {
type: "attributes",
addedNodes: null,
attributeName: "id",
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: null,
target: targetNode,
};
collectAutofillContentService["autofillFormElements"] = new Map([[targetNode, autofillForm]]);
jest.spyOn(collectAutofillContentService as any, "updateAutofillFormElementData");
collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord);
expect(collectAutofillContentService["updateAutofillFormElementData"]).toBeCalledWith(
mutationRecord.attributeName,
mutationRecord.target,
autofillForm
);
});
it("will update the autofill field element data if the target node can be found in the autofillFieldElements map", () => {
const targetNode = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
targetNode.setAttribute("name", "username");
targetNode.setAttribute("value", "jsmith");
const autofillField: AutofillField = {
elementNumber: 0,
htmlClass: "",
tabindex: "",
title: "",
viewable: false,
opid: "1234",
htmlName: "username",
htmlID: "username-id",
htmlType: "text",
htmlAutocomplete: "username",
htmlAutofocus: false,
htmlDisabled: false,
htmlMaxLength: 999,
htmlReadonly: false,
htmlRequired: false,
htmlValue: "jsmith",
};
const mutationRecord: MutationRecord = {
type: "attributes",
addedNodes: null,
attributeName: "id",
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: null,
target: targetNode,
};
collectAutofillContentService["autofillFieldElements"] = new Map([
[targetNode, autofillField],
]);
jest.spyOn(collectAutofillContentService as any, "updateAutofillFieldElementData");
collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord);
expect(collectAutofillContentService["updateAutofillFieldElementData"]).toBeCalledWith(
mutationRecord.attributeName,
mutationRecord.target,
autofillField
);
});
});
describe("updateAutofillFormElementData", () => {
const formElement = document.createElement("form") as ElementWithOpId<HTMLFormElement>;
const autofillForm: AutofillForm = {
opid: "1234",
htmlName: "formEl",
htmlID: "formEl-id",
htmlAction: "https://example.com",
htmlMethod: "POST",
};
const updatedAttributes = ["action", "name", "id", "method"];
updatedAttributes.forEach((attribute) => {
it(`will update the ${attribute} value for the form element`, () => {
jest.spyOn(collectAutofillContentService["autofillFormElements"], "set");
collectAutofillContentService["updateAutofillFormElementData"](
attribute,
formElement,
autofillForm
);
expect(collectAutofillContentService["autofillFormElements"].set).toBeCalledWith(
formElement,
autofillForm
);
});
});
it("will not update an attribute value if it is not present in the updateActions object", () => {
jest.spyOn(collectAutofillContentService["autofillFormElements"], "set");
collectAutofillContentService["updateAutofillFormElementData"](
"aria-label",
formElement,
autofillForm
);
expect(collectAutofillContentService["autofillFormElements"].set).not.toBeCalled();
});
});
describe("updateAutofillFieldElementData", () => {
const fieldElement = document.createElement("input") as ElementWithOpId<HTMLInputElement>;
const autofillField: AutofillField = {
htmlClass: "value",
htmlID: "",
htmlName: "",
opid: "",
tabindex: "",
title: "",
viewable: false,
elementNumber: 0,
};
const updatedAttributes = [
"maxlength",
"name",
"id",
"type",
"autocomplete",
"class",
"tabindex",
"title",
"value",
"rel",
"tagname",
"checked",
"disabled",
"readonly",
"data-label",
"aria-label",
"aria-hidden",
"aria-disabled",
"aria-haspopup",
"data-stripe",
];
updatedAttributes.forEach((attribute) => {
it(`will update the ${attribute} value for the field element`, async () => {
jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");
await collectAutofillContentService["updateAutofillFieldElementData"](
attribute,
fieldElement,
autofillField
);
expect(collectAutofillContentService["autofillFieldElements"].set).toBeCalledWith(
fieldElement,
autofillField
);
});
});
it("will check the dom element's visibility if the `style` or `class` attribute has updated ", async () => {
jest.spyOn(
collectAutofillContentService["domElementVisibilityService"],
"isFormFieldViewable"
);
const attributes = ["class", "style"];
for (const attribute of attributes) {
await collectAutofillContentService["updateAutofillFieldElementData"](
attribute,
fieldElement,
autofillField
);
expect(
collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable
).toBeCalledWith(fieldElement);
}
});
it("will not update an attribute value if it is not present in the updateActions object", async () => {
jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set");
await collectAutofillContentService["updateAutofillFieldElementData"](
"random-attribute",
fieldElement,
autofillField
);
expect(collectAutofillContentService["autofillFieldElements"].set).not.toBeCalled();
});
});
});

View File

@ -8,34 +8,70 @@ import {
FormElementWithAttribute,
} from "../types";
import { CollectAutofillContentService as CollectAutofillContentServiceInterface } from "./abstractions/collect-autofill-content.service";
import {
UpdateAutofillDataAttributeParams,
AutofillFieldElements,
AutofillFormElements,
CollectAutofillContentService as CollectAutofillContentServiceInterface,
} from "./abstractions/collect-autofill-content.service";
import DomElementVisibilityService from "./dom-element-visibility.service";
class CollectAutofillContentService implements CollectAutofillContentServiceInterface {
private readonly domElementVisibilityService: DomElementVisibilityService;
private noFieldsFound = false;
private domRecentlyMutated = true;
private autofillFormElements: AutofillFormElements = new Map();
private autofillFieldElements: AutofillFieldElements = new Map();
private currentLocationHref = "";
private mutationObserver: MutationObserver;
private updateAutofillElementsAfterMutationTimeout: NodeJS.Timeout;
private readonly updateAfterMutationTimeoutDelay = 1000;
constructor(domElementVisibilityService: DomElementVisibilityService) {
this.domElementVisibilityService = domElementVisibilityService;
}
/**
* Builds the data for all the forms and fields
* that are found within the page DOM.
* Builds the data for all forms and fields found within the page DOM.
* Sets up a mutation observer to verify DOM changes and returns early
* with cached data if no changes are detected.
* @returns {Promise<AutofillPageDetails>}
* @public
*/
async getPageDetails(): Promise<AutofillPageDetails> {
const autofillFormsData: Record<string, AutofillForm> = this.buildAutofillFormsData();
const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData();
if (!this.mutationObserver) {
this.setupMutationObserver();
}
return {
title: document.title,
url: (document.defaultView || window).location.href,
documentUrl: document.location.href,
forms: autofillFormsData,
fields: autofillFieldsData,
collectedTimestamp: Date.now(),
};
if (!this.domRecentlyMutated && this.noFieldsFound) {
return this.getFormattedPageDetails({}, []);
}
if (
!this.domRecentlyMutated &&
this.autofillFormElements.size &&
this.autofillFieldElements.size
) {
return this.getFormattedPageDetails(
this.getFormattedAutofillFormsData(),
this.getFormattedAutofillFieldsData()
);
}
const { formElements, formFieldElements } = this.queryAutofillFormAndFieldElements();
const autofillFormsData: Record<string, AutofillForm> =
this.buildAutofillFormsData(formElements);
const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData(
formFieldElements as FormFieldElement[]
);
this.sortAutofillFieldElementsMap();
if (!Object.values(autofillFormsData).length || !autofillFieldsData.length) {
this.noFieldsFound = true;
}
this.domRecentlyMutated = false;
return this.getFormattedPageDetails(autofillFormsData, autofillFieldsData);
}
/**
@ -46,15 +82,18 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @returns {FormFieldElement | null}
*/
getAutofillFieldElementByOpid(opid: string): FormFieldElement | null {
const fieldElements = this.getAutofillFieldElements();
const fieldElementsWithOpid = fieldElements.filter(
const cachedFormFieldElements = Array.from(this.autofillFieldElements.keys());
const formFieldElements = cachedFormFieldElements?.length
? cachedFormFieldElements
: this.getAutofillFieldElements();
const fieldElementsWithOpid = formFieldElements.filter(
(fieldElement) => (fieldElement as ElementWithOpId<FormFieldElement>).opid === opid
) as ElementWithOpId<FormFieldElement>[];
if (!fieldElementsWithOpid.length) {
const elementIndex = parseInt(opid.split("__")[1], 10);
return fieldElements[elementIndex] || null;
return formFieldElements[elementIndex] || null;
}
if (fieldElementsWithOpid.length > 1) {
@ -65,30 +104,120 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
return fieldElementsWithOpid[0];
}
/**
* Queries the DOM for all the nodes that match the given filter callback
* and returns a collection of nodes.
* @param {Node} rootNode
* @param {Function} filterCallback
* @param {boolean} isObservingShadowRoot
* @returns {Node[]}
*/
queryAllTreeWalkerNodes(
rootNode: Node,
filterCallback: CallableFunction,
isObservingShadowRoot = true
): Node[] {
const treeWalkerQueryResults: Node[] = [];
this.buildTreeWalkerNodesQueryResults(
rootNode,
treeWalkerQueryResults,
filterCallback,
isObservingShadowRoot
);
return treeWalkerQueryResults;
}
/**
* Sorts the AutofillFieldElements map by the elementNumber property.
* @private
*/
private sortAutofillFieldElementsMap() {
if (!this.autofillFieldElements.size) {
return;
}
this.autofillFieldElements = new Map(
[...this.autofillFieldElements].sort((a, b) => a[1].elementNumber - b[1].elementNumber)
);
}
/**
* Formats and returns the AutofillPageDetails object
* @param {Record<string, AutofillForm>} autofillFormsData
* @param {AutofillField[]} autofillFieldsData
* @returns {AutofillPageDetails}
* @private
*/
private getFormattedPageDetails(
autofillFormsData: Record<string, AutofillForm>,
autofillFieldsData: AutofillField[]
): AutofillPageDetails {
return {
title: document.title,
url: (document.defaultView || window).location.href,
documentUrl: document.location.href,
forms: autofillFormsData,
fields: autofillFieldsData,
collectedTimestamp: Date.now(),
};
}
/**
* Queries the DOM for all the forms elements and
* returns a collection of AutofillForm objects.
* @returns {Record<string, AutofillForm>}
* @private
*/
private buildAutofillFormsData(): Record<string, AutofillForm> {
const autofillForms: Record<string, AutofillForm> = {};
const documentFormElements = document.querySelectorAll("form");
documentFormElements.forEach((formElement: HTMLFormElement, index: number) => {
private buildAutofillFormsData(formElements: Node[]): Record<string, AutofillForm> {
for (let index = 0; index < formElements.length; index++) {
const formElement = formElements[index] as ElementWithOpId<HTMLFormElement>;
formElement.opid = `__form__${index}`;
autofillForms[formElement.opid] = {
const existingAutofillForm = this.autofillFormElements.get(formElement);
if (existingAutofillForm) {
existingAutofillForm.opid = formElement.opid;
this.autofillFormElements.set(formElement, existingAutofillForm);
continue;
}
this.autofillFormElements.set(formElement, {
opid: formElement.opid,
htmlAction: new URL(
this.getPropertyOrAttribute(formElement, "action"),
window.location.href
).href,
htmlAction: this.getFormActionAttribute(formElement),
htmlName: this.getPropertyOrAttribute(formElement, "name"),
htmlID: this.getPropertyOrAttribute(formElement, "id"),
htmlMethod: this.getPropertyOrAttribute(formElement, "method"),
};
});
});
}
return this.getFormattedAutofillFormsData();
}
/**
* Returns the action attribute of the form element. If the action attribute
* is a relative path, it will be converted to an absolute path.
* @param {ElementWithOpId<HTMLFormElement>} element
* @returns {string}
* @private
*/
private getFormActionAttribute(element: ElementWithOpId<HTMLFormElement>): string {
return new URL(this.getPropertyOrAttribute(element, "action"), window.location.href).href;
}
/**
* Iterates over all known form elements and returns an AutofillForm object
* containing a key value pair of the form element's opid and the form data.
* @returns {Record<string, AutofillForm>}
* @private
*/
private getFormattedAutofillFormsData(): Record<string, AutofillForm> {
const autofillForms: Record<string, AutofillForm> = {};
const autofillFormElements = Array.from(this.autofillFormElements);
for (let index = 0; index < autofillFormElements.length; index++) {
const [formElement, autofillForm] = autofillFormElements[index];
autofillForms[formElement.opid] = autofillForm;
}
return autofillForms;
}
@ -99,8 +228,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @returns {Promise<AutofillField[]>}
* @private
*/
private async buildAutofillFieldsData(): Promise<AutofillField[]> {
const autofillFieldElements = this.getAutofillFieldElements(50);
private async buildAutofillFieldsData(
formFieldElements: FormFieldElement[]
): Promise<AutofillField[]> {
const autofillFieldElements = this.getAutofillFieldElements(100, formFieldElements);
const autofillFieldDataPromises = autofillFieldElements.map(this.buildAutofillFieldItem);
return Promise.all(autofillFieldDataPromises);
@ -111,18 +242,19 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* and returns a list limited to the given `fieldsLimit` number that
* is ordered by priority.
* @param {number} fieldsLimit - The maximum number of fields to return
* @param {FormFieldElement[]} previouslyFoundFormFieldElements - The list of all the field elements
* @returns {FormFieldElement[]}
* @private
*/
private getAutofillFieldElements(fieldsLimit?: number): FormFieldElement[] {
const formFieldElements: FormFieldElement[] = [
...(document.querySelectorAll(
'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), ' +
"textarea:not([data-bwignore]), " +
"select:not([data-bwignore]), " +
"span[data-bwautofill]"
) as NodeListOf<FormFieldElement>),
];
private getAutofillFieldElements(
fieldsLimit?: number,
previouslyFoundFormFieldElements?: FormFieldElement[]
): FormFieldElement[] {
const formFieldElements =
previouslyFoundFormFieldElements ||
(this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) =>
this.isNodeFormFieldElement(node)
) as FormFieldElement[]);
if (!fieldsLimit || formFieldElements.length <= fieldsLimit) {
return formFieldElements;
@ -168,6 +300,15 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
): Promise<AutofillField> => {
element.opid = `__${index}`;
const existingAutofillField = this.autofillFieldElements.get(element);
if (existingAutofillField) {
existingAutofillField.opid = element.opid;
existingAutofillField.elementNumber = index;
this.autofillFieldElements.set(element, existingAutofillField);
return existingAutofillField;
}
const autofillFieldBase = {
opid: element.opid,
elementNumber: index,
@ -178,19 +319,16 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
htmlClass: this.getPropertyOrAttribute(element, "class"),
tabindex: this.getPropertyOrAttribute(element, "tabindex"),
title: this.getPropertyOrAttribute(element, "title"),
tagName: this.getPropertyOrAttribute(element, "tagName")?.toLowerCase(),
tagName: this.getAttributeLowerCase(element, "tagName"),
};
if (element instanceof HTMLSpanElement) {
this.autofillFieldElements.set(element, autofillFieldBase);
return autofillFieldBase;
}
let autofillFieldLabels = {};
const autoCompleteType =
this.getPropertyOrAttribute(element, "x-autocompletetype") ||
this.getPropertyOrAttribute(element, "autocompletetype") ||
this.getPropertyOrAttribute(element, "autocomplete");
const elementType = this.getPropertyOrAttribute(element, "type")?.toLowerCase();
const elementType = this.getAttributeLowerCase(element, "type");
if (elementType !== "hidden") {
autofillFieldLabels = {
"label-tag": this.createAutofillFieldLabelTag(element),
@ -203,26 +341,87 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
};
}
return {
const autofillField = {
...autofillFieldBase,
...autofillFieldLabels,
rel: this.getPropertyOrAttribute(element, "rel"),
type: elementType,
value: this.getElementValue(element),
checked: Boolean(this.getPropertyOrAttribute(element, "checked")),
autoCompleteType: autoCompleteType !== "off" ? autoCompleteType : null,
disabled: Boolean(this.getPropertyOrAttribute(element, "disabled")),
readonly: Boolean(this.getPropertyOrAttribute(element, "readOnly")),
checked: this.getAttributeBoolean(element, "checked"),
autoCompleteType: this.getAutoCompleteAttribute(element),
disabled: this.getAttributeBoolean(element, "disabled"),
readonly: this.getAttributeBoolean(element, "readonly"),
selectInfo:
element instanceof HTMLSelectElement ? this.getSelectElementOptions(element) : null,
form: element.form ? this.getPropertyOrAttribute(element.form, "opid") : null,
"aria-hidden": this.getPropertyOrAttribute(element, "aria-hidden") === "true",
"aria-disabled": this.getPropertyOrAttribute(element, "aria-disabled") === "true",
"aria-haspopup": this.getPropertyOrAttribute(element, "aria-haspopup") === "true",
"aria-hidden": this.getAttributeBoolean(element, "aria-hidden", true),
"aria-disabled": this.getAttributeBoolean(element, "aria-disabled", true),
"aria-haspopup": this.getAttributeBoolean(element, "aria-haspopup", true),
"data-stripe": this.getPropertyOrAttribute(element, "data-stripe"),
};
this.autofillFieldElements.set(element, autofillField);
return autofillField;
};
/**
* Identifies the autocomplete attribute associated with an element and returns
* the value of the attribute if it is not set to "off".
* @param {ElementWithOpId<FormFieldElement>} element
* @returns {string}
* @private
*/
private getAutoCompleteAttribute(element: ElementWithOpId<FormFieldElement>): string {
const autoCompleteType =
this.getPropertyOrAttribute(element, "x-autocompletetype") ||
this.getPropertyOrAttribute(element, "autocompletetype") ||
this.getPropertyOrAttribute(element, "autocomplete");
return autoCompleteType !== "off" ? autoCompleteType : null;
}
/**
* Returns a boolean representing the attribute value of an element.
* @param {ElementWithOpId<FormFieldElement>} element
* @param {string} attributeName
* @param {boolean} checkString
* @returns {boolean}
* @private
*/
private getAttributeBoolean(
element: ElementWithOpId<FormFieldElement>,
attributeName: string,
checkString = false
): boolean {
if (checkString) {
return this.getPropertyOrAttribute(element, attributeName) === "true";
}
return Boolean(this.getPropertyOrAttribute(element, attributeName));
}
/**
* Returns the attribute of an element as a lowercase value.
* @param {ElementWithOpId<FormFieldElement>} element
* @param {string} attributeName
* @returns {string}
* @private
*/
private getAttributeLowerCase(
element: ElementWithOpId<FormFieldElement>,
attributeName: string
): string {
return this.getPropertyOrAttribute(element, attributeName)?.toLowerCase();
}
/**
* Returns the value of an element's property or attribute.
* @returns {AutofillField[]}
* @private
*/
private getFormattedAutofillFieldsData(): AutofillField[] {
return Array.from(this.autofillFieldElements.values());
}
/**
* Creates a label tag used to autofill the element pulled from a label
* associated with the element's id, name, parent element or from an
@ -235,13 +434,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
*/
private createAutofillFieldLabelTag(element: FillableFormFieldElement): string {
const labelElementsSet: Set<HTMLElement> = new Set(element.labels);
if (labelElementsSet.size) {
return this.createLabelElementsTag(labelElementsSet);
}
const labelElements: NodeListOf<HTMLLabelElement> | null = this.queryElementLabels(element);
labelElements?.forEach((labelElement) => labelElementsSet.add(labelElement));
for (let labelIndex = 0; labelIndex < labelElements?.length; labelIndex++) {
labelElementsSet.add(labelElements[labelIndex]);
}
let currentElement: HTMLElement | null = element;
while (currentElement && currentElement !== document.documentElement) {
@ -286,7 +486,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
return null;
}
return document.querySelectorAll(labelQuerySelectors);
return (element.getRootNode() as Document | ShadowRoot).querySelectorAll(
labelQuerySelectors.replace(/\n/g, "")
);
}
/**
@ -297,7 +499,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @private
*/
private createLabelElementsTag = (labelElementsSet: Set<HTMLElement>): string => {
return [...labelElementsSet]
return Array.from(labelElementsSet)
.map((labelElement) => {
const textContent: string | null = labelElement
? labelElement.textContent || labelElement.innerText
@ -561,7 +763,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @private
*/
private getSelectElementOptions(element: HTMLSelectElement): { options: (string | null)[][] } {
const options = [...element.options].map((option) => {
const options = Array.from(element.options).map((option) => {
const optionText = option.text
? String(option.text)
.toLowerCase()
@ -573,6 +775,425 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
return { options };
}
/**
* Queries all potential form and field elements from the DOM and returns
* a collection of form and field elements. Leverages the TreeWalker API
* to deep query Shadow DOM elements.
* @returns {{formElements: Node[], formFieldElements: Node[]}}
* @private
*/
private queryAutofillFormAndFieldElements(): {
formElements: Node[];
formFieldElements: Node[];
} {
const formElements: Node[] = [];
const formFieldElements: Node[] = [];
this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) => {
if (node instanceof HTMLFormElement) {
formElements.push(node);
return true;
}
if (this.isNodeFormFieldElement(node)) {
formFieldElements.push(node);
return true;
}
return false;
});
return { formElements, formFieldElements };
}
/**
* Checks if the passed node is a form field element.
* @param {Node} node
* @returns {boolean}
* @private
*/
private isNodeFormFieldElement(node: Node): boolean {
const nodeIsSpanElementWithAutofillAttribute =
node instanceof HTMLSpanElement && node.hasAttribute("data-bwautofill");
const ignoredInputTypes = new Set(["hidden", "submit", "reset", "button", "image", "file"]);
const nodeIsValidInputElement =
node instanceof HTMLInputElement && !ignoredInputTypes.has(node.type);
const nodeIsTextAreaOrSelectElement =
node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement;
const nodeIsNonIgnoredFillableControlElement =
(nodeIsTextAreaOrSelectElement || nodeIsValidInputElement) &&
!node.hasAttribute("data-bwignore");
return nodeIsSpanElementWithAutofillAttribute || nodeIsNonIgnoredFillableControlElement;
}
/**
* Attempts to get the ShadowRoot of the passed node. If support for the
* extension based openOrClosedShadowRoot API is available, it will be used.
* @param {Node} node
* @returns {ShadowRoot | null}
* @private
*/
private getShadowRoot(node: Node): ShadowRoot | null {
if (!(node instanceof HTMLElement)) {
return null;
}
if ((chrome as any).dom?.openOrClosedShadowRoot) {
return (chrome as any).dom.openOrClosedShadowRoot(node);
}
return (node as any).openOrClosedShadowRoot || node.shadowRoot;
}
/**
* Recursively builds a collection of nodes that match the given filter callback.
* If a node has a ShadowRoot, it will be observed for mutations.
* @param {Node} rootNode
* @param {Node[]} treeWalkerQueryResults
* @param {Function} filterCallback
* @param {boolean} isObservingShadowRoot
* @private
*/
private buildTreeWalkerNodesQueryResults(
rootNode: Node,
treeWalkerQueryResults: Node[],
filterCallback: CallableFunction,
isObservingShadowRoot: boolean
) {
const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT);
let currentNode = treeWalker?.currentNode;
while (currentNode) {
if (filterCallback(currentNode)) {
treeWalkerQueryResults.push(currentNode);
}
const nodeShadowRoot = this.getShadowRoot(currentNode);
if (nodeShadowRoot) {
if (isObservingShadowRoot) {
this.mutationObserver.observe(nodeShadowRoot, {
attributes: true,
childList: true,
subtree: true,
});
}
this.buildTreeWalkerNodesQueryResults(
nodeShadowRoot,
treeWalkerQueryResults,
filterCallback,
isObservingShadowRoot
);
}
currentNode = treeWalker?.nextNode();
}
}
/**
* Sets up a mutation observer on the body of the document. Observes changes to
* DOM elements to ensure we have an updated set of autofill field data.
* @private
*/
private setupMutationObserver() {
this.currentLocationHref = globalThis.location.href;
this.mutationObserver = new MutationObserver(this.handleMutationObserverMutation);
this.mutationObserver.observe(document.documentElement, {
attributes: true,
childList: true,
subtree: true,
});
}
/**
* Handles observed DOM mutations and identifies if a mutation is related to
* an autofill element. If so, it will update the autofill element data.
* @param {MutationRecord[]} mutations
* @private
*/
private handleMutationObserverMutation = (mutations: MutationRecord[]) => {
if (this.currentLocationHref !== globalThis.location.href) {
this.handleWindowLocationMutation();
return;
}
for (let mutationsIndex = 0; mutationsIndex < mutations.length; mutationsIndex++) {
const mutation = mutations[mutationsIndex];
if (
mutation.type === "childList" &&
(this.isAutofillElementNodeMutated(mutation.removedNodes, true) ||
this.isAutofillElementNodeMutated(mutation.addedNodes))
) {
this.domRecentlyMutated = true;
this.noFieldsFound = false;
continue;
}
if (mutation.type === "attributes") {
this.handleAutofillElementAttributeMutation(mutation);
}
}
if (this.domRecentlyMutated) {
this.updateAutofillElementsAfterMutation();
}
};
/**
* Handles a mutation to the window location. Clears the autofill elements
* and updates the autofill elements after a timeout.
* @private
*/
private handleWindowLocationMutation() {
this.currentLocationHref = globalThis.location.href;
this.domRecentlyMutated = true;
this.noFieldsFound = false;
this.autofillFormElements.clear();
this.autofillFieldElements.clear();
this.updateAutofillElementsAfterMutation();
}
/**
* Checks if the passed nodes either contain or are autofill elements.
* @param {NodeList} nodes
* @param {boolean} isRemovingNodes
* @returns {boolean}
* @private
*/
private isAutofillElementNodeMutated(nodes: NodeList, isRemovingNodes = false): boolean {
if (!nodes.length) {
return false;
}
let isElementMutated = false;
const mutatedElements = [];
for (let index = 0; index < nodes.length; index++) {
const node = nodes[index];
if (!(node instanceof HTMLElement)) {
continue;
}
if (node instanceof HTMLFormElement || this.isNodeFormFieldElement(node)) {
isElementMutated = true;
mutatedElements.push(node);
continue;
}
const childNodes = this.queryAllTreeWalkerNodes(
node,
(node: Node) => node instanceof HTMLFormElement || this.isNodeFormFieldElement(node)
) as HTMLElement[];
if (childNodes.length) {
isElementMutated = true;
mutatedElements.push(...childNodes);
}
}
if (isRemovingNodes) {
for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) {
this.deleteCachedAutofillElement(
mutatedElements[elementIndex] as
| ElementWithOpId<HTMLFormElement>
| ElementWithOpId<FormFieldElement>
);
}
}
return isElementMutated;
}
/**
* Deletes any cached autofill elements that have been
* removed from the DOM.
* @param {ElementWithOpId<HTMLFormElement> | ElementWithOpId<FormFieldElement>} element
* @private
*/
private deleteCachedAutofillElement(
element: ElementWithOpId<HTMLFormElement> | ElementWithOpId<FormFieldElement>
) {
if (element instanceof HTMLFormElement && this.autofillFormElements.has(element)) {
this.autofillFormElements.delete(element);
return;
}
if (this.autofillFieldElements.has(element)) {
this.autofillFieldElements.delete(element);
}
}
/**
* Updates the autofill elements after a DOM mutation has occurred.
* Is debounced to prevent excessive updates.
* @private
*/
private updateAutofillElementsAfterMutation() {
if (this.updateAutofillElementsAfterMutationTimeout) {
clearTimeout(this.updateAutofillElementsAfterMutationTimeout);
}
this.updateAutofillElementsAfterMutationTimeout = setTimeout(
this.getPageDetails.bind(this),
this.updateAfterMutationTimeoutDelay
);
}
/**
* Handles observed DOM mutations related to an autofill element attribute.
* @param {MutationRecord} mutation
* @private
*/
private handleAutofillElementAttributeMutation(mutation: MutationRecord) {
const targetElement = mutation.target;
if (!(targetElement instanceof HTMLElement)) {
return;
}
const attributeName = mutation.attributeName?.toLowerCase();
const autofillForm = this.autofillFormElements.get(
targetElement as ElementWithOpId<HTMLFormElement>
);
if (autofillForm) {
this.updateAutofillFormElementData(
attributeName,
targetElement as ElementWithOpId<HTMLFormElement>,
autofillForm
);
return;
}
const autofillField = this.autofillFieldElements.get(
targetElement as ElementWithOpId<FormFieldElement>
);
if (!autofillField) {
return;
}
this.updateAutofillFieldElementData(
attributeName,
targetElement as ElementWithOpId<FormFieldElement>,
autofillField
);
}
/**
* Updates the autofill form element data based on the passed attribute name.
* @param {string} attributeName
* @param {ElementWithOpId<HTMLFormElement>} element
* @param {AutofillForm} dataTarget
* @private
*/
private updateAutofillFormElementData(
attributeName: string,
element: ElementWithOpId<HTMLFormElement>,
dataTarget: AutofillForm
) {
const updateAttribute = (dataTargetKey: string) => {
this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey });
};
const updateActions: Record<string, CallableFunction> = {
action: () => (dataTarget.htmlAction = this.getFormActionAttribute(element)),
name: () => updateAttribute("htmlName"),
id: () => updateAttribute("htmlID"),
method: () => updateAttribute("htmlMethod"),
};
if (!updateActions[attributeName]) {
return;
}
updateActions[attributeName]();
this.autofillFormElements.set(element, dataTarget);
}
/**
* Updates the autofill field element data based on the passed attribute name.
* @param {string} attributeName
* @param {ElementWithOpId<FormFieldElement>} element
* @param {AutofillField} dataTarget
* @returns {Promise<void>}
* @private
*/
private async updateAutofillFieldElementData(
attributeName: string,
element: ElementWithOpId<FormFieldElement>,
dataTarget: AutofillField
) {
const updateAttribute = (dataTargetKey: string) => {
this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey });
};
const updateActions: Record<string, CallableFunction> = {
maxlength: () => (dataTarget.maxLength = this.getAutofillFieldMaxLength(element)),
id: () => updateAttribute("htmlID"),
name: () => updateAttribute("htmlName"),
class: () => updateAttribute("htmlClass"),
tabindex: () => updateAttribute("tabindex"),
title: () => updateAttribute("tabindex"),
rel: () => updateAttribute("rel"),
tagname: () => (dataTarget.tagName = this.getAttributeLowerCase(element, "tagName")),
type: () => (dataTarget.type = this.getAttributeLowerCase(element, "type")),
value: () => (dataTarget.value = this.getElementValue(element)),
checked: () => (dataTarget.checked = this.getAttributeBoolean(element, "checked")),
disabled: () => (dataTarget.disabled = this.getAttributeBoolean(element, "disabled")),
readonly: () => (dataTarget.readonly = this.getAttributeBoolean(element, "readonly")),
autocomplete: () => (dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
"data-label": () => updateAttribute("label-data"),
"aria-label": () => updateAttribute("label-aria"),
"aria-hidden": () =>
(dataTarget["aria-hidden"] = this.getAttributeBoolean(element, "aria-hidden", true)),
"aria-disabled": () =>
(dataTarget["aria-disabled"] = this.getAttributeBoolean(element, "aria-disabled", true)),
"aria-haspopup": () =>
(dataTarget["aria-haspopup"] = this.getAttributeBoolean(element, "aria-haspopup", true)),
"data-stripe": () => updateAttribute("data-stripe"),
};
if (!updateActions[attributeName]) {
return;
}
updateActions[attributeName]();
const visibilityAttributesSet = new Set(["class", "style"]);
if (
visibilityAttributesSet.has(attributeName) &&
!dataTarget.htmlClass?.includes("com-bitwarden-browser-animated-fill")
) {
dataTarget.viewable = await this.domElementVisibilityService.isFormFieldViewable(element);
}
this.autofillFieldElements.set(element, dataTarget);
}
/**
* Gets the attribute value for the passed element, and returns it. If the dataTarget
* and dataTargetKey are passed, it will set the value of the dataTarget[dataTargetKey].
* @param UpdateAutofillDataAttributeParams
* @returns {string}
* @private
*/
private updateAutofillDataAttribute({
element,
attributeName,
dataTarget,
dataTargetKey,
}: UpdateAutofillDataAttributeParams) {
const attributeValue = this.getPropertyOrAttribute(element, attributeName);
if (dataTarget && dataTargetKey) {
dataTarget[dataTargetKey] = attributeValue;
}
return attributeValue;
}
}
export default CollectAutofillContentService;

View File

@ -13,7 +13,6 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac
*/
async isFormFieldViewable(element: FormFieldElement): Promise<boolean> {
const elementBoundingClientRect = element.getBoundingClientRect();
if (
this.isElementOutsideViewportBounds(element, elementBoundingClientRect) ||
this.isElementHiddenByCss(element)
@ -176,7 +175,10 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac
): boolean {
const elementBoundingClientRect =
targetElementBoundingClientRect || targetElement.getBoundingClientRect();
const elementAtCenterPoint = targetElement.ownerDocument.elementFromPoint(
const elementRootNode = targetElement.getRootNode();
const rootElement =
elementRootNode instanceof ShadowRoot ? elementRootNode : targetElement.ownerDocument;
const elementAtCenterPoint = rootElement.elementFromPoint(
elementBoundingClientRect.left + elementBoundingClientRect.width / 2,
elementBoundingClientRect.top + elementBoundingClientRect.height / 2
);

View File

@ -82,7 +82,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
if (
!savedUrls?.some((url) => url.startsWith(`https://${window.location.hostname}`)) ||
window.location.protocol !== "http:" ||
!document.querySelectorAll("input[type=password]")?.length
!this.isPasswordFieldWithinDocument()
) {
return false;
}
@ -95,6 +95,22 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
return !confirm(confirmationWarning);
}
/**
* Checks if there is a password field within the current document. Includes
* password fields that are present within the shadow DOM.
* @returns {boolean}
* @private
*/
private isPasswordFieldWithinDocument(): boolean {
return Boolean(
this.collectAutofillContentService.queryAllTreeWalkerNodes(
document.documentElement,
(node: Node) => node instanceof HTMLInputElement && node.type === "password",
false
)?.length
);
}
/**
* Checking if the autofill is occurring within an untrusted iframe. If the page is within an untrusted iframe,
* the user is prompted to confirm that they want to autofill on the page. If the user cancels the autofill,