1
- import ContentFeature from '../content-feature' ;
2
- import { isBeingFramed , withExponentialBackoff } from '../utils' ;
1
+ import { isBeingFramed , withRetry } from '../utils' ;
2
+ import { ActionExecutorBase } from './broker-protection' ;
3
+ import { ErrorResponse } from './broker-protection/types' ;
3
4
4
5
export const ANIMATION_DURATION_MS = 1000 ;
5
6
export const ANIMATION_ITERATIONS = Infinity ;
6
7
export const BACKGROUND_COLOR_START = 'rgba(85, 127, 243, 0.10)' ;
7
8
export const BACKGROUND_COLOR_END = 'rgba(85, 127, 243, 0.25)' ;
8
9
export const OVERLAY_ID = 'ddg-password-import-overlay' ;
9
10
export const DELAY_BEFORE_ANIMATION = 300 ;
11
+ const TAKEOUT_DOWNLOAD_URL_BASE = '/takeout/download' ;
10
12
11
13
/**
12
14
* @typedef ButtonAnimationStyle
@@ -33,7 +35,7 @@ export const DELAY_BEFORE_ANIMATION = 300;
33
35
* 2. Find the element to animate based on the path - using structural selectors first and then fallback to label texts),
34
36
* 3. Animate the element, or tap it if it should be autotapped.
35
37
*/
36
- export default class AutofillPasswordImport extends ContentFeature {
38
+ export default class AutofillImport extends ActionExecutorBase {
37
39
#exportButtonSettings;
38
40
39
41
#settingsButtonSettings;
@@ -53,6 +55,12 @@ export default class AutofillPasswordImport extends ContentFeature {
53
55
54
56
#domLoaded;
55
57
58
+ #exportId;
59
+
60
+ #processingBookmark;
61
+
62
+ #isBookmarkModalVisible = false ;
63
+
56
64
/** @type {WeakSet<Element> } */
57
65
#tappedElements = new WeakSet ( ) ;
58
66
@@ -135,10 +143,10 @@ export default class AutofillPasswordImport extends ContentFeature {
135
143
/**
136
144
* @returns {Promise<Element|HTMLElement|null> }
137
145
*/
138
- async runWithRetry ( fn ) {
146
+ async runWithRetry ( fn , maxAttempts = 4 , delay = 500 , strategy = 'exponential' ) {
139
147
try {
140
- return await withExponentialBackoff ( fn ) ;
141
- } catch {
148
+ return await withRetry ( fn , maxAttempts , delay , strategy ) ;
149
+ } catch ( error ) {
142
150
return null ;
143
151
}
144
152
}
@@ -457,20 +465,53 @@ export default class AutofillPasswordImport extends ContentFeature {
457
465
] . includes ( path ) ;
458
466
}
459
467
460
- async handlePath ( path ) {
468
+ async handlePasswordManagerPath ( pathname ) {
461
469
this . removeOverlayIfNeeded ( ) ;
462
- if ( this . isSupportedPath ( path ) ) {
470
+ if ( this . isSupportedPath ( pathname ) ) {
463
471
try {
464
- this . setCurrentElementConfig ( await this . getElementAndStyleFromPath ( path ) ) ;
472
+ this . setCurrentElementConfig ( await this . getElementAndStyleFromPath ( pathname ) ) ;
465
473
if ( this . currentElementConfig ?. element && ! this . #tappedElements. has ( this . currentElementConfig ?. element ) ) {
466
474
await this . animateOrTapElement ( ) ;
467
475
if ( this . currentElementConfig ?. shouldTap && this . currentElementConfig ?. tapOnce ) {
468
476
this . #tappedElements. add ( this . currentElementConfig . element ) ;
469
477
}
470
478
}
471
479
} catch {
472
- console . error ( 'password-import: failed for path:' , path ) ;
480
+ console . error ( 'password-import: failed for path:' , pathname ) ;
481
+ }
482
+ }
483
+ }
484
+
485
+ /**
486
+ * @returns {Array<Record<string, any>> }
487
+ */
488
+ get bookmarkImportActionSettings ( ) {
489
+ return this . getFeatureSetting ( 'actions' ) || [ ] ;
490
+ }
491
+
492
+ /**
493
+ * @returns {Record<string, string> }
494
+ */
495
+ get bookmarkImportSelectorSettings ( ) {
496
+ return this . getFeatureSetting ( 'selectors' ) ;
497
+ }
498
+
499
+ /**
500
+ * @param {Location } location
501
+ *
502
+ */
503
+ async handleLocation ( location ) {
504
+ const { pathname } = location ;
505
+ if ( this . bookmarkImportActionSettings . length > 0 ) {
506
+ if ( this . #processingBookmark) {
507
+ return ;
473
508
}
509
+ this . #processingBookmark = true ;
510
+ await this . handleBookmarkImportPath ( pathname ) ;
511
+ } else if ( this . getFeatureSetting ( 'settingsButton' ) ) {
512
+ await this . handlePasswordManagerPath ( pathname ) ;
513
+ } else {
514
+ // Unknown feature, we bail out
474
515
}
475
516
}
476
517
@@ -547,24 +588,98 @@ export default class AutofillPasswordImport extends ContentFeature {
547
588
return `${ this . #settingsButtonSettings?. selectors ?. join ( ',' ) } , ${ this . settingsLabelTextSelector } ` ;
548
589
}
549
590
550
- setButtonSettings ( ) {
591
+ /** Bookmark import code */
592
+ async downloadData ( ) {
593
+ // sleep for a second, sometimes download link is not yet available
594
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
595
+
596
+ const userId = document . querySelector ( this . bookmarkImportSelectorSettings . userIdLink ) ?. getAttribute ( 'href' ) ?. split ( '&user=' ) [ 1 ] ;
597
+ await this . runWithRetry ( ( ) => document . querySelector ( `a[href="./manage/archive/${ this . #exportId} "]` ) , 15 , 2000 , 'linear' ) ;
598
+ if ( userId != null && this . #exportId != null ) {
599
+ const downloadURL = `${ TAKEOUT_DOWNLOAD_URL_BASE } ?j=${ this . #exportId} &i=0&user=${ userId } ` ;
600
+ window . location . href = downloadURL ;
601
+ } else {
602
+ // If there's no user id or export id, we post an action failed message
603
+ this . postBookmarkImportMessage ( 'actionCompleted' , {
604
+ result : new ErrorResponse ( {
605
+ actionID : 'download-data' ,
606
+ message : 'No user id or export id found' ,
607
+ } ) ,
608
+ } ) ;
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Here we ignore the action and return a default retry config
614
+ * as for now the retry doesn't need to be per action.
615
+ */
616
+ retryConfigFor ( _ ) {
617
+ return {
618
+ interval : { ms : 1000 } ,
619
+ maxAttempts : 30 ,
620
+ } ;
621
+ }
622
+
623
+ postBookmarkImportMessage ( name , data ) {
624
+ globalThis . ddgBookmarkImport ?. postMessage (
625
+ JSON . stringify ( {
626
+ name,
627
+ data,
628
+ } ) ,
629
+ ) ;
630
+ }
631
+
632
+ patchMessagingAndProcessAction ( action ) {
633
+ // Ideally we should be usuing standard messaging in Android, but we are not ready yet
634
+ // So just patching the notify method to post a message to the Android side
635
+ this . messaging . notify = this . postBookmarkImportMessage . bind ( this ) ;
636
+ return this . processActionAndNotify ( action , { } ) ;
637
+ }
638
+
639
+ async handleBookmarkImportPath ( pathname ) {
640
+ if ( pathname === '/' && ! this . #isBookmarkModalVisible) {
641
+ for ( const action of this . bookmarkImportActionSettings ) {
642
+ // Before clicking on the manage button, we need to store the export id
643
+ if ( action . id === 'manage-button-click' ) {
644
+ await this . storeExportId ( ) ;
645
+ }
646
+
647
+ await this . patchMessagingAndProcessAction ( action ) ;
648
+ }
649
+ await this . downloadData ( ) ;
650
+ }
651
+ }
652
+
653
+ setPasswordImportSettings ( ) {
551
654
this . #exportButtonSettings = this . getFeatureSetting ( 'exportButton' ) ;
552
655
this . #signInButtonSettings = this . getFeatureSetting ( 'signInButton' ) ;
553
656
this . #settingsButtonSettings = this . getFeatureSetting ( 'settingsButton' ) ;
554
657
this . #exportConfirmButtonSettings = this . getFeatureSetting ( 'exportConfirmButton' ) ;
555
658
}
556
659
660
+ findExportId ( ) {
661
+ const panels = document . querySelectorAll ( this . bookmarkImportSelectorSettings . tabPanel ) ;
662
+ const exportPanel = panels [ panels . length - 1 ] ;
663
+ return exportPanel . querySelector ( 'div[data-archive-id]' ) ?. getAttribute ( 'data-archive-id' ) ;
664
+ }
665
+
666
+ async storeExportId ( ) {
667
+ this . #exportId = await this . runWithRetry ( ( ) => this . findExportId ( ) , 30 , 1000 , 'linear' ) ;
668
+ }
669
+
557
670
urlChanged ( ) {
558
- this . handlePath ( window . location . pathname ) ;
671
+ this . handleLocation ( window . location ) ;
559
672
}
560
673
561
674
init ( ) {
562
675
if ( isBeingFramed ( ) ) {
563
676
return ;
564
677
}
565
- this . setButtonSettings ( ) ;
566
678
567
- const handlePath = this . handlePath . bind ( this ) ;
679
+ if ( this . getFeatureSetting ( 'settingsButton' ) ) {
680
+ this . setPasswordImportSettings ( ) ;
681
+ }
682
+ const handleLocation = this . handleLocation . bind ( this ) ;
568
683
569
684
this . #domLoaded = new Promise ( ( resolve ) => {
570
685
if ( document . readyState !== 'loading' ) {
@@ -578,8 +693,7 @@ export default class AutofillPasswordImport extends ContentFeature {
578
693
async ( ) => {
579
694
// @ts -expect-error - caller doesn't expect a value here
580
695
resolve ( ) ;
581
- const path = window . location . pathname ;
582
- await handlePath ( path ) ;
696
+ await handleLocation ( window . location ) ;
583
697
} ,
584
698
{ once : true } ,
585
699
) ;
0 commit comments