Skip to content

Commit b281096

Browse files
committed
wip: reuse dbp
1 parent 5a94b61 commit b281096

File tree

3 files changed

+211
-55
lines changed

3 files changed

+211
-55
lines changed

injected/src/features/autofill-password-import.js

Lines changed: 209 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ContentFeature from '../content-feature';
22
import { isBeingFramed, withExponentialBackoff } from '../utils';
3+
import { click } from './broker-protection/actions/click';
34

45
export const ANIMATION_DURATION_MS = 1000;
56
export const ANIMATION_ITERATIONS = Infinity;
@@ -55,6 +56,8 @@ export default class AutofillPasswordImport extends ContentFeature {
5556

5657
#exportId;
5758

59+
#processingBookmark;
60+
5861
#isBookmarkModalVisible = false;
5962
#isBookmarkProcessed = false;
6063

@@ -137,6 +140,59 @@ export default class AutofillPasswordImport extends ContentFeature {
137140
return this.#domLoaded;
138141
}
139142

143+
async runWithRetry(fn, maxAttempts = 4, delay = 500) {
144+
try {
145+
return await withExponentialBackoff(fn, maxAttempts, delay);
146+
} catch (error) {
147+
return null;
148+
}
149+
}
150+
151+
/**
152+
* Wait for an element's attribute to change using mutation observer
153+
* @param {string} selector - CSS selector for the element
154+
* @param {string} attribute - Attribute to watch
155+
* @param {Function} condition - Function that returns true when condition is met
156+
* @returns {Promise<void>}
157+
*/
158+
async waitForAttributeChange(selector, attribute, condition) {
159+
return new Promise((resolve, reject) => {
160+
const element = document.querySelector(selector);
161+
if (!element) {
162+
reject(new Error(`Element with selector "${selector}" not found`));
163+
return;
164+
}
165+
166+
// Check if condition is already met
167+
if (condition(element)) {
168+
resolve();
169+
return;
170+
}
171+
172+
const observer = new MutationObserver((mutations) => {
173+
mutations.forEach((mutation) => {
174+
if (mutation.type === 'attributes' && mutation.attributeName === attribute) {
175+
if (condition(element)) {
176+
observer.disconnect();
177+
resolve();
178+
}
179+
}
180+
});
181+
});
182+
183+
observer.observe(element, {
184+
attributes: true,
185+
attributeFilter: [attribute]
186+
});
187+
188+
// Fallback timeout after 10 seconds
189+
setTimeout(() => {
190+
observer.disconnect();
191+
reject(new Error(`Timeout waiting for attribute "${attribute}" to change`));
192+
}, 10000);
193+
});
194+
}
195+
140196
/**
141197
* Takes a path and returns the element and style to animate.
142198
* @param {string} path
@@ -429,22 +485,38 @@ export default class AutofillPasswordImport extends ContentFeature {
429485
}
430486

431487
async downloadData() {
432-
const userId = document.querySelector('a[href*="&user="]')?.getAttribute('href')?.split('&user=')[1];
488+
const userId = document.querySelector(this.userIdSelector)?.getAttribute('href')?.split('&user=')[1];
433489
console.log('DEEP DEBUG autofill-password-import: userId', userId);
434-
await withExponentialBackoff(() => document.querySelector(`a[href="./manage/archive/${this.#exportId}"]`), 8);
490+
await this.runWithRetry(() => document.querySelector(`a[href="./manage/archive/${this.#exportId}"]`), 8);
435491
const downloadURL = `${TAKEOUT_DOWNLOAD_URL_BASE}?j=${this.#exportId}&i=0&user=${userId}`;
436492
window.location.href = downloadURL;
437493
}
438494

439495
async handleBookmarkImportPath(pathname) {
440496
console.log('DEEP DEBUG autofill-password-import: handleBookmarkImportPath', pathname);
441497
if (pathname === '/' && !this.#isBookmarkModalVisible) {
442-
await this.clickDisselectAllButton();
498+
console.log('DEEP DEBUG autofill-password-import: handleBookmarkImportPath', this.disselectAllButtonSelector);
499+
click({
500+
id: 'disselect-all-click',
501+
actionType: 'click',
502+
elements: [{
503+
type: 'element',
504+
selector: this.disselectAllButtonSelector
505+
}]
506+
}, {}, /** @type HTMLElement */ (this.getRoot(this.tabPanelSelector)))
507+
await this.scrollToChromeSection();
508+
await this.openBookmarkModal();
443509
await this.selectBookmark();
444-
this.startExportProcess();
510+
await this.startExportProcess();
445511
await this.storeExportId();
446-
const manageButton = /** @type HTMLAnchorElement */ (document.querySelector('a[href="manage"]'));
447-
manageButton?.click();
512+
click({
513+
id: 'manage-button-click',
514+
actionType: 'click',
515+
elements: [{
516+
type: 'element',
517+
selector: this.manageButtonSelector
518+
}]
519+
}, {}, document)
448520
await this.downloadData();
449521
}
450522
}
@@ -456,6 +528,10 @@ export default class AutofillPasswordImport extends ContentFeature {
456528
async handleLocation(location) {
457529
const { pathname, hostname } = location;
458530
if (hostname === BOOKMARK_IMPORT_DOMAIN) {
531+
if (this.#processingBookmark) {
532+
return;
533+
}
534+
this.#processingBookmark = true;
459535
this.handleBookmarkImportPath(pathname);
460536
} else {
461537
await this.handlePasswordManagerPath(pathname);
@@ -526,39 +602,76 @@ export default class AutofillPasswordImport extends ContentFeature {
526602
return `${this.#settingsButtonSettings?.selectors?.join(',')}, ${this.settingsLabelTextSelector}`;
527603
}
528604

605+
get manageButtonSelector() {
606+
return 'a[href="manage"]';
607+
}
608+
609+
get userIdSelector() {
610+
return 'a[href*="&user="]';
611+
}
612+
529613
setButtonSettings() {
530614
this.#exportButtonSettings = this.getFeatureSetting('exportButton');
531615
this.#signInButtonSettings = this.getFeatureSetting('signInButton');
532616
this.#settingsButtonSettings = this.getFeatureSetting('settingsButton');
533617
}
534618

535-
/** Bookmark import code */
619+
/* ****************************** Bookmark import code ****************************** */
620+
621+
getRoot(selector) {
622+
return /** @type HTMLElement */ (document.querySelector(selector)) ?? document;
623+
}
624+
536625
get disselectAllButtonSelector() {
537-
return 'c-wiz[data-node-index="4;0"] button';
626+
return `${this.tabPanelSelector} div:nth-child(2) div:nth-child(2)`;
627+
}
628+
629+
get bookmarkModalSelector() {
630+
return 'fieldset.rcetic';
538631
}
539632

540633
get bookmarkSelectButtonSelector() {
541-
return 'fieldset.rcetic input';
634+
return `${this.bookmarkModalSelector} input`;
635+
}
636+
637+
get chromeInputCheckboxSelector() {
638+
return `${this.tabPanelSelector} div:nth-child(10) input[type="checkbox"]`;
542639
}
543640

544641
get chromeSectionSelector() {
545642
return 'c-wiz [data-id="chrome"]';
546643
}
547644

645+
get chromeDataButtonSelector() {
646+
return `${this.tabPanelSelector} div:nth-child(10) > div:nth-child(2) > div:nth-child(2) button`;
647+
}
648+
548649
get nextStepButtonSelector() {
549-
return 'div[data-jobid] > div:nth-of-type(2) button';
650+
return `${this.tabPanelSelector} > div:nth-child(1) > div:nth-child(2) button`;
550651
}
551652

552653
get createExportButtonSelector() {
553-
return 'div[data-configure-step] button';
654+
return 'div[data-configure-step="1"] button';
655+
}
656+
657+
get tabPanelSelector() {
658+
return 'div[role="tabpanel"]';
659+
}
660+
661+
get inputCheckboxSelector() {
662+
return `${this.tabPanelSelector} input[type="checkbox"]`;
663+
}
664+
665+
get okButtonSelector() {
666+
return 'div[isfullscreen] div:nth-child(3) div:last-child';
554667
}
555668

556-
async findDisselectAllButton() {
557-
return await withExponentialBackoff(() => document.querySelectorAll(this.disselectAllButtonSelector)[1]);
669+
get bookmarkCheckboxSelector() {
670+
return `${this.bookmarkModalSelector} div:nth-child(3) > div:nth-of-type(2) input`;
558671
}
559672

560673
async findExportId() {
561-
const panels = document.querySelectorAll('div[role="tabpanel"]');
674+
const panels = document.querySelectorAll(this.tabPanelSelector);
562675
const exportPanel = panels[panels.length - 1];
563676
return await withExponentialBackoff(() => exportPanel.querySelector('div[data-archive-id]')?.getAttribute('data-archive-id'));
564677
}
@@ -569,72 +682,114 @@ export default class AutofillPasswordImport extends ContentFeature {
569682
}
570683

571684
startExportProcess() {
572-
const nextStepButton = /** @type HTMLButtonElement */ (document.querySelectorAll(this.nextStepButtonSelector)[0]);
573-
nextStepButton?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
574-
nextStepButton?.click();
685+
click({
686+
id: 'next-step-button-click',
687+
actionType: 'click',
688+
elements: [{
689+
type: 'element',
690+
selector: this.nextStepButtonSelector
691+
}]
692+
}, {}, document)
693+
694+
click({
695+
id: 'create-export-button-click',
696+
actionType: 'click',
697+
elements: [{
698+
type: 'element',
699+
selector: this.createExportButtonSelector
700+
}]
701+
}, {}, document)
702+
}
703+
704+
async openBookmarkModal() {
705+
if (this.#isBookmarkProcessed || this.#isBookmarkModalVisible) {
706+
return;
707+
}
708+
709+
await this.runWithRetry(() => {
710+
const element = /** @type HTMLButtonElement */ (document.querySelector(this.chromeDataButtonSelector));
711+
return element?.checkVisibility();
712+
});
575713

576-
const createExportButton = /** @type HTMLButtonElement */ (document.querySelectorAll(this.createExportButtonSelector)[0]);
577-
createExportButton?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
578-
createExportButton?.click();
714+
click({
715+
id: 'select-bookmark-click',
716+
actionType: 'click',
717+
elements: [{
718+
type: 'element',
719+
selector: this.chromeDataButtonSelector
720+
}]
721+
}, {}, document);
722+
await this.runWithRetry(() => document.querySelector(this.bookmarkModalSelector) != null)
723+
this.#isBookmarkModalVisible = true;
579724
}
580725

581726
async selectBookmark() {
582727
if (this.#isBookmarkProcessed) {
583728
return;
584729
}
585-
const chromeDataButtonSelector = `${this.chromeSectionSelector} button`;
586-
const chromeDataButton = /** @type HTMLButtonElement */ (
587-
await withExponentialBackoff(() => document.querySelectorAll(chromeDataButtonSelector)[1], 5)
588-
);
589-
chromeDataButton?.focus();
590-
chromeDataButton?.click();
591-
this.#isBookmarkModalVisible = true;
592-
await this.domLoaded;
593-
const disselectAllButton = /** @type HTMLButtonElement */ (
594-
await withExponentialBackoff(() => document.querySelectorAll('fieldset.rcetic button')[1])
595-
);
596730

597-
disselectAllButton?.click();
731+
const disselectSelector = `${this.bookmarkModalSelector} div:nth-child(2) button:nth-of-type(2)`;
598732

599-
const bookmarkSelectButton = /** @type HTMLInputElement */ (
600-
await withExponentialBackoff(() => document.querySelectorAll(this.bookmarkSelectButtonSelector)[1])
601-
);
733+
click({
734+
id: 'bookmark-disselect-all-click',
735+
actionType: 'click',
736+
elements: [{
737+
type: 'element',
738+
selector: disselectSelector
739+
}]
740+
}, {}, this.getRoot(this.bookmarkModalSelector))
602741

603-
await withExponentialBackoff(() => !bookmarkSelectButton?.checked);
742+
await this.runWithRetry(() => {
743+
const element = /** @type HTMLInputElement */ (document.querySelector(this.bookmarkCheckboxSelector));
744+
return !element.checked;
745+
});
604746

605-
bookmarkSelectButton?.click();
747+
click({
748+
id: 'bookmark-checkbox-click',
749+
actionType: 'click',
750+
elements: [{
751+
type: 'element',
752+
selector: this.bookmarkCheckboxSelector
753+
}]
754+
}, {}, document)
755+
756+
await this.runWithRetry(() => {
757+
const element = /** @type HTMLInputElement */ (document.querySelector(this.bookmarkCheckboxSelector));
758+
return element?.checked;
759+
});
606760

607-
const okButton = /** @type HTMLButtonElement */ (document.querySelectorAll('div[role="button"]')[7]);
761+
await this.runWithRetry(() => {
762+
const okButton = /** @type HTMLButtonElement */ (document.querySelector(this.okButtonSelector));
763+
return okButton?.ariaDisabled !== 'true';
764+
});
608765

609-
await withExponentialBackoff(() => okButton.ariaDisabled !== 'true');
766+
click({
767+
id: 'bookmark-ok-button-click',
768+
actionType: 'click',
769+
elements: [{
770+
type: 'element',
771+
selector: this.okButtonSelector
772+
}]
773+
}, {}, document)
610774

611-
okButton?.click();
612775
this.#isBookmarkModalVisible = false;
613776
this.#isBookmarkProcessed = true;
614777
}
615778

616-
async clickDisselectAllButton() {
617-
const element = /** @type HTMLButtonElement */ (await this.findDisselectAllButton());
618-
console.log('Deep element', element);
619-
if (element != null) {
620-
element.click();
621-
}
622-
779+
async scrollToChromeSection() {
623780
const chromeSectionElement = /** @type HTMLInputElement */ (
624-
await withExponentialBackoff(() => document.querySelectorAll(this.chromeSectionSelector)[0].querySelector('input'))
781+
await this.runWithRetry(() => document.querySelectorAll(this.chromeSectionSelector)[0].querySelector('input'))
625782
);
626783
console.log('DEEP chromeSectionElement', chromeSectionElement);
627784

628-
// First wait for the element to become unchecked (due to slow disselection)
629-
await withExponentialBackoff(() => !chromeSectionElement?.checked);
630-
631-
chromeSectionElement.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
785+
await this.runWithRetry(() => !chromeSectionElement.checked);
632786

787+
chromeSectionElement.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
633788
chromeSectionElement?.click();
634789
}
635790

636-
urlChanged() {
637-
console.log('DEEP DEBUG autofill-password-import: urlChanged', window.location);
791+
urlChanged(navigationType) {
792+
console.log('DEEP DEBUG autofill-password-import: urlChanged', window.location.pathname, navigationType);
638793
this.handleLocation(window.location);
639794
}
640795

injected/src/features/broker-protection/actions/click.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { processTemplateStringWithUserData } from './build-url-transforms.js';
77
* @param {Record<string, any>} action
88
* @param {Record<string, any>} userData
99
* @param {Document | HTMLElement} root
10-
* @return {import('../types.js').ActionResponse}
10+
* @return {import('../types.js').ActionResponse}
1111
*/
1212
export function click(action, userData, root = document) {
1313
/** @type {Array<any> | null} */

injected/src/features/broker-protection/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* @property {string} [captchaType]
1313
* @property {string} [injectCaptchaHandler]
1414
* @property {string} [dataSource]
15+
* @property {string} [url]
1516
*/
1617

1718
/**

0 commit comments

Comments
 (0)