|
746 | 746 | const platformSupport = {
|
747 | 747 | apple: ['webCompat', ...baseFeatures],
|
748 | 748 | 'apple-isolated': ['duckPlayer', 'brokerProtection', 'performanceMetrics', 'clickToLoad', 'messageBridge'],
|
749 |
| - android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer'], |
| 749 | + android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge'], |
750 | 750 | 'android-autofill-password-import': ['autofillPasswordImport'],
|
751 | 751 | windows: ['cookie', ...baseFeatures, 'windowsPermissionUsage', 'duckPlayer', 'brokerProtection', 'breakageReporting'],
|
752 | 752 | firefox: ['cookie', ...baseFeatures, 'clickToLoad'],
|
@@ -10254,6 +10254,217 @@
|
10254 | 10254 | }
|
10255 | 10255 | }
|
10256 | 10256 |
|
| 10257 | + /** |
| 10258 | + * @typedef {Pick<import("../captured-globals.js"), |
| 10259 | + * "dispatchEvent" | "addEventListener" | "CustomEvent"> |
| 10260 | + * } Captured |
| 10261 | + */ |
| 10262 | + |
| 10263 | + /** |
| 10264 | + * This part has access to messaging handlers |
| 10265 | + */ |
| 10266 | + class MessageBridge extends ContentFeature { |
| 10267 | + /** @type {Captured} */ |
| 10268 | + captured = capturedGlobals; |
| 10269 | + /** |
| 10270 | + * A mapping of feature names to instances of `Messaging`. |
| 10271 | + * This allows the bridge to handle more than 1 feature at a time. |
| 10272 | + * @type {Map<string, Messaging>} |
| 10273 | + */ |
| 10274 | + proxies = new Map$1(); |
| 10275 | + |
| 10276 | + /** |
| 10277 | + * If any subscriptions are created, we store the cleanup functions |
| 10278 | + * for later use. |
| 10279 | + * @type {Map<string, () => void>} |
| 10280 | + */ |
| 10281 | + subscriptions = new Map$1(); |
| 10282 | + |
| 10283 | + /** |
| 10284 | + * This side of the bridge can only be instantiated once, |
| 10285 | + * so we use this flag to ensure we can handle multiple invocations |
| 10286 | + */ |
| 10287 | + installed = false; |
| 10288 | + |
| 10289 | + init(args) { |
| 10290 | + /** |
| 10291 | + * This feature never operates in a frame or insecure context |
| 10292 | + */ |
| 10293 | + if (isBeingFramed() || !isSecureContext) return; |
| 10294 | + /** |
| 10295 | + * This feature never operates without messageSecret |
| 10296 | + */ |
| 10297 | + if (!args.messageSecret) return; |
| 10298 | + |
| 10299 | + const { captured } = this; |
| 10300 | + |
| 10301 | + /** |
| 10302 | + * @param {string} eventName |
| 10303 | + * @return {`${string}-${string}`} |
| 10304 | + */ |
| 10305 | + function appendToken(eventName) { |
| 10306 | + return `${eventName}-${args.messageSecret}`; |
| 10307 | + } |
| 10308 | + |
| 10309 | + /** |
| 10310 | + * @param {{name: string; id: string} & Record<string, any>} incoming |
| 10311 | + */ |
| 10312 | + const reply = (incoming) => { |
| 10313 | + if (!args.messageSecret) return this.log('ignoring because args.messageSecret was absent'); |
| 10314 | + const eventName = appendToken(incoming.name + '-' + incoming.id); |
| 10315 | + const event = new captured.CustomEvent(eventName, { detail: incoming }); |
| 10316 | + captured.dispatchEvent(event); |
| 10317 | + }; |
| 10318 | + |
| 10319 | + /** |
| 10320 | + * @template T |
| 10321 | + * @param {{ create: (params: any) => T | null, NAME: string }} ClassType - A class with a `create` static method. |
| 10322 | + * @param {(instance: T) => void} callback - A callback that receives an instance of the class. |
| 10323 | + */ |
| 10324 | + const accept = (ClassType, callback) => { |
| 10325 | + captured.addEventListener(appendToken(ClassType.NAME), (/** @type {CustomEvent<unknown>} */ e) => { |
| 10326 | + this.log(`${ClassType.NAME}`, JSON.stringify(e.detail)); |
| 10327 | + const instance = ClassType.create(e.detail); |
| 10328 | + if (instance) { |
| 10329 | + callback(instance); |
| 10330 | + } else { |
| 10331 | + this.log('Failed to create an instance'); |
| 10332 | + } |
| 10333 | + }); |
| 10334 | + }; |
| 10335 | + |
| 10336 | + /** |
| 10337 | + * These are all the messages we accept from the page-world. |
| 10338 | + */ |
| 10339 | + this.log(`bridge is installing...`); |
| 10340 | + accept(InstallProxy, (install) => { |
| 10341 | + this.installProxyFor(install, args.messagingConfig, reply); |
| 10342 | + }); |
| 10343 | + accept(ProxyNotification, (notification) => this.proxyNotification(notification)); |
| 10344 | + accept(ProxyRequest, (request) => this.proxyRequest(request, reply)); |
| 10345 | + accept(SubscriptionRequest, (subscription) => this.proxySubscription(subscription, reply)); |
| 10346 | + accept(SubscriptionUnsubscribe, (unsubscribe) => this.removeSubscription(unsubscribe.id)); |
| 10347 | + } |
| 10348 | + |
| 10349 | + /** |
| 10350 | + * Installing a feature proxy is the act of creating a fresh instance of 'Messaging', but |
| 10351 | + * using the same underlying transport |
| 10352 | + * |
| 10353 | + * @param {InstallProxy} install |
| 10354 | + * @param {import('@duckduckgo/messaging').MessagingConfig} config |
| 10355 | + * @param {(payload: {name: string; id: string} & Record<string, any>) => void} reply |
| 10356 | + */ |
| 10357 | + installProxyFor(install, config, reply) { |
| 10358 | + const { id, featureName } = install; |
| 10359 | + if (this.proxies.has(featureName)) return this.log('ignoring `installProxyFor` because it exists', featureName); |
| 10360 | + const allowed = this.getFeatureSettingEnabled(featureName); |
| 10361 | + if (!allowed) { |
| 10362 | + return this.log('not installing proxy, because', featureName, 'was not enabled'); |
| 10363 | + } |
| 10364 | + |
| 10365 | + const ctx = { ...this.messaging.messagingContext, featureName }; |
| 10366 | + const messaging = new Messaging(ctx, config); |
| 10367 | + this.proxies.set(featureName, messaging); |
| 10368 | + |
| 10369 | + this.log('did install proxy for ', featureName); |
| 10370 | + reply(new DidInstall({ id })); |
| 10371 | + } |
| 10372 | + |
| 10373 | + /** |
| 10374 | + * @param {ProxyRequest} request |
| 10375 | + * @param {(payload: {name: string; id: string} & Record<string, any>) => void} reply |
| 10376 | + */ |
| 10377 | + async proxyRequest(request, reply) { |
| 10378 | + const { id, featureName, method, params } = request; |
| 10379 | + |
| 10380 | + const proxy = this.proxies.get(featureName); |
| 10381 | + if (!proxy) return this.log('proxy was not installed for ', featureName); |
| 10382 | + |
| 10383 | + this.log('will proxy', request); |
| 10384 | + |
| 10385 | + try { |
| 10386 | + const result = await proxy.request(method, params); |
| 10387 | + const responseEvent = new ProxyResponse({ |
| 10388 | + method, |
| 10389 | + featureName, |
| 10390 | + result, |
| 10391 | + id, |
| 10392 | + }); |
| 10393 | + reply(responseEvent); |
| 10394 | + } catch (e) { |
| 10395 | + const errorResponseEvent = new ProxyResponse({ |
| 10396 | + method, |
| 10397 | + featureName, |
| 10398 | + error: { message: e.message }, |
| 10399 | + id, |
| 10400 | + }); |
| 10401 | + reply(errorResponseEvent); |
| 10402 | + } |
| 10403 | + } |
| 10404 | + |
| 10405 | + /** |
| 10406 | + * @param {SubscriptionRequest} subscription |
| 10407 | + * @param {(payload: {name: string; id: string} & Record<string, any>) => void} reply |
| 10408 | + */ |
| 10409 | + proxySubscription(subscription, reply) { |
| 10410 | + const { id, featureName, subscriptionName } = subscription; |
| 10411 | + const proxy = this.proxies.get(subscription.featureName); |
| 10412 | + if (!proxy) return this.log('proxy was not installed for', featureName); |
| 10413 | + |
| 10414 | + this.log('will setup subscription', subscription); |
| 10415 | + |
| 10416 | + // cleanup existing subscriptions first |
| 10417 | + const prev = this.subscriptions.get(id); |
| 10418 | + if (prev) { |
| 10419 | + this.removeSubscription(id); |
| 10420 | + } |
| 10421 | + |
| 10422 | + const unsubscribe = proxy.subscribe(subscriptionName, (/** @type {Record<string, any>} */ data) => { |
| 10423 | + const responseEvent = new SubscriptionResponse({ |
| 10424 | + subscriptionName, |
| 10425 | + featureName, |
| 10426 | + params: data, |
| 10427 | + id, |
| 10428 | + }); |
| 10429 | + reply(responseEvent); |
| 10430 | + }); |
| 10431 | + |
| 10432 | + this.subscriptions.set(id, unsubscribe); |
| 10433 | + } |
| 10434 | + |
| 10435 | + /** |
| 10436 | + * @param {string} id |
| 10437 | + */ |
| 10438 | + removeSubscription(id) { |
| 10439 | + const unsubscribe = this.subscriptions.get(id); |
| 10440 | + this.log(`will remove subscription`, id); |
| 10441 | + unsubscribe?.(); |
| 10442 | + this.subscriptions.delete(id); |
| 10443 | + } |
| 10444 | + |
| 10445 | + /** |
| 10446 | + * @param {ProxyNotification} notification |
| 10447 | + */ |
| 10448 | + proxyNotification(notification) { |
| 10449 | + const proxy = this.proxies.get(notification.featureName); |
| 10450 | + if (!proxy) return this.log('proxy was not installed for', notification.featureName); |
| 10451 | + |
| 10452 | + this.log('will proxy notification', notification); |
| 10453 | + proxy.notify(notification.method, notification.params); |
| 10454 | + } |
| 10455 | + |
| 10456 | + /** |
| 10457 | + * @param {Parameters<console['log']>} args |
| 10458 | + */ |
| 10459 | + log(...args) { |
| 10460 | + if (this.isDebug) { |
| 10461 | + console.log('[isolated]', ...args); |
| 10462 | + } |
| 10463 | + } |
| 10464 | + |
| 10465 | + load(args) {} |
| 10466 | + } |
| 10467 | + |
10257 | 10468 | var platformFeatures = {
|
10258 | 10469 | ddg_feature_fingerprintingAudio: FingerprintingAudio,
|
10259 | 10470 | ddg_feature_fingerprintingBattery: FingerprintingBattery,
|
|
10270 | 10481 | ddg_feature_apiManipulation: ApiManipulation,
|
10271 | 10482 | ddg_feature_webCompat: WebCompat,
|
10272 | 10483 | ddg_feature_breakageReporting: BreakageReporting,
|
10273 |
| - ddg_feature_duckPlayer: DuckPlayerFeature |
| 10484 | + ddg_feature_duckPlayer: DuckPlayerFeature, |
| 10485 | + ddg_feature_messageBridge: MessageBridge |
10274 | 10486 | };
|
10275 | 10487 |
|
10276 | 10488 | let initArgs = null;
|
|
0 commit comments