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' ;
34
45export const ANIMATION_DURATION_MS = 1000 ;
56export const ANIMATION_ITERATIONS = Infinity ;
67export const BACKGROUND_COLOR_START = 'rgba(85, 127, 243, 0.10)' ;
78export const BACKGROUND_COLOR_END = 'rgba(85, 127, 243, 0.25)' ;
89export const OVERLAY_ID = 'ddg-password-import-overlay' ;
910export const DELAY_BEFORE_ANIMATION = 300 ;
11+ const TAKEOUT_DOWNLOAD_URL_BASE = '/takeout/download' ;
1012
1113/**
1214 * @typedef ButtonAnimationStyle
@@ -33,7 +35,7 @@ export const DELAY_BEFORE_ANIMATION = 300;
3335 * 2. Find the element to animate based on the path - using structural selectors first and then fallback to label texts),
3436 * 3. Animate the element, or tap it if it should be autotapped.
3537 */
36- export default class AutofillPasswordImport extends ContentFeature {
38+ export default class AutofillImport extends ActionExecutorBase {
3739 #exportButtonSettings;
3840
3941 #settingsButtonSettings;
@@ -53,6 +55,12 @@ export default class AutofillPasswordImport extends ContentFeature {
5355
5456 #domLoaded;
5557
58+ #exportId;
59+
60+ #processingBookmark;
61+
62+ #isBookmarkModalVisible = false ;
63+
5664 /** @type {WeakSet<Element> } */
5765 #tappedElements = new WeakSet ( ) ;
5866
@@ -135,10 +143,10 @@ export default class AutofillPasswordImport extends ContentFeature {
135143 /**
136144 * @returns {Promise<Element|HTMLElement|null> }
137145 */
138- async runWithRetry ( fn ) {
146+ async runWithRetry ( fn , maxAttempts = 4 , delay = 500 , strategy = 'exponential' ) {
139147 try {
140- return await withExponentialBackoff ( fn ) ;
141- } catch {
148+ return await withRetry ( fn , maxAttempts , delay , strategy ) ;
149+ } catch ( error ) {
142150 return null ;
143151 }
144152 }
@@ -457,20 +465,53 @@ export default class AutofillPasswordImport extends ContentFeature {
457465 ] . includes ( path ) ;
458466 }
459467
460- async handlePath ( path ) {
468+ async handlePasswordManagerPath ( pathname ) {
461469 this . removeOverlayIfNeeded ( ) ;
462- if ( this . isSupportedPath ( path ) ) {
470+ if ( this . isSupportedPath ( pathname ) ) {
463471 try {
464- this . setCurrentElementConfig ( await this . getElementAndStyleFromPath ( path ) ) ;
472+ this . setCurrentElementConfig ( await this . getElementAndStyleFromPath ( pathname ) ) ;
465473 if ( this . currentElementConfig ?. element && ! this . #tappedElements. has ( this . currentElementConfig ?. element ) ) {
466474 await this . animateOrTapElement ( ) ;
467475 if ( this . currentElementConfig ?. shouldTap && this . currentElementConfig ?. tapOnce ) {
468476 this . #tappedElements. add ( this . currentElementConfig . element ) ;
469477 }
470478 }
471479 } 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 ;
473508 }
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
474515 }
475516 }
476517
@@ -547,24 +588,98 @@ export default class AutofillPasswordImport extends ContentFeature {
547588 return `${ this . #settingsButtonSettings?. selectors ?. join ( ',' ) } , ${ this . settingsLabelTextSelector } ` ;
548589 }
549590
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 ( ) {
551654 this . #exportButtonSettings = this . getFeatureSetting ( 'exportButton' ) ;
552655 this . #signInButtonSettings = this . getFeatureSetting ( 'signInButton' ) ;
553656 this . #settingsButtonSettings = this . getFeatureSetting ( 'settingsButton' ) ;
554657 this . #exportConfirmButtonSettings = this . getFeatureSetting ( 'exportConfirmButton' ) ;
555658 }
556659
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+
557670 urlChanged ( ) {
558- this . handlePath ( window . location . pathname ) ;
671+ this . handleLocation ( window . location ) ;
559672 }
560673
561674 init ( ) {
562675 if ( isBeingFramed ( ) ) {
563676 return ;
564677 }
565- this . setButtonSettings ( ) ;
566678
567- const handlePath = this . handlePath . bind ( this ) ;
679+ if ( this . getFeatureSetting ( 'settingsButton' ) ) {
680+ this . setPasswordImportSettings ( ) ;
681+ }
682+ const handleLocation = this . handleLocation . bind ( this ) ;
568683
569684 this . #domLoaded = new Promise ( ( resolve ) => {
570685 if ( document . readyState !== 'loading' ) {
@@ -578,8 +693,7 @@ export default class AutofillPasswordImport extends ContentFeature {
578693 async ( ) => {
579694 // @ts -expect-error - caller doesn't expect a value here
580695 resolve ( ) ;
581- const path = window . location . pathname ;
582- await handlePath ( path ) ;
696+ await handleLocation ( window . location ) ;
583697 } ,
584698 { once : true } ,
585699 ) ;
0 commit comments