refactor(gui): remove JavaScript window.open interception
Remove ~280 lines of window.open interception code: - WindowOpenDebouncer class and deduplication logic - User interaction tracking - Fake window proxy creation - WindowIntent message passing JavaScript no longer intercepts window.open - WebKit's native create signal handles it properly at the browser layer.
- commit
- c21700deb0f3fed87710151485f4b8cc9f8d4020
- author
- bamen · · 2025-10-12 07:05 UTC
- parent
- dbdc6c0
- signature
- unsigned
1 file · +0 -320
M gui/src/injected/main-world.ts +0 -320
| @@ -14,7 +14,6 @@ declare global { | |||
| 14 | 14 | interface Window { | |
| 15 | 15 | __dumber: DumberAPI; | |
| 16 | 16 | __dumber_page_bridge_installed?: boolean; | |
| 17 | - | __dumber_window_open_intercepted?: boolean; | |
| 18 | 17 | __dumber_dom_zoom_seed?: number; | |
| 19 | 18 | __dumber_dom_zoom_level?: number; | |
| 20 | 19 | __dumber_initial_theme?: string; | |
| @@ -46,16 +45,6 @@ declare global { | |||
| 46 | 45 | } | |
| 47 | 46 | } | |
| 48 | 47 | ||
| 49 | - | interface WindowIntent { | |
| 50 | - | url: string; | |
| 51 | - | target: string; | |
| 52 | - | features: string; | |
| 53 | - | timestamp: number; | |
| 54 | - | userTriggered: boolean; | |
| 55 | - | requestId: string; | |
| 56 | - | windowType: string; // "tab", "popup", "unknown" | |
| 57 | - | } | |
| 58 | - | ||
| 59 | 48 | interface Suggestion { | |
| 60 | 49 | // Define proper suggestion types based on your needs | |
| 61 | 50 | [key: string]: unknown; | |
| @@ -75,36 +64,6 @@ interface DumberAPI { | |||
| 75 | 64 | }; | |
| 76 | 65 | } | |
| 77 | 66 | ||
| 78 | - | // Function to detect if window.open call is for a popup based on features | |
| 79 | - | function detectWindowType(features?: string | null): string { | |
| 80 | - | if (!features) { | |
| 81 | - | return "tab"; // Default to tab if no features specified | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | const featuresStr = features.toLowerCase(); | |
| 85 | - | ||
| 86 | - | // Check for typical popup characteristics | |
| 87 | - | const hasSize = /width=\d+|height=\d+/.test(featuresStr); | |
| 88 | - | const hasNoToolbar = /toolbar=0|toolbar=no/.test(featuresStr); | |
| 89 | - | const hasNoMenubar = /menubar=0|menubar=no/.test(featuresStr); | |
| 90 | - | const hasNoLocation = /location=0|location=no/.test(featuresStr); | |
| 91 | - | ||
| 92 | - | // OAuth/login popups typically have size constraints and disabled UI elements | |
| 93 | - | const popupIndicators = [ | |
| 94 | - | hasSize, | |
| 95 | - | hasNoToolbar, | |
| 96 | - | hasNoMenubar, | |
| 97 | - | hasNoLocation, | |
| 98 | - | ].filter(Boolean).length; | |
| 99 | - | ||
| 100 | - | // If 2 or more popup indicators are present, treat as popup | |
| 101 | - | if (popupIndicators >= 2) { | |
| 102 | - | return "popup"; | |
| 103 | - | } | |
| 104 | - | ||
| 105 | - | return "tab"; | |
| 106 | - | } | |
| 107 | - | ||
| 108 | 67 | (() => { | |
| 109 | 68 | try { | |
| 110 | 69 | // Prevent multiple installations | |
| @@ -899,284 +858,6 @@ function detectWindowType(features?: string | null): string { | |||
| 899 | 858 | // Setup OAuth callback detection | |
| 900 | 859 | setupOAuthCallbackDetection(); | |
| 901 | 860 | ||
| 902 | - | // Enhanced window.open deduplication system | |
| 903 | - | interface PendingRequest { | |
| 904 | - | id: string; | |
| 905 | - | url: string; | |
| 906 | - | timestamp: number; | |
| 907 | - | webviewId: string; | |
| 908 | - | resolved: boolean; | |
| 909 | - | } | |
| 910 | - | ||
| 911 | - | class WindowOpenDebouncer { | |
| 912 | - | private pendingRequests = new Map<string, PendingRequest>(); | |
| 913 | - | private readonly DEBOUNCE_WINDOW = 150; // Increased from 100ms | |
| 914 | - | private readonly CLEANUP_INTERVAL = 1000; // Clean old entries every 1s | |
| 915 | - | ||
| 916 | - | generateRequestId(): string { | |
| 917 | - | return `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | |
| 918 | - | } | |
| 919 | - | ||
| 920 | - | isDuplicate(url: string, webviewId: string): boolean { | |
| 921 | - | const key = this.createKey(url, webviewId); | |
| 922 | - | const existing = this.pendingRequests.get(key); | |
| 923 | - | ||
| 924 | - | if (!existing) return false; | |
| 925 | - | ||
| 926 | - | const timeDiff = Date.now() - existing.timestamp; | |
| 927 | - | return timeDiff < this.DEBOUNCE_WINDOW && !existing.resolved; | |
| 928 | - | } | |
| 929 | - | ||
| 930 | - | registerRequest(url: string, webviewId: string): string { | |
| 931 | - | const requestId = this.generateRequestId(); | |
| 932 | - | const key = this.createKey(url, webviewId); | |
| 933 | - | ||
| 934 | - | this.pendingRequests.set(key, { | |
| 935 | - | id: requestId, | |
| 936 | - | url, | |
| 937 | - | timestamp: Date.now(), | |
| 938 | - | webviewId, | |
| 939 | - | resolved: false, | |
| 940 | - | }); | |
| 941 | - | ||
| 942 | - | // Schedule cleanup | |
| 943 | - | setTimeout(() => this.markResolved(key), this.DEBOUNCE_WINDOW); | |
| 944 | - | return requestId; | |
| 945 | - | } | |
| 946 | - | ||
| 947 | - | private createKey(url: string, webviewId: string): string { | |
| 948 | - | return `${webviewId}:${url}`; | |
| 949 | - | } | |
| 950 | - | ||
| 951 | - | private markResolved(key: string): void { | |
| 952 | - | const request = this.pendingRequests.get(key); | |
| 953 | - | if (request) { | |
| 954 | - | request.resolved = true; | |
| 955 | - | // Clean up after additional delay | |
| 956 | - | setTimeout( | |
| 957 | - | () => this.pendingRequests.delete(key), | |
| 958 | - | this.CLEANUP_INTERVAL, | |
| 959 | - | ); | |
| 960 | - | } | |
| 961 | - | } | |
| 962 | - | } | |
| 963 | - | ||
| 964 | - | // Global debouncer instance | |
| 965 | - | const windowOpenDebouncer = new WindowOpenDebouncer(); | |
| 966 | - | ||
| 967 | - | // User interaction tracking | |
| 968 | - | let lastUserInteraction = 0; | |
| 969 | - | const INTERACTION_TIMEOUT = 5000; // 5 seconds | |
| 970 | - | ||
| 971 | - | const trackUserInteraction = () => { | |
| 972 | - | lastUserInteraction = Date.now(); | |
| 973 | - | }; | |
| 974 | - | ||
| 975 | - | const isUserInteraction = (): boolean => { | |
| 976 | - | const now = Date.now(); | |
| 977 | - | const timeSinceInteraction = now - lastUserInteraction; | |
| 978 | - | return timeSinceInteraction <= INTERACTION_TIMEOUT; | |
| 979 | - | }; | |
| 980 | - | ||
| 981 | - | const createFakeWindow = (url: string, popupId?: string): WindowProxy => { | |
| 982 | - | // Generate unique popup ID if not provided | |
| 983 | - | const actualPopupId = | |
| 984 | - | popupId || | |
| 985 | - | `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | |
| 986 | - | ||
| 987 | - | // Set up communication channel via shared localStorage | |
| 988 | - | try { | |
| 989 | - | localStorage.setItem( | |
| 990 | - | `popup_${actualPopupId}_parent_info`, | |
| 991 | - | JSON.stringify({ | |
| 992 | - | parentUrl: window.location.href, | |
| 993 | - | parentWebViewId: window.__dumber_webview_id, | |
| 994 | - | timestamp: Date.now(), | |
| 995 | - | popupUrl: url, | |
| 996 | - | }), | |
| 997 | - | ); | |
| 998 | - | ||
| 999 | - | console.log( | |
| 1000 | - | `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`, | |
| 1001 | - | ); | |
| 1002 | - | } catch (err) { | |
| 1003 | - | console.warn( | |
| 1004 | - | `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, | |
| 1005 | - | err, | |
| 1006 | - | ); | |
| 1007 | - | } | |
| 1008 | - | ||
| 1009 | - | return { | |
| 1010 | - | closed: false, | |
| 1011 | - | location: { href: url }, | |
| 1012 | - | close() { | |
| 1013 | - | (this as { closed: boolean }).closed = true; | |
| 1014 | - | }, | |
| 1015 | - | focus: () => { | |
| 1016 | - | try { | |
| 1017 | - | localStorage.setItem( | |
| 1018 | - | `popup_${actualPopupId}_parent_action`, | |
| 1019 | - | JSON.stringify({ | |
| 1020 | - | action: "focus", | |
| 1021 | - | timestamp: Date.now(), | |
| 1022 | - | }), | |
| 1023 | - | ); | |
| 1024 | - | } catch (err) { | |
| 1025 | - | console.warn(`[window-open] Failed to store focus action:`, err); | |
| 1026 | - | } | |
| 1027 | - | }, | |
| 1028 | - | blur: () => { | |
| 1029 | - | try { | |
| 1030 | - | localStorage.setItem( | |
| 1031 | - | `popup_${actualPopupId}_parent_action`, | |
| 1032 | - | JSON.stringify({ | |
| 1033 | - | action: "blur", | |
| 1034 | - | timestamp: Date.now(), | |
| 1035 | - | }), | |
| 1036 | - | ); | |
| 1037 | - | } catch (err) { | |
| 1038 | - | console.warn(`[window-open] Failed to store blur action:`, err); | |
| 1039 | - | } | |
| 1040 | - | }, | |
| 1041 | - | postMessage: (data: unknown, targetOrigin?: string) => { | |
| 1042 | - | try { | |
| 1043 | - | localStorage.setItem( | |
| 1044 | - | `popup_${actualPopupId}_message_to_popup`, | |
| 1045 | - | JSON.stringify({ | |
| 1046 | - | data, | |
| 1047 | - | origin: targetOrigin || "*", | |
| 1048 | - | timestamp: Date.now(), | |
| 1049 | - | source: "parent", | |
| 1050 | - | }), | |
| 1051 | - | ); | |
| 1052 | - | console.log( | |
| 1053 | - | `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, | |
| 1054 | - | data, | |
| 1055 | - | ); | |
| 1056 | - | } catch (err) { | |
| 1057 | - | console.warn(`[window-open] Failed to store postMessage:`, err); | |
| 1058 | - | } | |
| 1059 | - | }, | |
| 1060 | - | } as unknown as WindowProxy; | |
| 1061 | - | }; | |
| 1062 | - | ||
| 1063 | - | // Track user interactions for popup validation | |
| 1064 | - | const interactionEvents = [ | |
| 1065 | - | "click", | |
| 1066 | - | "mousedown", | |
| 1067 | - | "keydown", | |
| 1068 | - | "touchstart", | |
| 1069 | - | ] as const; | |
| 1070 | - | interactionEvents.forEach((eventType) => { | |
| 1071 | - | document.addEventListener(eventType, trackUserInteraction, true); | |
| 1072 | - | }); | |
| 1073 | - | cleanupHandlers.push(() => { | |
| 1074 | - | interactionEvents.forEach((eventType) => { | |
| 1075 | - | document.removeEventListener(eventType, trackUserInteraction, true); | |
| 1076 | - | }); | |
| 1077 | - | }); | |
| 1078 | - | ||
| 1079 | - | console.log( | |
| 1080 | - | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | |
| 1081 | - | ); | |
| 1082 | - | ||
| 1083 | - | // Window.open interceptor - only install once | |
| 1084 | - | if (!window.__dumber_window_open_intercepted) { | |
| 1085 | - | try { | |
| 1086 | - | window.open = function ( | |
| 1087 | - | url?: string | URL | null, | |
| 1088 | - | target?: string | null, | |
| 1089 | - | features?: string | null, | |
| 1090 | - | ): WindowProxy | null { | |
| 1091 | - | const urlString = url | |
| 1092 | - | ? typeof url === "string" | |
| 1093 | - | ? url | |
| 1094 | - | : url.toString() | |
| 1095 | - | : ""; | |
| 1096 | - | const webviewId = window.__dumber_webview_id || "unknown"; | |
| 1097 | - | ||
| 1098 | - | console.log( | |
| 1099 | - | `[window-open] [WebView ${webviewId}] Bridge called with:`, | |
| 1100 | - | urlString, | |
| 1101 | - | target, | |
| 1102 | - | features, | |
| 1103 | - | ); | |
| 1104 | - | ||
| 1105 | - | // Enhanced duplicate check | |
| 1106 | - | if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) { | |
| 1107 | - | console.log( | |
| 1108 | - | `[window-open] [WebView ${webviewId}] BLOCKED: Duplicate request within debounce window`, | |
| 1109 | - | ); | |
| 1110 | - | return createFakeWindow(urlString); | |
| 1111 | - | } | |
| 1112 | - | ||
| 1113 | - | // Active WebView check | |
| 1114 | - | if (!window.__dumber_is_active) { | |
| 1115 | - | console.log( | |
| 1116 | - | `[window-open] [WebView ${webviewId}] BLOCKED: Inactive WebView`, | |
| 1117 | - | ); | |
| 1118 | - | return createFakeWindow(urlString); | |
| 1119 | - | } | |
| 1120 | - | ||
| 1121 | - | // User interaction check | |
| 1122 | - | if (!isUserInteraction()) { | |
| 1123 | - | console.log( | |
| 1124 | - | `[window-open] [WebView ${webviewId}] BLOCKED: No recent user interaction`, | |
| 1125 | - | ); | |
| 1126 | - | return createFakeWindow(""); | |
| 1127 | - | } | |
| 1128 | - | ||
| 1129 | - | // Register request and get unique ID | |
| 1130 | - | const requestId = windowOpenDebouncer.registerRequest( | |
| 1131 | - | urlString, | |
| 1132 | - | webviewId, | |
| 1133 | - | ); | |
| 1134 | - | ||
| 1135 | - | const intent: WindowIntent = { | |
| 1136 | - | url: urlString, | |
| 1137 | - | target: target || "_blank", | |
| 1138 | - | features: features || "", | |
| 1139 | - | timestamp: Date.now(), | |
| 1140 | - | userTriggered: true, | |
| 1141 | - | requestId, // NEW: Add unique request ID | |
| 1142 | - | windowType: detectWindowType(features), // Detect popup vs tab | |
| 1143 | - | }; | |
| 1144 | - | ||
| 1145 | - | // Send to Go backend | |
| 1146 | - | try { | |
| 1147 | - | if (window.webkit?.messageHandlers?.dumber) { | |
| 1148 | - | window.webkit.messageHandlers.dumber.postMessage( | |
| 1149 | - | JSON.stringify({ | |
| 1150 | - | type: "handle-window-open", | |
| 1151 | - | payload: intent, | |
| 1152 | - | }), | |
| 1153 | - | ); | |
| 1154 | - | console.log( | |
| 1155 | - | `[window-open] [WebView ${webviewId}] Sent request ${requestId} to Go backend`, | |
| 1156 | - | ); | |
| 1157 | - | } | |
| 1158 | - | } catch (err) { | |
| 1159 | - | console.error( | |
| 1160 | - | `[window-open] [WebView ${webviewId}] Failed to send request ${requestId}:`, | |
| 1161 | - | err, | |
| 1162 | - | ); | |
| 1163 | - | } | |
| 1164 | - | ||
| 1165 | - | return createFakeWindow(urlString, requestId); | |
| 1166 | - | }; | |
| 1167 | - | ||
| 1168 | - | window.__dumber_window_open_intercepted = true; | |
| 1169 | - | console.log( | |
| 1170 | - | `[window-open] [WebView ${window.__dumber_webview_id}] ✅ Page-world bridge installed`, | |
| 1171 | - | ); | |
| 1172 | - | } catch (error) { | |
| 1173 | - | console.error( | |
| 1174 | - | `[window-open] [WebView ${window.__dumber_webview_id}] ❌ Failed to install page-world bridge:`, | |
| 1175 | - | error, | |
| 1176 | - | ); | |
| 1177 | - | } | |
| 1178 | - | } | |
| 1179 | - | ||
| 1180 | 861 | const teardown = () => { | |
| 1181 | 862 | if (teardownExecuted) { | |
| 1182 | 863 | return; | |
| @@ -1198,7 +879,6 @@ function detectWindowType(features?: string | null): string { | |||
| 1198 | 879 | omniboxQueue.length = 0; | |
| 1199 | 880 | omniboxReady = false; | |
| 1200 | 881 | ||
| 1201 | - | window.__dumber_window_open_intercepted = false; | |
| 1202 | 882 | window.__dumber_page_bridge_installed = false; | |
| 1203 | 883 | window.__dumber_teardown = undefined; | |
| 1204 | 884 | window.__dumber_omnibox_suggestions = undefined; | |
| @@ -14,7 +14,6 @@ declare global { | |||||
| 14 | interface Window { | 14 | interface Window { | ||
| 15 | __dumber: DumberAPI; | 15 | __dumber: DumberAPI; | ||
| 16 | __dumber_page_bridge_installed?: boolean; | 16 | __dumber_page_bridge_installed?: boolean; | ||
| 17 | - | __dumber_window_open_intercepted?: boolean; | |||
| 18 | __dumber_dom_zoom_seed?: number; | 17 | __dumber_dom_zoom_seed?: number; | ||
| 19 | __dumber_dom_zoom_level?: number; | 18 | __dumber_dom_zoom_level?: number; | ||
| 20 | __dumber_initial_theme?: string; | 19 | __dumber_initial_theme?: string; | ||
| @@ -46,16 +45,6 @@ declare global { | |||||
| 46 | } | 45 | } | ||
| 47 | } | 46 | } | ||
| 48 | 47 | ||||
| 49 | - | interface WindowIntent { | |||
| 50 | - | url: string; | |||
| 51 | - | target: string; | |||
| 52 | - | features: string; | |||
| 53 | - | timestamp: number; | |||
| 54 | - | userTriggered: boolean; | |||
| 55 | - | requestId: string; | |||
| 56 | - | windowType: string; // "tab", "popup", "unknown" | |||
| 57 | - | } | |||
| 58 | - | ||||
| 59 | interface Suggestion { | 48 | interface Suggestion { | ||
| 60 | // Define proper suggestion types based on your needs | 49 | // Define proper suggestion types based on your needs | ||
| 61 | [key: string]: unknown; | 50 | [key: string]: unknown; | ||
| @@ -75,36 +64,6 @@ interface DumberAPI { | |||||
| 75 | }; | 64 | }; | ||
| 76 | } | 65 | } | ||
| 77 | 66 | ||||
| 78 | - | // Function to detect if window.open call is for a popup based on features | |||
| 79 | - | function detectWindowType(features?: string | null): string { | |||
| 80 | - | if (!features) { | |||
| 81 | - | return "tab"; // Default to tab if no features specified | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | - | const featuresStr = features.toLowerCase(); | |||
| 85 | - | ||||
| 86 | - | // Check for typical popup characteristics | |||
| 87 | - | const hasSize = /width=\d+|height=\d+/.test(featuresStr); | |||
| 88 | - | const hasNoToolbar = /toolbar=0|toolbar=no/.test(featuresStr); | |||
| 89 | - | const hasNoMenubar = /menubar=0|menubar=no/.test(featuresStr); | |||
| 90 | - | const hasNoLocation = /location=0|location=no/.test(featuresStr); | |||
| 91 | - | ||||
| 92 | - | // OAuth/login popups typically have size constraints and disabled UI elements | |||
| 93 | - | const popupIndicators = [ | |||
| 94 | - | hasSize, | |||
| 95 | - | hasNoToolbar, | |||
| 96 | - | hasNoMenubar, | |||
| 97 | - | hasNoLocation, | |||
| 98 | - | ].filter(Boolean).length; | |||
| 99 | - | ||||
| 100 | - | // If 2 or more popup indicators are present, treat as popup | |||
| 101 | - | if (popupIndicators >= 2) { | |||
| 102 | - | return "popup"; | |||
| 103 | - | } | |||
| 104 | - | ||||
| 105 | - | return "tab"; | |||
| 106 | - | } | |||
| 107 | - | ||||
| 108 | (() => { | 67 | (() => { | ||
| 109 | try { | 68 | try { | ||
| 110 | // Prevent multiple installations | 69 | // Prevent multiple installations | ||
| @@ -899,284 +858,6 @@ function detectWindowType(features?: string | null): string { | |||||
| 899 | // Setup OAuth callback detection | 858 | // Setup OAuth callback detection | ||
| 900 | setupOAuthCallbackDetection(); | 859 | setupOAuthCallbackDetection(); | ||
| 901 | 860 | ||||
| 902 | - | // Enhanced window.open deduplication system | |||
| 903 | - | interface PendingRequest { | |||
| 904 | - | id: string; | |||
| 905 | - | url: string; | |||
| 906 | - | timestamp: number; | |||
| 907 | - | webviewId: string; | |||
| 908 | - | resolved: boolean; | |||
| 909 | - | } | |||
| 910 | - | ||||
| 911 | - | class WindowOpenDebouncer { | |||
| 912 | - | private pendingRequests = new Map<string, PendingRequest>(); | |||
| 913 | - | private readonly DEBOUNCE_WINDOW = 150; // Increased from 100ms | |||
| 914 | - | private readonly CLEANUP_INTERVAL = 1000; // Clean old entries every 1s | |||
| 915 | - | ||||
| 916 | - | generateRequestId(): string { | |||
| 917 | - | return `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | |||
| 918 | - | } | |||
| 919 | - | ||||
| 920 | - | isDuplicate(url: string, webviewId: string): boolean { | |||
| 921 | - | const key = this.createKey(url, webviewId); | |||
| 922 | - | const existing = this.pendingRequests.get(key); | |||
| 923 | - | ||||
| 924 | - | if (!existing) return false; | |||
| 925 | - | ||||
| 926 | - | const timeDiff = Date.now() - existing.timestamp; | |||
| 927 | - | return timeDiff < this.DEBOUNCE_WINDOW && !existing.resolved; | |||
| 928 | - | } | |||
| 929 | - | ||||
| 930 | - | registerRequest(url: string, webviewId: string): string { | |||
| 931 | - | const requestId = this.generateRequestId(); | |||
| 932 | - | const key = this.createKey(url, webviewId); | |||
| 933 | - | ||||
| 934 | - | this.pendingRequests.set(key, { | |||
| 935 | - | id: requestId, | |||
| 936 | - | url, | |||
| 937 | - | timestamp: Date.now(), | |||
| 938 | - | webviewId, | |||
| 939 | - | resolved: false, | |||
| 940 | - | }); | |||
| 941 | - | ||||
| 942 | - | // Schedule cleanup | |||
| 943 | - | setTimeout(() => this.markResolved(key), this.DEBOUNCE_WINDOW); | |||
| 944 | - | return requestId; | |||
| 945 | - | } | |||
| 946 | - | ||||
| 947 | - | private createKey(url: string, webviewId: string): string { | |||
| 948 | - | return `${webviewId}:${url}`; | |||
| 949 | - | } | |||
| 950 | - | ||||
| 951 | - | private markResolved(key: string): void { | |||
| 952 | - | const request = this.pendingRequests.get(key); | |||
| 953 | - | if (request) { | |||
| 954 | - | request.resolved = true; | |||
| 955 | - | // Clean up after additional delay | |||
| 956 | - | setTimeout( | |||
| 957 | - | () => this.pendingRequests.delete(key), | |||
| 958 | - | this.CLEANUP_INTERVAL, | |||
| 959 | - | ); | |||
| 960 | - | } | |||
| 961 | - | } | |||
| 962 | - | } | |||
| 963 | - | ||||
| 964 | - | // Global debouncer instance | |||
| 965 | - | const windowOpenDebouncer = new WindowOpenDebouncer(); | |||
| 966 | - | ||||
| 967 | - | // User interaction tracking | |||
| 968 | - | let lastUserInteraction = 0; | |||
| 969 | - | const INTERACTION_TIMEOUT = 5000; // 5 seconds | |||
| 970 | - | ||||
| 971 | - | const trackUserInteraction = () => { | |||
| 972 | - | lastUserInteraction = Date.now(); | |||
| 973 | - | }; | |||
| 974 | - | ||||
| 975 | - | const isUserInteraction = (): boolean => { | |||
| 976 | - | const now = Date.now(); | |||
| 977 | - | const timeSinceInteraction = now - lastUserInteraction; | |||
| 978 | - | return timeSinceInteraction <= INTERACTION_TIMEOUT; | |||
| 979 | - | }; | |||
| 980 | - | ||||
| 981 | - | const createFakeWindow = (url: string, popupId?: string): WindowProxy => { | |||
| 982 | - | // Generate unique popup ID if not provided | |||
| 983 | - | const actualPopupId = | |||
| 984 | - | popupId || | |||
| 985 | - | `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | |||
| 986 | - | ||||
| 987 | - | // Set up communication channel via shared localStorage | |||
| 988 | - | try { | |||
| 989 | - | localStorage.setItem( | |||
| 990 | - | `popup_${actualPopupId}_parent_info`, | |||
| 991 | - | JSON.stringify({ | |||
| 992 | - | parentUrl: window.location.href, | |||
| 993 | - | parentWebViewId: window.__dumber_webview_id, | |||
| 994 | - | timestamp: Date.now(), | |||
| 995 | - | popupUrl: url, | |||
| 996 | - | }), | |||
| 997 | - | ); | |||
| 998 | - | ||||
| 999 | - | console.log( | |||
| 1000 | - | `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`, | |||
| 1001 | - | ); | |||
| 1002 | - | } catch (err) { | |||
| 1003 | - | console.warn( | |||
| 1004 | - | `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, | |||
| 1005 | - | err, | |||
| 1006 | - | ); | |||
| 1007 | - | } | |||
| 1008 | - | ||||
| 1009 | - | return { | |||
| 1010 | - | closed: false, | |||
| 1011 | - | location: { href: url }, | |||
| 1012 | - | close() { | |||
| 1013 | - | (this as { closed: boolean }).closed = true; | |||
| 1014 | - | }, | |||
| 1015 | - | focus: () => { | |||
| 1016 | - | try { | |||
| 1017 | - | localStorage.setItem( | |||
| 1018 | - | `popup_${actualPopupId}_parent_action`, | |||
| 1019 | - | JSON.stringify({ | |||
| 1020 | - | action: "focus", | |||
| 1021 | - | timestamp: Date.now(), | |||
| 1022 | - | }), | |||
| 1023 | - | ); | |||
| 1024 | - | } catch (err) { | |||
| 1025 | - | console.warn(`[window-open] Failed to store focus action:`, err); | |||
| 1026 | - | } | |||
| 1027 | - | }, | |||
| 1028 | - | blur: () => { | |||
| 1029 | - | try { | |||
| 1030 | - | localStorage.setItem( | |||
| 1031 | - | `popup_${actualPopupId}_parent_action`, | |||
| 1032 | - | JSON.stringify({ | |||
| 1033 | - | action: "blur", | |||
| 1034 | - | timestamp: Date.now(), | |||
| 1035 | - | }), | |||
| 1036 | - | ); | |||
| 1037 | - | } catch (err) { | |||
| 1038 | - | console.warn(`[window-open] Failed to store blur action:`, err); | |||
| 1039 | - | } | |||
| 1040 | - | }, | |||
| 1041 | - | postMessage: (data: unknown, targetOrigin?: string) => { | |||
| 1042 | - | try { | |||
| 1043 | - | localStorage.setItem( | |||
| 1044 | - | `popup_${actualPopupId}_message_to_popup`, | |||
| 1045 | - | JSON.stringify({ | |||
| 1046 | - | data, | |||
| 1047 | - | origin: targetOrigin || "*", | |||
| 1048 | - | timestamp: Date.now(), | |||
| 1049 | - | source: "parent", | |||
| 1050 | - | }), | |||
| 1051 | - | ); | |||
| 1052 | - | console.log( | |||
| 1053 | - | `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, | |||
| 1054 | - | data, | |||
| 1055 | - | ); | |||
| 1056 | - | } catch (err) { | |||
| 1057 | - | console.warn(`[window-open] Failed to store postMessage:`, err); | |||
| 1058 | - | } | |||
| 1059 | - | }, | |||
| 1060 | - | } as unknown as WindowProxy; | |||
| 1061 | - | }; | |||
| 1062 | - | ||||
| 1063 | - | // Track user interactions for popup validation | |||
| 1064 | - | const interactionEvents = [ | |||
| 1065 | - | "click", | |||
| 1066 | - | "mousedown", | |||
| 1067 | - | "keydown", | |||
| 1068 | - | "touchstart", | |||
| 1069 | - | ] as const; | |||
| 1070 | - | interactionEvents.forEach((eventType) => { | |||
| 1071 | - | document.addEventListener(eventType, trackUserInteraction, true); | |||
| 1072 | - | }); | |||
| 1073 | - | cleanupHandlers.push(() => { | |||
| 1074 | - | interactionEvents.forEach((eventType) => { | |||
| 1075 | - | document.removeEventListener(eventType, trackUserInteraction, true); | |||
| 1076 | - | }); | |||
| 1077 | - | }); | |||
| 1078 | - | ||||
| 1079 | - | console.log( | |||
| 1080 | - | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | |||
| 1081 | - | ); | |||
| 1082 | - | ||||
| 1083 | - | // Window.open interceptor - only install once | |||
| 1084 | - | if (!window.__dumber_window_open_intercepted) { | |||
| 1085 | - | try { | |||
| 1086 | - | window.open = function ( | |||
| 1087 | - | url?: string | URL | null, | |||
| 1088 | - | target?: string | null, | |||
| 1089 | - | features?: string | null, | |||
| 1090 | - | ): WindowProxy | null { | |||
| 1091 | - | const urlString = url | |||
| 1092 | - | ? typeof url === "string" | |||
| 1093 | - | ? url | |||
| 1094 | - | : url.toString() | |||
| 1095 | - | : ""; | |||
| 1096 | - | const webviewId = window.__dumber_webview_id || "unknown"; | |||
| 1097 | - | ||||
| 1098 | - | console.log( | |||
| 1099 | - | `[window-open] [WebView ${webviewId}] Bridge called with:`, | |||
| 1100 | - | urlString, | |||
| 1101 | - | target, | |||
| 1102 | - | features, | |||
| 1103 | - | ); | |||
| 1104 | - | ||||
| 1105 | - | // Enhanced duplicate check | |||
| 1106 | - | if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) { | |||
| 1107 | - | console.log( | |||
| 1108 | - | `[window-open] [WebView ${webviewId}] BLOCKED: Duplicate request within debounce window`, | |||
| 1109 | - | ); | |||
| 1110 | - | return createFakeWindow(urlString); | |||
| 1111 | - | } | |||
| 1112 | - | ||||
| 1113 | - | // Active WebView check | |||
| 1114 | - | if (!window.__dumber_is_active) { | |||
| 1115 | - | console.log( | |||
| 1116 | - | `[window-open] [WebView ${webviewId}] BLOCKED: Inactive WebView`, | |||
| 1117 | - | ); | |||
| 1118 | - | return createFakeWindow(urlString); | |||
| 1119 | - | } | |||
| 1120 | - | ||||
| 1121 | - | // User interaction check | |||
| 1122 | - | if (!isUserInteraction()) { | |||
| 1123 | - | console.log( | |||
| 1124 | - | `[window-open] [WebView ${webviewId}] BLOCKED: No recent user interaction`, | |||
| 1125 | - | ); | |||
| 1126 | - | return createFakeWindow(""); | |||
| 1127 | - | } | |||
| 1128 | - | ||||
| 1129 | - | // Register request and get unique ID | |||
| 1130 | - | const requestId = windowOpenDebouncer.registerRequest( | |||
| 1131 | - | urlString, | |||
| 1132 | - | webviewId, | |||
| 1133 | - | ); | |||
| 1134 | - | ||||
| 1135 | - | const intent: WindowIntent = { | |||
| 1136 | - | url: urlString, | |||
| 1137 | - | target: target || "_blank", | |||
| 1138 | - | features: features || "", | |||
| 1139 | - | timestamp: Date.now(), | |||
| 1140 | - | userTriggered: true, | |||
| 1141 | - | requestId, // NEW: Add unique request ID | |||
| 1142 | - | windowType: detectWindowType(features), // Detect popup vs tab | |||
| 1143 | - | }; | |||
| 1144 | - | ||||
| 1145 | - | // Send to Go backend | |||
| 1146 | - | try { | |||
| 1147 | - | if (window.webkit?.messageHandlers?.dumber) { | |||
| 1148 | - | window.webkit.messageHandlers.dumber.postMessage( | |||
| 1149 | - | JSON.stringify({ | |||
| 1150 | - | type: "handle-window-open", | |||
| 1151 | - | payload: intent, | |||
| 1152 | - | }), | |||
| 1153 | - | ); | |||
| 1154 | - | console.log( | |||
| 1155 | - | `[window-open] [WebView ${webviewId}] Sent request ${requestId} to Go backend`, | |||
| 1156 | - | ); | |||
| 1157 | - | } | |||
| 1158 | - | } catch (err) { | |||
| 1159 | - | console.error( | |||
| 1160 | - | `[window-open] [WebView ${webviewId}] Failed to send request ${requestId}:`, | |||
| 1161 | - | err, | |||
| 1162 | - | ); | |||
| 1163 | - | } | |||
| 1164 | - | ||||
| 1165 | - | return createFakeWindow(urlString, requestId); | |||
| 1166 | - | }; | |||
| 1167 | - | ||||
| 1168 | - | window.__dumber_window_open_intercepted = true; | |||
| 1169 | - | console.log( | |||
| 1170 | - | `[window-open] [WebView ${window.__dumber_webview_id}] ✅ Page-world bridge installed`, | |||
| 1171 | - | ); | |||
| 1172 | - | } catch (error) { | |||
| 1173 | - | console.error( | |||
| 1174 | - | `[window-open] [WebView ${window.__dumber_webview_id}] ❌ Failed to install page-world bridge:`, | |||
| 1175 | - | error, | |||
| 1176 | - | ); | |||
| 1177 | - | } | |||
| 1178 | - | } | |||
| 1179 | - | ||||
| 1180 | const teardown = () => { | 861 | const teardown = () => { | ||
| 1181 | if (teardownExecuted) { | 862 | if (teardownExecuted) { | ||
| 1182 | return; | 863 | return; | ||
| @@ -1198,7 +879,6 @@ function detectWindowType(features?: string | null): string { | |||||
| 1198 | omniboxQueue.length = 0; | 879 | omniboxQueue.length = 0; | ||
| 1199 | omniboxReady = false; | 880 | omniboxReady = false; | ||
| 1200 | 881 | ||||
| 1201 | - | window.__dumber_window_open_intercepted = false; | |||
| 1202 | window.__dumber_page_bridge_installed = false; | 882 | window.__dumber_page_bridge_installed = false; | ||
| 1203 | window.__dumber_teardown = undefined; | 883 | window.__dumber_teardown = undefined; | ||
| 1204 | window.__dumber_omnibox_suggestions = undefined; | 884 | window.__dumber_omnibox_suggestions = undefined; | ||