From a42ffcebe581ac4126055c3746d823cdc636dc2f Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:36:53 +0530 Subject: [PATCH 01/34] add MonetizationLinkManager: basics of link available on page load Co-authored-by: Radu-Cristian Popa --- src/content/container.ts | 6 +- src/content/services/contentScript.ts | 2 +- .../services/monetizationLinkManager.ts | 259 ++++++++++++++++++ src/content/types.ts | 2 +- 4 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 src/content/services/monetizationLinkManager.ts diff --git a/src/content/container.ts b/src/content/container.ts index 069971f0..4e68758e 100644 --- a/src/content/container.ts +++ b/src/content/container.ts @@ -2,7 +2,7 @@ import { asClass, asValue, createContainer, InjectionMode } from 'awilix' import browser, { type Browser } from 'webextension-polyfill' import { createLogger, Logger } from '@/shared/logger' import { ContentScript } from './services/contentScript' -import { MonetizationTagManager } from './services/monetizationTagManager' +import { MonetizationLinkManager } from './services/monetizationLinkManager' import { LOG_LEVEL } from '@/shared/defines' import { FrameManager } from './services/frameManager' import { @@ -16,7 +16,7 @@ export interface Cradle { document: Document window: Window message: MessageManager - monetizationTagManager: MonetizationTagManager + monetizationTagManager: MonetizationLinkManager frameManager: FrameManager contentScript: ContentScript } @@ -39,7 +39,7 @@ export const configureContainer = () => { .inject(() => ({ logger: logger.getLogger('content-script:frameManager') })), - monetizationTagManager: asClass(MonetizationTagManager) + monetizationTagManager: asClass(MonetizationLinkManager) .singleton() .inject(() => ({ logger: logger.getLogger('content-script:tagManager') diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index de615fb2..d78ff254 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -38,7 +38,7 @@ export class ContentScript { if (this.isFirstLevelFrame) { this.logger.info('Content script started') - if (this.isTopFrame) this.frameManager.start() + // if (this.isTopFrame) this.frameManager.start() this.monetizationTagManager.start() } diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts new file mode 100644 index 00000000..f3cd0ccd --- /dev/null +++ b/src/content/services/monetizationLinkManager.ts @@ -0,0 +1,259 @@ +import { EventEmitter } from 'events' +import type { MonetizationTagDetails } from '../types' +import type { WalletAddress } from '@interledger/open-payments/dist/types' +import { checkWalletAddressUrlFormat, mozClone } from '../utils' +import type { + // EmitToggleWMPayload, + MonetizationEventPayload, + // ResumeMonetizationPayload, + StartMonetizationPayload + // StopMonetizationPayload +} from '@/shared/messages' +import { ContentToContentAction } from '../messages' +import type { Cradle } from '@/content/container' + +// observeLinks() { +// find all links in the page +// observe changes to links - add/remove/attr-changes +// validate link tags +// } + +// observe page visibility + +// on change: start/stop/pause/resume monetization + +// handle events from background - monetization/load/error events + +export class MonetizationLinkManager extends EventEmitter { + private window: Cradle['window'] + private document: Cradle['document'] + private logger: Cradle['logger'] + private message: Cradle['message'] + + private isTopFrame: boolean + private isFirstLevelFrame: boolean + private documentObserver: MutationObserver + private monetizationTagAttrObserver: MutationObserver + private id: string + // only entries corresponding to valid wallet addresses are here + private monetizationLinks = new Map() + + constructor({ window, document, logger, message }: Cradle) { + super() + Object.assign(this, { + window, + document, + logger, + message + }) + + // this.documentObserver = new MutationObserver((records) => + // this.onWholeDocumentObserved(records) + // ) + // this.monetizationTagAttrObserver = new MutationObserver((records) => + // this.onMonetizationTagAttrsChange(records) + // ) + + // document.addEventListener('visibilitychange', async () => { + // if (document.visibilityState === 'visible') { + // await this.resumeAllMonetization() + // } else { + // this.stopAllMonetization() + // } + // }) + + this.isTopFrame = window === window.top + this.isFirstLevelFrame = window.parent === window.top + this.id = crypto.randomUUID() + + if (!this.isTopFrame && this.isFirstLevelFrame) { + // this.bindMessageHandler() + } + } + + start(): void { + if (isDocumentReady(this.document)) { + this.run() + return + } + + document.addEventListener( + 'readystatechange', + () => { + if (isDocumentReady(this.document)) { + this.run() + } else { + document.addEventListener( + 'visibilitychange', + () => { + if (isDocumentReady(this.document)) { + this.run() + } + }, + { once: true } + ) + } + }, + { once: true } + ) + } + + /** + * Check if iframe or not + */ + private async run() { + this.document.querySelectorAll('[onmonetization]').forEach((node) => { + this.fireOnMonetizationAttrChangedEvent(node) + }) + // this.documentObserver.observe(this.document, { + // subtree: true, + // childList: true, + // attributeFilter: ['onmonetization'] + // }) + + const monetizationLinks = getMonetizationLinkTags( + this.document, + this.isTopFrame + ) + + for (const elem of monetizationLinks) { + this.observeMonetizationLinkAttrs(elem) + } + + const validMonetizationLinks = ( + await Promise.all(monetizationLinks.map((elem) => this.checkLink(elem))) + ).filter(isNotNull) + + for (const { link, details } of validMonetizationLinks) { + this.monetizationLinks.set(link, details) + } + + this.sendStartMonetization(validMonetizationLinks.map((e) => e.details)) + } + + end() {} + + /** @throws never throws */ + private async checkLink(link: HTMLLinkElement) { + if (!(link instanceof HTMLLinkElement && link.rel === 'monetization')) { + return null + } + if (link.hasAttribute('disabled')) { + return null + } + + const walletAddress = await this.validateWalletAddress(link) + if (!walletAddress) { + return null + } + + return { + link, + details: { + requestId: crypto.randomUUID(), + walletAddress: walletAddress + } + } + } + + /** @throws never throws */ + private async validateWalletAddress( + tag: HTMLLinkElement + ): Promise { + const walletAddressUrl = tag.href.trim() + try { + checkWalletAddressUrlFormat(walletAddressUrl) + const response = await this.message.send('CHECK_WALLET_ADDRESS_URL', { + walletAddressUrl + }) + + if (response.success === false) { + throw new Error( + `Could not retrieve wallet address information for ${JSON.stringify(walletAddressUrl)}.` + ) + } + + this.dispatchLoadEvent(tag) + return response.payload + } catch (e) { + this.logger.error(e) + this.dispatchErrorEvent(tag) + return null + } + } + + private dispatchLoadEvent(tag: HTMLLinkElement) { + tag.dispatchEvent(new Event('load')) + } + + private dispatchErrorEvent(tag: HTMLLinkElement) { + tag.dispatchEvent(new Event('error')) + } + + dispatchMonetizationEvent({ requestId, details }: MonetizationEventPayload) { + for (const [tag, tagDetails] of this.monetizationLinks) { + if (tagDetails.requestId !== requestId) continue + + tag.dispatchEvent( + new CustomEvent('__wm_ext_monetization', { + detail: mozClone(details, this.document), + bubbles: true + }) + ) + break + } + } + + private observeMonetizationLinkAttrs(link: HTMLLinkElement) { + this.logger.debug(link) + } + + private fireOnMonetizationAttrChangedEvent(node: Element) { + this.logger.debug(node) + } + + private sendStartMonetization(payload: StartMonetizationPayload[]) { + if (!payload.length) return + + if (this.isTopFrame) { + void this.message.send('START_MONETIZATION', payload) + } else if (this.isFirstLevelFrame) { + this.window.parent.postMessage( + { + message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, + id: this.id, + payload: payload + }, + '*' + ) + } + } +} + +function isDocumentReady(document: Document) { + return ( + (document.readyState === 'interactive' || + document.readyState === 'complete') && + document.visibilityState === 'visible' + ) +} + +function getMonetizationLinkTags( + document: Document, + isTopFrame: boolean +): HTMLLinkElement[] { + if (isTopFrame) { + return Array.from( + document.querySelectorAll('link[rel="monetization"]') + ) + } else { + const monetizationTag = document.querySelector( + 'head link[rel="monetization"]' + ) + return monetizationTag ? [monetizationTag] : [] + } +} + +function isNotNull(value: T | null): value is T { + return value !== null +} diff --git a/src/content/types.ts b/src/content/types.ts index ec773fc7..e81ce6f6 100644 --- a/src/content/types.ts +++ b/src/content/types.ts @@ -4,6 +4,6 @@ export type MonetizationTag = HTMLLinkElement & { href?: string } export type MonetizationTagList = NodeListOf export type MonetizationTagDetails = { - walletAddress: WalletAddress | null + walletAddress: WalletAddress requestId: string } From 659c831735f7ba8bc1967e15b921ae11da879e42 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:49:07 +0530 Subject: [PATCH 02/34] stop/resume on visibilitychange --- .../services/monetizationLinkManager.ts | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index f3cd0ccd..d1fb1842 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -5,9 +5,9 @@ import { checkWalletAddressUrlFormat, mozClone } from '../utils' import type { // EmitToggleWMPayload, MonetizationEventPayload, - // ResumeMonetizationPayload, - StartMonetizationPayload - // StopMonetizationPayload + ResumeMonetizationPayload, + StartMonetizationPayload, + StopMonetizationPayload } from '@/shared/messages' import { ContentToContentAction } from '../messages' import type { Cradle } from '@/content/container' @@ -54,14 +54,6 @@ export class MonetizationLinkManager extends EventEmitter { // this.onMonetizationTagAttrsChange(records) // ) - // document.addEventListener('visibilitychange', async () => { - // if (document.visibilityState === 'visible') { - // await this.resumeAllMonetization() - // } else { - // this.stopAllMonetization() - // } - // }) - this.isTopFrame = window === window.top this.isFirstLevelFrame = window.parent === window.top this.id = crypto.randomUUID() @@ -102,6 +94,14 @@ export class MonetizationLinkManager extends EventEmitter { * Check if iframe or not */ private async run() { + this.document.addEventListener('visibilitychange', async () => { + if (this.document.visibilityState === 'visible') { + this.resumeMonetization() + } else { + this.stopMonetization() + } + }) + this.document.querySelectorAll('[onmonetization]').forEach((node) => { this.fireOnMonetizationAttrChangedEvent(node) }) @@ -228,6 +228,36 @@ export class MonetizationLinkManager extends EventEmitter { ) } } + + private stopMonetization(intent?: StopMonetizationPayload['intent']) { + const payload: StopMonetizationPayload[] = [ + ...this.monetizationLinks.values() + ].map(({ requestId }) => ({ requestId, intent })) + + if (!payload.length) return + void this.message.send('STOP_MONETIZATION', payload) + } + + private resumeMonetization() { + const payload: ResumeMonetizationPayload[] = [ + ...this.monetizationLinks.values() + ].map(({ requestId }) => ({ requestId })) + + if (this.isTopFrame) { + if (payload.length) { + void this.message.send('RESUME_MONETIZATION', payload) + } + } else if (this.isFirstLevelFrame) { + this.window.parent.postMessage( + { + message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, + id: this.id, + payload: payload + }, + '*' + ) + } + } } function isDocumentReady(document: Document) { From fcee306fb565a9443bea101065a2f32081caff31 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:51:16 +0530 Subject: [PATCH 03/34] nit: rename param --- src/content/services/monetizationLinkManager.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index d1fb1842..b5049030 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -260,11 +260,10 @@ export class MonetizationLinkManager extends EventEmitter { } } -function isDocumentReady(document: Document) { +function isDocumentReady(doc: Document) { return ( - (document.readyState === 'interactive' || - document.readyState === 'complete') && - document.visibilityState === 'visible' + (doc.readyState === 'interactive' || doc.readyState === 'complete') && + doc.visibilityState === 'visible' ) } From 810cb7be89511fa395297eba5b3ad632ae56dc32 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:34:57 +0530 Subject: [PATCH 04/34] watch for add/remove of link tags --- src/content/services/contentScript.ts | 2 +- .../services/monetizationLinkManager.ts | 80 +++++++++++++++++-- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index d78ff254..15cc1f56 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -56,7 +56,7 @@ export class ContentScript { return case 'EMIT_TOGGLE_WM': - this.monetizationTagManager.toggleWM(message.payload) + // this.monetizationTagManager.toggleWM(message.payload) return diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index b5049030..84b3f8ec 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -47,9 +47,9 @@ export class MonetizationLinkManager extends EventEmitter { message }) - // this.documentObserver = new MutationObserver((records) => - // this.onWholeDocumentObserved(records) - // ) + this.documentObserver = new MutationObserver((records) => + this.onWholeDocumentObserved(records) + ) // this.monetizationTagAttrObserver = new MutationObserver((records) => // this.onMonetizationTagAttrsChange(records) // ) @@ -105,11 +105,11 @@ export class MonetizationLinkManager extends EventEmitter { this.document.querySelectorAll('[onmonetization]').forEach((node) => { this.fireOnMonetizationAttrChangedEvent(node) }) - // this.documentObserver.observe(this.document, { - // subtree: true, - // childList: true, - // attributeFilter: ['onmonetization'] - // }) + this.documentObserver.observe(this.document, { + subtree: true, + childList: true, + attributeFilter: ['onmonetization'] + }) const monetizationLinks = getMonetizationLinkTags( this.document, @@ -258,6 +258,70 @@ export class MonetizationLinkManager extends EventEmitter { ) } } + + private async onWholeDocumentObserved(records: MutationRecord[]) { + const stopMonetizationPayload: StopMonetizationPayload[] = [] + + for (const record of records) { + if (record.type === 'childList') { + record.removedNodes.forEach(async (node) => { + if (!(node instanceof HTMLLinkElement)) return + const payloadEntry = this.onRemovedLink(node) + stopMonetizationPayload.push(payloadEntry) + }) + } + } + if (stopMonetizationPayload.length) { + await this.message.send('STOP_MONETIZATION', stopMonetizationPayload) + } + + if (this.isTopFrame) { + const addedNodes = records + .filter((e) => e.type === 'childList') + .flatMap((e) => [...e.addedNodes]) + const allAddedLinkTags = await Promise.all( + addedNodes.map((node) => this.onAddedNode(node)) + ) + const startMonetizationPayload = allAddedLinkTags + .filter(isNotNull) + .map(({ details }) => details) + + this.sendStartMonetization(startMonetizationPayload) + } + + // this.onOnMonetizationChangeObserved(records) + } + + private onRemovedLink(link: HTMLLinkElement): StopMonetizationPayload { + const details = this.monetizationLinks.get(link) + if (!details) { + throw new Error( + 'Could not find details for monetization node ' + + // node is removed, so the reference can not be displayed + link.outerHTML.slice(0, 200) + ) + } + + this.monetizationLinks.delete(link) + + return { requestId: details.requestId, intent: 'remove' } + } + + private async onAddedNode(node: Node) { + if (node instanceof HTMLElement) { + this.fireOnMonetizationAttrChangedEvent(node) + } + + if (node instanceof HTMLLinkElement) { + return await this.onAddedLink(node) + } + return null + } + + private async onAddedLink(link: HTMLLinkElement) { + // this.observeMonetizationTagAttrs(link) + return await this.checkLink(link) + } } function isDocumentReady(doc: Document) { From dd9f59bde2e3975e4fb8835bdd179284451dd011 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:09:45 +0530 Subject: [PATCH 05/34] handle onmonetization attribute change Co-authored-by: Radu-Cristian Popa --- .../services/monetizationLinkManager.ts | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 84b3f8ec..6a2046e4 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -102,9 +102,12 @@ export class MonetizationLinkManager extends EventEmitter { } }) - this.document.querySelectorAll('[onmonetization]').forEach((node) => { - this.fireOnMonetizationAttrChangedEvent(node) - }) + this.document + .querySelectorAll('[onmonetization]') + .forEach((node) => { + this.dispatchOnMonetizationAttrChangedEvent(node) + }) + this.documentObserver.observe(this.document, { subtree: true, childList: true, @@ -204,12 +207,22 @@ export class MonetizationLinkManager extends EventEmitter { } } - private observeMonetizationLinkAttrs(link: HTMLLinkElement) { - this.logger.debug(link) + private dispatchOnMonetizationAttrChangedEvent( + node: HTMLElement, + { changeDetected = false } = {} + ) { + const attribute = node.getAttribute('onmonetization') + if (!attribute && !changeDetected) return + + const customEvent = new CustomEvent('__wm_ext_onmonetization_attr_change', { + bubbles: true, + detail: mozClone({ attribute }, this.document) + }) + node.dispatchEvent(customEvent) } - private fireOnMonetizationAttrChangedEvent(node: Element) { - this.logger.debug(node) + private observeMonetizationLinkAttrs(link: HTMLLinkElement) { + this.logger.debug(link) } private sendStartMonetization(payload: StartMonetizationPayload[]) { @@ -289,7 +302,17 @@ export class MonetizationLinkManager extends EventEmitter { this.sendStartMonetization(startMonetizationPayload) } - // this.onOnMonetizationChangeObserved(records) + for (const record of records) { + if ( + record.type === 'attributes' && + record.target instanceof HTMLElement && + record.attributeName === 'onmonetization' + ) { + this.dispatchOnMonetizationAttrChangedEvent(record.target, { + changeDetected: true + }) + } + } } private onRemovedLink(link: HTMLLinkElement): StopMonetizationPayload { @@ -309,7 +332,7 @@ export class MonetizationLinkManager extends EventEmitter { private async onAddedNode(node: Node) { if (node instanceof HTMLElement) { - this.fireOnMonetizationAttrChangedEvent(node) + this.dispatchOnMonetizationAttrChangedEvent(node) } if (node instanceof HTMLLinkElement) { From ee0fad94074c45cd89ceaadd073b2ca837969798 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:52:35 +0530 Subject: [PATCH 06/34] handle element attribute changes; move things around Co-authored-by: Radu-Cristian Popa --- .../services/monetizationLinkManager.ts | 179 ++++++++++++++---- 1 file changed, 139 insertions(+), 40 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 6a2046e4..7a8bc16f 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -33,7 +33,7 @@ export class MonetizationLinkManager extends EventEmitter { private isTopFrame: boolean private isFirstLevelFrame: boolean private documentObserver: MutationObserver - private monetizationTagAttrObserver: MutationObserver + private monetizationLinkAttrObserver: MutationObserver private id: string // only entries corresponding to valid wallet addresses are here private monetizationLinks = new Map() @@ -50,9 +50,10 @@ export class MonetizationLinkManager extends EventEmitter { this.documentObserver = new MutationObserver((records) => this.onWholeDocumentObserved(records) ) - // this.monetizationTagAttrObserver = new MutationObserver((records) => - // this.onMonetizationTagAttrsChange(records) - // ) + + this.monetizationLinkAttrObserver = new MutationObserver((records) => + this.onLinkAttrChange(records) + ) this.isTopFrame = window === window.top this.isFirstLevelFrame = window.parent === window.top @@ -90,15 +91,17 @@ export class MonetizationLinkManager extends EventEmitter { ) } + end() {} + /** * Check if iframe or not */ private async run() { - this.document.addEventListener('visibilitychange', async () => { + this.document.addEventListener('visibilitychange', () => { if (this.document.visibilityState === 'visible') { - this.resumeMonetization() + void this.resumeMonetization() } else { - this.stopMonetization() + void this.stopMonetization() } }) @@ -119,23 +122,21 @@ export class MonetizationLinkManager extends EventEmitter { this.isTopFrame ) - for (const elem of monetizationLinks) { - this.observeMonetizationLinkAttrs(elem) + for (const link of monetizationLinks) { + this.observeLinkAttrs(link) } - const validMonetizationLinks = ( + const validLinks = ( await Promise.all(monetizationLinks.map((elem) => this.checkLink(elem))) ).filter(isNotNull) - for (const { link, details } of validMonetizationLinks) { + for (const { link, details } of validLinks) { this.monetizationLinks.set(link, details) } - this.sendStartMonetization(validMonetizationLinks.map((e) => e.details)) + void this.sendStartMonetization(validLinks.map((e) => e.details)) } - end() {} - /** @throws never throws */ private async checkLink(link: HTMLLinkElement) { if (!(link instanceof HTMLLinkElement && link.rel === 'monetization')) { @@ -145,7 +146,7 @@ export class MonetizationLinkManager extends EventEmitter { return null } - const walletAddress = await this.validateWalletAddress(link) + const walletAddress = await this.validateLink(link) if (!walletAddress) { return null } @@ -160,10 +161,10 @@ export class MonetizationLinkManager extends EventEmitter { } /** @throws never throws */ - private async validateWalletAddress( - tag: HTMLLinkElement + private async validateLink( + link: HTMLLinkElement ): Promise { - const walletAddressUrl = tag.href.trim() + const walletAddressUrl = link.href.trim() try { checkWalletAddressUrlFormat(walletAddressUrl) const response = await this.message.send('CHECK_WALLET_ADDRESS_URL', { @@ -176,15 +177,23 @@ export class MonetizationLinkManager extends EventEmitter { ) } - this.dispatchLoadEvent(tag) + this.dispatchLoadEvent(link) return response.payload } catch (e) { this.logger.error(e) - this.dispatchErrorEvent(tag) + this.dispatchErrorEvent(link) return null } } + private observeLinkAttrs(link: HTMLLinkElement) { + this.monetizationLinkAttrObserver.observe(link, { + childList: false, + attributeOldValue: true, + attributeFilter: ['href', 'disabled', 'rel', 'crossorigin', 'type'] + }) + } + private dispatchLoadEvent(tag: HTMLLinkElement) { tag.dispatchEvent(new Event('load')) } @@ -221,15 +230,27 @@ export class MonetizationLinkManager extends EventEmitter { node.dispatchEvent(customEvent) } - private observeMonetizationLinkAttrs(link: HTMLLinkElement) { - this.logger.debug(link) + private async stopMonetization() { + const payload: StopMonetizationPayload[] = [ + ...this.monetizationLinks.values() + ].map(({ requestId }) => ({ requestId })) + + await this.sendStopMonetization(payload) + } + + private async resumeMonetization() { + const payload: ResumeMonetizationPayload[] = [ + ...this.monetizationLinks.values() + ].map(({ requestId }) => ({ requestId })) + + await this.sendResumeMonetization(payload) } - private sendStartMonetization(payload: StartMonetizationPayload[]) { + private async sendStartMonetization(payload: StartMonetizationPayload[]) { if (!payload.length) return if (this.isTopFrame) { - void this.message.send('START_MONETIZATION', payload) + await this.message.send('START_MONETIZATION', payload) } else if (this.isFirstLevelFrame) { this.window.parent.postMessage( { @@ -242,23 +263,15 @@ export class MonetizationLinkManager extends EventEmitter { } } - private stopMonetization(intent?: StopMonetizationPayload['intent']) { - const payload: StopMonetizationPayload[] = [ - ...this.monetizationLinks.values() - ].map(({ requestId }) => ({ requestId, intent })) - + private async sendStopMonetization(payload: StopMonetizationPayload[]) { if (!payload.length) return - void this.message.send('STOP_MONETIZATION', payload) + await this.message.send('STOP_MONETIZATION', payload) } - private resumeMonetization() { - const payload: ResumeMonetizationPayload[] = [ - ...this.monetizationLinks.values() - ].map(({ requestId }) => ({ requestId })) - + private async sendResumeMonetization(payload: ResumeMonetizationPayload[]) { if (this.isTopFrame) { if (payload.length) { - void this.message.send('RESUME_MONETIZATION', payload) + await this.message.send('RESUME_MONETIZATION', payload) } } else if (this.isFirstLevelFrame) { this.window.parent.postMessage( @@ -284,9 +297,8 @@ export class MonetizationLinkManager extends EventEmitter { }) } } - if (stopMonetizationPayload.length) { - await this.message.send('STOP_MONETIZATION', stopMonetizationPayload) - } + + await this.sendStopMonetization(stopMonetizationPayload) if (this.isTopFrame) { const addedNodes = records @@ -299,7 +311,7 @@ export class MonetizationLinkManager extends EventEmitter { .filter(isNotNull) .map(({ details }) => details) - this.sendStartMonetization(startMonetizationPayload) + void this.sendStartMonetization(startMonetizationPayload) } for (const record of records) { @@ -315,6 +327,92 @@ export class MonetizationLinkManager extends EventEmitter { } } + private async onLinkAttrChange(records: MutationRecord[]) { + const handledTags = new Set() + const startMonetizationPayload: StartMonetizationPayload[] = [] + const stopMonetizationPayload: StopMonetizationPayload[] = [] + + // Check for a non specified link with the type now specified and + // just treat it as a newly seen, monetization tag + for (const record of records) { + const target = record.target as HTMLLinkElement + if (handledTags.has(target)) { + continue + } + + const hasTarget = this.monetizationLinks.has(target) + const linkRelSpecified = + target instanceof HTMLLinkElement && target.rel === 'monetization' + // this will also handle the case of a @disabled tag that + // is not tracked, becoming enabled + if (!hasTarget && linkRelSpecified) { + const payloadEntry = await this.checkLink(target) + if (payloadEntry) { + this.monetizationLinks.set(target, payloadEntry.details) + startMonetizationPayload.push(payloadEntry.details) + } + handledTags.add(target) + } else if (hasTarget && !linkRelSpecified) { + const payloadEntry = this.onRemovedLink(target) + stopMonetizationPayload.push(payloadEntry) + handledTags.add(target) + } else if (!hasTarget && !linkRelSpecified) { + // ignore these changes + handledTags.add(target) + } else if (hasTarget && linkRelSpecified) { + if ( + record.type === 'attributes' && + record.attributeName === 'disabled' && + target instanceof HTMLLinkElement && + target.getAttribute('disabled') !== record.oldValue + ) { + const wasDisabled = record.oldValue !== null + const isDisabled = target.hasAttribute('disabled') + if (wasDisabled != isDisabled) { + try { + const details = this.monetizationLinks.get(target) + if (!details) { + throw new Error('Could not find details for monetization node') + } + if (isDisabled) { + stopMonetizationPayload.push({ + requestId: details.requestId, + intent: 'disable' + }) + } else { + startMonetizationPayload.push(details) + } + } catch { + const payloadEntry = await this.checkLink(target) + if (payloadEntry) { + this.monetizationLinks.set(target, payloadEntry.details) + startMonetizationPayload.push(payloadEntry.details) + } + } + + handledTags.add(target) + } + } else if ( + record.type === 'attributes' && + record.attributeName === 'href' && + target instanceof HTMLLinkElement && + target.href !== record.oldValue + ) { + const payloadEntry = await this.checkLink(target) + if (payloadEntry) { + startMonetizationPayload.push(payloadEntry.details) + } else { + stopMonetizationPayload.push(this.onRemovedLink(target)) + } + handledTags.add(target) + } + } + } + + await this.sendStopMonetization(stopMonetizationPayload) + void this.sendStartMonetization(startMonetizationPayload) + } + private onRemovedLink(link: HTMLLinkElement): StopMonetizationPayload { const details = this.monetizationLinks.get(link) if (!details) { @@ -336,6 +434,7 @@ export class MonetizationLinkManager extends EventEmitter { } if (node instanceof HTMLLinkElement) { + this.observeLinkAttrs(node) return await this.onAddedLink(node) } return null From 0b5707fd7a9986d9a41a95a48565f6a668667cef Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:01:10 +0530 Subject: [PATCH 07/34] onAddedNode/onAddedLink; nits --- .../services/monetizationLinkManager.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 7a8bc16f..33c91ef2 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -66,7 +66,7 @@ export class MonetizationLinkManager extends EventEmitter { start(): void { if (isDocumentReady(this.document)) { - this.run() + void this.run() return } @@ -74,13 +74,13 @@ export class MonetizationLinkManager extends EventEmitter { 'readystatechange', () => { if (isDocumentReady(this.document)) { - this.run() + void this.run() } else { document.addEventListener( 'visibilitychange', () => { if (isDocumentReady(this.document)) { - this.run() + void this.run() } }, { once: true } @@ -134,7 +134,7 @@ export class MonetizationLinkManager extends EventEmitter { this.monetizationLinks.set(link, details) } - void this.sendStartMonetization(validLinks.map((e) => e.details)) + await this.sendStartMonetization(validLinks.map((e) => e.details)) } /** @throws never throws */ @@ -413,6 +413,26 @@ export class MonetizationLinkManager extends EventEmitter { void this.sendStartMonetization(startMonetizationPayload) } + private async onAddedNode(node: Node) { + if (node instanceof HTMLElement) { + this.dispatchOnMonetizationAttrChangedEvent(node) + } + + if (node instanceof HTMLLinkElement) { + return await this.onAddedLink(node) + } + return null + } + + private async onAddedLink(link: HTMLLinkElement) { + this.observeLinkAttrs(link) + const res = await this.checkLink(link) + if (res) { + this.monetizationLinks.set(link, res.details) + } + return res + } + private onRemovedLink(link: HTMLLinkElement): StopMonetizationPayload { const details = this.monetizationLinks.get(link) if (!details) { @@ -427,23 +447,6 @@ export class MonetizationLinkManager extends EventEmitter { return { requestId: details.requestId, intent: 'remove' } } - - private async onAddedNode(node: Node) { - if (node instanceof HTMLElement) { - this.dispatchOnMonetizationAttrChangedEvent(node) - } - - if (node instanceof HTMLLinkElement) { - this.observeLinkAttrs(node) - return await this.onAddedLink(node) - } - return null - } - - private async onAddedLink(link: HTMLLinkElement) { - // this.observeMonetizationTagAttrs(link) - return await this.checkLink(link) - } } function isDocumentReady(doc: Document) { From 1fec4d8c0a2c8a71e547b0cd58a352ffad950cf0 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:23:38 +0530 Subject: [PATCH 08/34] stop first on href change --- src/content/services/monetizationLinkManager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 33c91ef2..58103f87 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -398,11 +398,10 @@ export class MonetizationLinkManager extends EventEmitter { target instanceof HTMLLinkElement && target.href !== record.oldValue ) { + stopMonetizationPayload.push(this.onRemovedLink(target)) const payloadEntry = await this.checkLink(target) if (payloadEntry) { startMonetizationPayload.push(payloadEntry.details) - } else { - stopMonetizationPayload.push(this.onRemovedLink(target)) } handledTags.add(target) } From 2056b5f4f63cdfbf3eb668064931aeabc949fc8d Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:04:34 +0530 Subject: [PATCH 09/34] handle toggleWM within background only --- src/background/services/monetization.ts | 6 +++++- src/content/services/contentScript.ts | 6 ------ src/shared/messages.ts | 4 ---- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index 2de577cb..ce78575e 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -244,7 +244,11 @@ export class MonetizationService { async toggleWM() { const { enabled } = await this.storage.get(['enabled']); await this.storage.set({ enabled: !enabled }); - await this.message.sendToActiveTab('EMIT_TOGGLE_WM', { enabled: !enabled }); + if (enabled) { + this.stopAllSessions(); + } else { + await this.resumePaymentSessionActiveTab(); + } } async pay(amount: string) { diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index bd2b1c63..55bbc6e0 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -54,12 +54,6 @@ export class ContentScript { message.payload, ); return; - - case 'EMIT_TOGGLE_WM': - // this.monetizationTagManager.toggleWM(message.payload) - - return; - default: return; } diff --git a/src/shared/messages.ts b/src/shared/messages.ts index f9dea560..03cf1b11 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -222,10 +222,6 @@ export type BackgroundToContentMessage = { input: MonetizationEventPayload; output: never; }; - EMIT_TOGGLE_WM: { - input: EmitToggleWMPayload; - output: never; - }; }; export type ToContentMessage = { From 2df5eb6f33339d42b620614a887bd1160afb994b Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:06:10 +0530 Subject: [PATCH 10/34] remove todo notes/comments --- src/content/services/monetizationLinkManager.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index bfb0225a..1402326c 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -12,18 +12,6 @@ import type { import { ContentToContentAction } from '../messages'; import type { Cradle } from '@/content/container'; -// observeLinks() { -// find all links in the page -// observe changes to links - add/remove/attr-changes -// validate link tags -// } - -// observe page visibility - -// on change: start/stop/pause/resume monetization - -// handle events from background - monetization/load/error events - export class MonetizationLinkManager extends EventEmitter { private window: Cradle['window']; private document: Cradle['document']; From 038cfed6c61cfa559d09620b7856072a12c6d16e Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:07:19 +0530 Subject: [PATCH 11/34] remove unused types --- src/content/services/monetizationLinkManager.ts | 1 - src/content/services/monetizationTagManager.ts | 17 ++++++++--------- src/shared/messages.ts | 4 ---- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 1402326c..a14b9adf 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -3,7 +3,6 @@ import type { MonetizationTagDetails } from '../types'; import type { WalletAddress } from '@interledger/open-payments/dist/types'; import { checkWalletAddressUrlFormat, mozClone } from '../utils'; import type { - // EmitToggleWMPayload, MonetizationEventPayload, ResumeMonetizationPayload, StartMonetizationPayload, diff --git a/src/content/services/monetizationTagManager.ts b/src/content/services/monetizationTagManager.ts index af50d29d..6e7e756d 100644 --- a/src/content/services/monetizationTagManager.ts +++ b/src/content/services/monetizationTagManager.ts @@ -4,7 +4,6 @@ import type { MonetizationTagDetails } from '../types'; import type { WalletAddress } from '@interledger/open-payments/dist/types'; import { checkWalletAddressUrlFormat } from '../utils'; import type { - EmitToggleWMPayload, MonetizationEventPayload, ResumeMonetizationPayload, StartMonetizationPayload, @@ -555,12 +554,12 @@ export class MonetizationTagManager extends EventEmitter { }); } - async toggleWM({ enabled }: EmitToggleWMPayload) { - if (enabled) { - await this.resumeAllMonetization(); - } else { - // TODO: https://github.com/interledger/web-monetization-extension/issues/452 - this.stopAllMonetization(); - } - } + // async toggleWM({ enabled }: EmitToggleWMPayload) { + // if (enabled) { + // await this.resumeAllMonetization(); + // } else { + // // TODO: https://github.com/interledger/web-monetization-extension/issues/452 + // this.stopAllMonetization(); + // } + // } } diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 03cf1b11..bcc7d398 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -213,10 +213,6 @@ export interface MonetizationEventPayload { details: MonetizationEventDetails; } -export interface EmitToggleWMPayload { - enabled: boolean; -} - export type BackgroundToContentMessage = { MONETIZATION_EVENT: { input: MonetizationEventPayload; From bfa8d6474dcf6819dddd76df1009b51512c62a88 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:32:35 +0530 Subject: [PATCH 12/34] bring in iframe support with existing frameMananger --- src/content/services/contentScript.ts | 2 +- .../services/monetizationLinkManager.ts | 38 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index 55bbc6e0..ef758e18 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -38,7 +38,7 @@ export class ContentScript { if (this.isFirstLevelFrame) { this.logger.info('Content script started'); - // if (this.isTopFrame) this.frameManager.start() + if (this.isTopFrame) this.frameManager.start(); this.monetizationTagManager.start(); } diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index a14b9adf..73a470c2 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -50,7 +50,7 @@ export class MonetizationLinkManager extends EventEmitter { this.id = crypto.randomUUID(); if (!this.isTopFrame && this.isFirstLevelFrame) { - // this.bindMessageHandler() + this.bindMessageHandler(); } } @@ -127,6 +127,28 @@ export class MonetizationLinkManager extends EventEmitter { await this.sendStartMonetization(validLinks.map((e) => e.details)); } + private bindMessageHandler() { + type Message = { + message: ContentToContentAction; + id: string; + payload: any; + }; + this.window.addEventListener('message', (event: MessageEvent) => { + const { message, id, payload } = event.data; + + if (event.origin === window.location.href || id !== this.id) return; + + switch (message) { + case ContentToContentAction.START_MONETIZATION: + return void this.sendStartMonetization(payload, true); + case ContentToContentAction.RESUME_MONETIZATION: + return void this.sendResumeMonetization(payload, true); + default: + return; + } + }); + } + /** @throws never throws */ private async checkLink(link: HTMLLinkElement) { if (!(link instanceof HTMLLinkElement && link.rel === 'monetization')) { @@ -236,12 +258,15 @@ export class MonetizationLinkManager extends EventEmitter { await this.sendResumeMonetization(payload); } - private async sendStartMonetization(payload: StartMonetizationPayload[]) { + private async sendStartMonetization( + payload: StartMonetizationPayload[], + onlyToTopIframe = false, + ) { if (!payload.length) return; if (this.isTopFrame) { await this.message.send('START_MONETIZATION', payload); - } else if (this.isFirstLevelFrame) { + } else if (this.isFirstLevelFrame && !onlyToTopIframe) { this.window.parent.postMessage( { message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, @@ -258,12 +283,15 @@ export class MonetizationLinkManager extends EventEmitter { await this.message.send('STOP_MONETIZATION', payload); } - private async sendResumeMonetization(payload: ResumeMonetizationPayload[]) { + private async sendResumeMonetization( + payload: ResumeMonetizationPayload[], + onlyToTopIframe = false, + ) { if (this.isTopFrame) { if (payload.length) { await this.message.send('RESUME_MONETIZATION', payload); } - } else if (this.isFirstLevelFrame) { + } else if (this.isFirstLevelFrame && !onlyToTopIframe) { this.window.parent.postMessage( { message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, From c53b7e19daf8b264643ebe03b55e2d54f0c37efc Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:44:35 +0530 Subject: [PATCH 13/34] remove monetizationTagManager; use monetizationLinkManager --- src/content/container.ts | 4 +- src/content/services/contentScript.ts | 12 +- .../services/monetizationTagManager.ts | 565 ------------------ 3 files changed, 8 insertions(+), 573 deletions(-) delete mode 100644 src/content/services/monetizationTagManager.ts diff --git a/src/content/container.ts b/src/content/container.ts index a31e442f..c5eaf2d8 100644 --- a/src/content/container.ts +++ b/src/content/container.ts @@ -16,7 +16,7 @@ export interface Cradle { document: Document; window: Window; message: MessageManager; - monetizationTagManager: MonetizationLinkManager; + monetizationLinkManager: MonetizationLinkManager; frameManager: FrameManager; contentScript: ContentScript; } @@ -39,7 +39,7 @@ export const configureContainer = () => { .inject(() => ({ logger: logger.getLogger('content-script:frameManager'), })), - monetizationTagManager: asClass(MonetizationLinkManager) + monetizationLinkManager: asClass(MonetizationLinkManager) .singleton() .inject(() => ({ logger: logger.getLogger('content-script:tagManager'), diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index ef758e18..85466344 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -1,12 +1,12 @@ import type { ToContentMessage } from '@/shared/messages'; -import { failure } from '@/shared/helpers'; import type { Cradle } from '@/content/container'; +import { failure } from '@/shared/helpers'; export class ContentScript { private browser: Cradle['browser']; private window: Cradle['window']; private logger: Cradle['logger']; - private monetizationTagManager: Cradle['monetizationTagManager']; + private monetizationLinkManager: Cradle['monetizationLinkManager']; private frameManager: Cradle['frameManager']; private isFirstLevelFrame: boolean; @@ -16,14 +16,14 @@ export class ContentScript { browser, window, logger, - monetizationTagManager, + monetizationLinkManager, frameManager, }: Cradle) { Object.assign(this, { browser, window, logger, - monetizationTagManager, + monetizationLinkManager, frameManager, }); @@ -40,7 +40,7 @@ export class ContentScript { if (this.isTopFrame) this.frameManager.start(); - this.monetizationTagManager.start(); + this.monetizationLinkManager.start(); } } @@ -50,7 +50,7 @@ export class ContentScript { try { switch (message.action) { case 'MONETIZATION_EVENT': - this.monetizationTagManager.dispatchMonetizationEvent( + this.monetizationLinkManager.dispatchMonetizationEvent( message.payload, ); return; diff --git a/src/content/services/monetizationTagManager.ts b/src/content/services/monetizationTagManager.ts deleted file mode 100644 index 6e7e756d..00000000 --- a/src/content/services/monetizationTagManager.ts +++ /dev/null @@ -1,565 +0,0 @@ -import { EventEmitter } from 'events'; -import { mozClone } from '../utils'; -import type { MonetizationTagDetails } from '../types'; -import type { WalletAddress } from '@interledger/open-payments/dist/types'; -import { checkWalletAddressUrlFormat } from '../utils'; -import type { - MonetizationEventPayload, - ResumeMonetizationPayload, - StartMonetizationPayload, - StopMonetizationPayload, -} from '@/shared/messages'; -import { ContentToContentAction } from '../messages'; -import type { Cradle } from '@/content/container'; - -export type MonetizationTag = HTMLLinkElement; - -interface FireOnMonetizationChangeIfHaveAttributeParams { - node: HTMLElement; - changeDetected?: boolean; -} - -export class MonetizationTagManager extends EventEmitter { - private window: Cradle['window']; - private document: Cradle['document']; - private logger: Cradle['logger']; - private message: Cradle['message']; - - private isTopFrame: boolean; - private isFirstLevelFrame: boolean; - private documentObserver: MutationObserver; - private monetizationTagAttrObserver: MutationObserver; - private id: string; - private monetizationTags = new Map(); - - constructor({ window, document, logger, message }: Cradle) { - super(); - Object.assign(this, { - window, - document, - logger, - message, - }); - - this.documentObserver = new MutationObserver((records) => - this.onWholeDocumentObserved(records), - ); - this.monetizationTagAttrObserver = new MutationObserver((records) => - this.onMonetizationTagAttrsChange(records), - ); - - document.addEventListener('visibilitychange', async () => { - if (document.visibilityState === 'visible') { - await this.resumeAllMonetization(); - } else { - this.stopAllMonetization(); - } - }); - - this.isTopFrame = window === window.top; - this.isFirstLevelFrame = window.parent === window.top; - this.id = crypto.randomUUID(); - - if (!this.isTopFrame && this.isFirstLevelFrame) { - this.bindMessageHandler(); - } - } - - private dispatchLoadEvent(tag: MonetizationTag) { - tag.dispatchEvent(new Event('load')); - } - - private dispatchErrorEvent(tag: MonetizationTag) { - tag.dispatchEvent(new Event('error')); - } - - dispatchMonetizationEvent({ requestId, details }: MonetizationEventPayload) { - this.monetizationTags.forEach((tagDetails, tag) => { - if (tagDetails.requestId !== requestId) return; - - tag.dispatchEvent( - new CustomEvent('__wm_ext_monetization', { - detail: mozClone(details, this.document), - bubbles: true, - }), - ); - }); - return; - } - - private async resumeAllMonetization() { - const response = await this.message.send('IS_WM_ENABLED'); - - if (response.success && response.payload) { - const resumeMonetizationTags: ResumeMonetizationPayload[] = []; - - this.monetizationTags.forEach((value) => { - if (value.requestId && value.walletAddress) { - resumeMonetizationTags.push({ requestId: value.requestId }); - } - }); - - this.sendResumeMonetization(resumeMonetizationTags); - } - } - - private stopAllMonetization(intent?: StopMonetizationPayload['intent']) { - const stopMonetizationTags: StopMonetizationPayload[] = []; - this.monetizationTags.forEach((value) => { - if (value.requestId && value.walletAddress) { - stopMonetizationTags.push({ requestId: value.requestId, intent }); - } - }); - - this.sendStopMonetization(stopMonetizationTags); - } - - private async onWholeDocumentObserved(records: MutationRecord[]) { - const startMonetizationTagsPromises: Promise[] = - []; - const stopMonetizationTags: StopMonetizationPayload[] = []; - - for (const record of records) { - if (record.type === 'childList') { - record.removedNodes.forEach(async (node) => { - const stopMonetizationTag = this.checkRemoved(node); - if (stopMonetizationTag) - stopMonetizationTags.push(stopMonetizationTag); - }); - } - } - - await this.sendStopMonetization(stopMonetizationTags); - - if (this.isTopFrame) { - for (const record of records) { - if (record.type === 'childList') { - record.addedNodes.forEach(async (node) => { - const startMonetizationTag = this.checkAdded(node); - startMonetizationTagsPromises.push(startMonetizationTag); - }); - } - } - - Promise.allSettled(startMonetizationTagsPromises).then((result) => { - const startMonetizationTags: StartMonetizationPayload[] = []; - result.forEach((res) => { - if (res.status === 'fulfilled' && res.value) { - startMonetizationTags.push(res.value); - } - }); - - this.sendStartMonetization(startMonetizationTags); - }); - } - - this.onOnMonetizationChangeObserved(records); - } - - async onMonetizationTagAttrsChange(records: MutationRecord[]) { - const handledTags = new Set(); - const startMonetizationTags: StartMonetizationPayload[] = []; - const stopMonetizationTags: StopMonetizationPayload[] = []; - - // Check for a non specified link with the type now specified and - // just treat it as a newly seen, monetization tag - for (const record of records) { - const target = record.target as MonetizationTag; - if (handledTags.has(target)) { - continue; - } - const hasTarget = this.monetizationTags.has(target); - const typeSpecified = - target instanceof HTMLLinkElement && target.rel === 'monetization'; - // this will also handle the case of a @disabled tag that - // is not tracked, becoming enabled - if (!hasTarget && typeSpecified) { - const startMonetizationTag = await this.onAddedTag(target); - if (startMonetizationTag) - startMonetizationTags.push(startMonetizationTag); - - handledTags.add(target); - } else if (hasTarget && !typeSpecified) { - const stopMonetizationTag = this.onRemovedTag(target); - stopMonetizationTags.push(stopMonetizationTag); - - handledTags.add(target); - } else if (!hasTarget && !typeSpecified) { - // ignore these changes - handledTags.add(target); - } else if (hasTarget && typeSpecified) { - if ( - record.type === 'attributes' && - record.attributeName === 'disabled' && - target instanceof HTMLLinkElement && - target.getAttribute('disabled') !== record.oldValue - ) { - const wasDisabled = record.oldValue !== null; - const isDisabled = target.hasAttribute('disabled'); - if (wasDisabled != isDisabled) { - try { - const { requestId, walletAddress } = this.getTagDetails( - target, - 'onChangeDisabled', - ); - if (isDisabled) { - stopMonetizationTags.push({ requestId, intent: 'disable' }); - } else if (walletAddress) { - startMonetizationTags.push({ requestId, walletAddress }); - } - } catch { - const startMonetizationPayload = await this.onAddedTag(target); - if (startMonetizationPayload) { - startMonetizationTags.push(startMonetizationPayload); - } - } - - handledTags.add(target); - } - } else if ( - record.type === 'attributes' && - record.attributeName === 'href' && - target instanceof HTMLLinkElement && - target.href !== record.oldValue - ) { - const { startMonetizationTag, stopMonetizationTag } = - await this.onChangedWalletAddressUrl(target); - if (startMonetizationTag) - startMonetizationTags.push(startMonetizationTag); - if (stopMonetizationTag) - stopMonetizationTags.push(stopMonetizationTag); - - handledTags.add(target); - } - } - } - - await this.sendStopMonetization(stopMonetizationTags); - this.sendStartMonetization(startMonetizationTags); - } - - private async checkAdded(node: Node) { - if (node instanceof HTMLElement) { - this.fireOnMonetizationAttrChangedEvent({ node }); - } - - if (node instanceof HTMLLinkElement) { - this.observeMonetizationTagAttrs(node); - return await this.onAddedTag(node); - } - - return null; - } - - private checkRemoved(node: Node) { - return node instanceof HTMLLinkElement && this.monetizationTags.has(node) - ? this.onRemovedTag(node) - : null; - } - - private observeMonetizationTagAttrs(tag: MonetizationTag) { - this.monetizationTagAttrObserver.observe(tag, { - childList: false, - attributeOldValue: true, - attributeFilter: ['href', 'disabled', 'rel', 'crossorigin', 'type'], - }); - } - - private getTagDetails(tag: MonetizationTag, caller = '') { - const tagDetails = this.monetizationTags.get(tag); - - if (!tagDetails) { - throw new Error( - `${caller}: tag not tracked: ${tag.outerHTML.slice(0, 200)}`, - ); - } - - return tagDetails; - } - - // If wallet address changed, remove old tag and add new one - async onChangedWalletAddressUrl( - tag: MonetizationTag, - wasDisabled = false, - isDisabled = false, - ) { - let stopMonetizationTag = null; - - if (!wasDisabled && !isDisabled) { - stopMonetizationTag = this.onRemovedTag(tag); - } - - const startMonetizationTag = await this.onAddedTag(tag); - - return { startMonetizationTag, stopMonetizationTag }; - } - - private onOnMonetizationChangeObserved(records: MutationRecord[]) { - for (const record of records) { - if ( - record.type === 'attributes' && - record.target instanceof HTMLElement && - record.attributeName === 'onmonetization' - ) { - this.fireOnMonetizationAttrChangedEvent({ - node: record.target, - changeDetected: true, - }); - } - } - } - - private fireOnMonetizationAttrChangedEvent({ - node, - changeDetected = false, - }: FireOnMonetizationChangeIfHaveAttributeParams) { - const attribute = node.getAttribute('onmonetization'); - - if (!attribute && !changeDetected) return; - - const customEvent = new CustomEvent('__wm_ext_onmonetization_attr_change', { - bubbles: true, - detail: mozClone({ attribute }, this.document), - }); - - node.dispatchEvent(customEvent); - } - - private isDocumentReady() { - return ( - (document.readyState === 'interactive' || - document.readyState === 'complete') && - document.visibilityState === 'visible' - ); - } - - start(): void { - if (this.isDocumentReady()) { - this.run(); - return; - } - - document.addEventListener( - 'readystatechange', - () => { - if (this.isDocumentReady()) { - this.run(); - } else { - document.addEventListener( - 'visibilitychange', - () => { - if (this.isDocumentReady()) { - this.run(); - } - }, - { once: true }, - ); - } - }, - { once: true }, - ); - } - - private run() { - if (!this.isTopFrame && this.isFirstLevelFrame) { - this.window.parent.postMessage( - { - message: ContentToContentAction.INITIALIZE_IFRAME, - id: this.id, - }, - '*', - ); - } - - let monetizationTags: NodeListOf | MonetizationTag[]; - - if (this.isTopFrame) { - monetizationTags = this.document.querySelectorAll( - 'link[rel="monetization"]', - ); - } else { - const monetizationTag: MonetizationTag | null = - this.document.querySelector('head link[rel="monetization"]'); - monetizationTags = monetizationTag ? [monetizationTag] : []; - } - - const startMonetizationTagsPromises: Promise[] = - []; - - monetizationTags.forEach(async (tag) => { - try { - this.observeMonetizationTagAttrs(tag); - const startMonetizationTag = this.onAddedTag(tag); - startMonetizationTagsPromises.push(startMonetizationTag); - } catch (e) { - this.logger.error(e); - } - }); - - Promise.allSettled(startMonetizationTagsPromises).then((result) => { - const startMonetizationTags: StartMonetizationPayload[] = []; - result.forEach((res) => { - if (res.status === 'fulfilled' && res.value) { - startMonetizationTags.push(res.value); - } - }); - - this.sendStartMonetization(startMonetizationTags); - }); - - const onMonetizations: NodeListOf = - this.document.querySelectorAll('[onmonetization]'); - - onMonetizations.forEach((node) => { - this.fireOnMonetizationAttrChangedEvent({ node }); - }); - - this.documentObserver.observe(this.document, { - subtree: true, - childList: true, - attributeFilter: ['onmonetization'], - }); - } - - stop() { - this.documentObserver.disconnect(); - this.monetizationTagAttrObserver.disconnect(); - this.monetizationTags.clear(); - } - - // Remove tag from list & stop monetization - private onRemovedTag(tag: MonetizationTag): StopMonetizationPayload { - const { requestId } = this.getTagDetails(tag, 'onRemovedTag'); - this.monetizationTags.delete(tag); - - return { requestId, intent: 'remove' }; - } - - // Add tag to list & start monetization - private async onAddedTag( - tag: MonetizationTag, - crtRequestId?: string, - ): Promise { - const walletAddress = await this.checkTag(tag); - if (!walletAddress) return null; - - const requestId = crtRequestId ?? crypto.randomUUID(); - const details: MonetizationTagDetails = { - walletAddress, - requestId, - }; - - this.monetizationTags.set(tag, details); - return { walletAddress, requestId }; - } - - private sendStartMonetization(tags: StartMonetizationPayload[]) { - if (!tags.length) return; - - if (this.isTopFrame) { - if (tags.length) { - void this.message.send('START_MONETIZATION', tags); - } - } else if (this.isFirstLevelFrame) { - this.window.parent.postMessage( - { - message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, - id: this.id, - payload: tags, - }, - '*', - ); - } - } - - private async sendStopMonetization(tags: StopMonetizationPayload[]) { - if (!tags.length) return; - await this.message.send('STOP_MONETIZATION', tags); - } - - private sendResumeMonetization(tags: ResumeMonetizationPayload[]) { - if (this.isTopFrame) { - if (tags.length) { - void this.message.send('RESUME_MONETIZATION', tags); - } - } else if (this.isFirstLevelFrame) { - this.window.parent.postMessage( - { - message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, - id: this.id, - payload: tags, - }, - '*', - ); - } - } - - // Check tag to be enabled and for valid wallet address - private async checkTag(tag: MonetizationTag): Promise { - if (!(tag instanceof HTMLLinkElement && tag.rel === 'monetization')) - return null; - - if (tag.hasAttribute('disabled')) return null; - - const walletAddressInfo = await this.validateWalletAddress(tag); - - return walletAddressInfo; - } - - private async validateWalletAddress( - tag: MonetizationTag, - ): Promise { - const walletAddressUrl = tag.href.trim(); - try { - checkWalletAddressUrlFormat(walletAddressUrl); - const response = await this.message.send('CHECK_WALLET_ADDRESS_URL', { - walletAddressUrl, - }); - - if (response.success === false) { - throw new Error( - `Could not retrieve wallet address information for ${JSON.stringify(walletAddressUrl)}.`, - ); - } - - this.dispatchLoadEvent(tag); - return response.payload; - } catch (e) { - this.logger.error(e); - this.dispatchErrorEvent(tag); - return null; - } - } - - private bindMessageHandler() { - this.window.addEventListener('message', (event) => { - const { message, id, payload } = event.data; - - if (event.origin === window.location.href || id !== this.id) return; - - switch (message) { - case ContentToContentAction.START_MONETIZATION: - if (payload.length) { - void this.message.send('START_MONETIZATION', payload); - } - return; - case ContentToContentAction.RESUME_MONETIZATION: - if (payload.length) { - void this.message.send('RESUME_MONETIZATION', payload); - } - return; - default: - return; - } - }); - } - - // async toggleWM({ enabled }: EmitToggleWMPayload) { - // if (enabled) { - // await this.resumeAllMonetization(); - // } else { - // // TODO: https://github.com/interledger/web-monetization-extension/issues/452 - // this.stopAllMonetization(); - // } - // } -} From b192f056fa3e18c6cc8649277018053038bcdde9 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:52:40 +0530 Subject: [PATCH 14/34] update payload types (extract *PayloadEntry out) --- src/background/services/monetization.ts | 6 +++--- src/content/services/frameManager.ts | 10 +++++----- .../services/monetizationLinkManager.ts | 19 ++++++++++--------- src/shared/messages.ts | 15 +++++++++------ 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index ce78575e..d657a223 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -52,7 +52,7 @@ export class MonetizationService { } async startPaymentSession( - payload: StartMonetizationPayload[], + payload: StartMonetizationPayload, sender: Runtime.MessageSender, ) { if (!payload.length) { @@ -146,7 +146,7 @@ export class MonetizationService { } async stopPaymentSession( - payload: StopMonetizationPayload[], + payload: StopMonetizationPayload, sender: Runtime.MessageSender, ) { let needsAdjustAmount = false; @@ -191,7 +191,7 @@ export class MonetizationService { } async resumePaymentSession( - payload: ResumeMonetizationPayload[], + payload: ResumeMonetizationPayload, sender: Runtime.MessageSender, ) { const tabId = getTabId(sender); diff --git a/src/content/services/frameManager.ts b/src/content/services/frameManager.ts index 61b0552e..9ce50ba8 100644 --- a/src/content/services/frameManager.ts +++ b/src/content/services/frameManager.ts @@ -1,7 +1,7 @@ import { ContentToContentAction } from '../messages'; import type { - ResumeMonetizationPayload, - StartMonetizationPayload, + ResumeMonetizationPayloadEntry, + StartMonetizationPayloadEntry, StopMonetizationPayload, } from '@/shared/messages'; import type { Cradle } from '@/content/container'; @@ -103,7 +103,7 @@ export class FrameManager { const frameDetails = this.frames.get(frame); - const stopMonetizationTags: StopMonetizationPayload[] = + const stopMonetizationTags: StopMonetizationPayload = frameDetails?.requestIds.map((requestId) => ({ requestId, intent: 'remove', @@ -213,7 +213,7 @@ export class FrameManager { this.frames.set(frame, { frameId: id, requestIds: payload.map( - (p: StartMonetizationPayload) => p.requestId, + (p: StartMonetizationPayloadEntry) => p.requestId, ), }); event.source.postMessage( @@ -234,7 +234,7 @@ export class FrameManager { this.frames.set(frame, { frameId: id, requestIds: payload.map( - (p: ResumeMonetizationPayload) => p.requestId, + (p: ResumeMonetizationPayloadEntry) => p.requestId, ), }); event.source.postMessage( diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 73a470c2..b0c26a4e 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -7,6 +7,7 @@ import type { ResumeMonetizationPayload, StartMonetizationPayload, StopMonetizationPayload, + StopMonetizationPayloadEntry, } from '@/shared/messages'; import { ContentToContentAction } from '../messages'; import type { Cradle } from '@/content/container'; @@ -243,7 +244,7 @@ export class MonetizationLinkManager extends EventEmitter { } private async stopMonetization() { - const payload: StopMonetizationPayload[] = [ + const payload: StopMonetizationPayload = [ ...this.monetizationLinks.values(), ].map(({ requestId }) => ({ requestId })); @@ -251,7 +252,7 @@ export class MonetizationLinkManager extends EventEmitter { } private async resumeMonetization() { - const payload: ResumeMonetizationPayload[] = [ + const payload: ResumeMonetizationPayload = [ ...this.monetizationLinks.values(), ].map(({ requestId }) => ({ requestId })); @@ -259,7 +260,7 @@ export class MonetizationLinkManager extends EventEmitter { } private async sendStartMonetization( - payload: StartMonetizationPayload[], + payload: StartMonetizationPayload, onlyToTopIframe = false, ) { if (!payload.length) return; @@ -278,13 +279,13 @@ export class MonetizationLinkManager extends EventEmitter { } } - private async sendStopMonetization(payload: StopMonetizationPayload[]) { + private async sendStopMonetization(payload: StopMonetizationPayload) { if (!payload.length) return; await this.message.send('STOP_MONETIZATION', payload); } private async sendResumeMonetization( - payload: ResumeMonetizationPayload[], + payload: ResumeMonetizationPayload, onlyToTopIframe = false, ) { if (this.isTopFrame) { @@ -304,7 +305,7 @@ export class MonetizationLinkManager extends EventEmitter { } private async onWholeDocumentObserved(records: MutationRecord[]) { - const stopMonetizationPayload: StopMonetizationPayload[] = []; + const stopMonetizationPayload: StopMonetizationPayload = []; for (const record of records) { if (record.type === 'childList') { @@ -347,8 +348,8 @@ export class MonetizationLinkManager extends EventEmitter { private async onLinkAttrChange(records: MutationRecord[]) { const handledTags = new Set(); - const startMonetizationPayload: StartMonetizationPayload[] = []; - const stopMonetizationPayload: StopMonetizationPayload[] = []; + const startMonetizationPayload: StartMonetizationPayload = []; + const stopMonetizationPayload: StopMonetizationPayload = []; // Check for a non specified link with the type now specified and // just treat it as a newly seen, monetization tag @@ -450,7 +451,7 @@ export class MonetizationLinkManager extends EventEmitter { return res; } - private onRemovedLink(link: HTMLLinkElement): StopMonetizationPayload { + private onRemovedLink(link: HTMLLinkElement): StopMonetizationPayloadEntry { const details = this.monetizationLinks.get(link); if (!details) { throw new Error( diff --git a/src/shared/messages.ts b/src/shared/messages.ts index bcc7d398..2f317328 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -147,19 +147,22 @@ export interface CheckWalletAddressUrlPayload { walletAddressUrl: string; } -export interface StartMonetizationPayload { +export interface StartMonetizationPayloadEntry { walletAddress: WalletAddress; requestId: string; } +export type StartMonetizationPayload = StartMonetizationPayloadEntry[]; -export interface StopMonetizationPayload { +export interface StopMonetizationPayloadEntry { requestId: string; intent?: 'remove' | 'disable'; } +export type StopMonetizationPayload = StopMonetizationPayloadEntry[]; -export interface ResumeMonetizationPayload { +export interface ResumeMonetizationPayloadEntry { requestId: string; } +export type ResumeMonetizationPayload = ResumeMonetizationPayloadEntry[]; export interface IsTabMonetizedPayload { value: boolean; @@ -171,15 +174,15 @@ export type ContentToBackgroundMessage = { output: WalletAddress; }; STOP_MONETIZATION: { - input: StopMonetizationPayload[]; + input: StopMonetizationPayload; output: never; }; START_MONETIZATION: { - input: StartMonetizationPayload[]; + input: StartMonetizationPayload; output: never; }; RESUME_MONETIZATION: { - input: ResumeMonetizationPayload[]; + input: ResumeMonetizationPayload; output: never; }; IS_WM_ENABLED: { From 5885b5f8c0a76817421da882c5f56fd970c74b21 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:58:18 +0530 Subject: [PATCH 15/34] nit: extract postMessage call to private method --- .../services/monetizationLinkManager.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index b0c26a4e..f9253d95 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -268,13 +268,9 @@ export class MonetizationLinkManager extends EventEmitter { if (this.isTopFrame) { await this.message.send('START_MONETIZATION', payload); } else if (this.isFirstLevelFrame && !onlyToTopIframe) { - this.window.parent.postMessage( - { - message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, - id: this.id, - payload: payload, - }, - '*', + this.postMessage( + ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, + payload, ); } } @@ -293,13 +289,9 @@ export class MonetizationLinkManager extends EventEmitter { await this.message.send('RESUME_MONETIZATION', payload); } } else if (this.isFirstLevelFrame && !onlyToTopIframe) { - this.window.parent.postMessage( - { - message: ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, - id: this.id, - payload: payload, - }, - '*', + this.postMessage( + ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, + payload, ); } } @@ -346,6 +338,10 @@ export class MonetizationLinkManager extends EventEmitter { } } + private postMessage(message: ContentToContentAction, payload: any) { + this.window.parent.postMessage({ message, id: this.id, payload }, '*'); + } + private async onLinkAttrChange(records: MutationRecord[]) { const handledTags = new Set(); const startMonetizationPayload: StartMonetizationPayload = []; From 1a9920e6a0d5ede751b2967052eec83ba5b456c9 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:14:11 +0530 Subject: [PATCH 16/34] add types for Window.postMessage messages --- src/content/messages.ts | 30 ++++++++--- src/content/services/frameManager.ts | 43 +++++++-------- .../services/monetizationLinkManager.ts | 53 +++++++++---------- 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/src/content/messages.ts b/src/content/messages.ts index 4eb30d26..c2657404 100644 --- a/src/content/messages.ts +++ b/src/content/messages.ts @@ -1,9 +1,23 @@ -export enum ContentToContentAction { - INITIALIZE_IFRAME = 'INITIALIZE_IFRAME', - IS_MONETIZATION_ALLOWED_ON_START = 'IS_MONETIZATION_ALLOWED_ON_START', - IS_MONETIZATION_ALLOWED_ON_RESUME = 'IS_MONETIZATION_ALLOWED_ON_RESUME', - IS_MONETIZATION_ALLOWED_ON_STOP = 'IS_MONETIZATION_ALLOWED_ON_STOP', - START_MONETIZATION = 'START_MONETIZATION', - STOP_MONETIZATION = 'STOP_MONETIZATION', - RESUME_MONETIZATION = 'RESUME_MONETIZATION', +import type { + ResumeMonetizationPayload, + StartMonetizationPayload, + StopMonetizationPayload, +} from '@/shared/messages'; + +export interface ContentToContentMessageMap { + INITIALIZE_IFRAME: void; + IS_MONETIZATION_ALLOWED_ON_START: StartMonetizationPayload; + IS_MONETIZATION_ALLOWED_ON_RESUME: ResumeMonetizationPayload; + IS_MONETIZATION_ALLOWED_ON_STOP: StopMonetizationPayload; + START_MONETIZATION: StartMonetizationPayload; + STOP_MONETIZATION: StopMonetizationPayload; + RESUME_MONETIZATION: ResumeMonetizationPayload; } + +export type ContentToContentMessage = { + [K in keyof ContentToContentMessageMap]: { + message: K; + id: string; + payload: ContentToContentMessageMap[K]; + }; +}[keyof ContentToContentMessageMap]; diff --git a/src/content/services/frameManager.ts b/src/content/services/frameManager.ts index 9ce50ba8..6c47e3bc 100644 --- a/src/content/services/frameManager.ts +++ b/src/content/services/frameManager.ts @@ -1,4 +1,4 @@ -import { ContentToContentAction } from '../messages'; +import type { ContentToContentMessage } from '../messages'; import type { ResumeMonetizationPayloadEntry, StartMonetizationPayloadEntry, @@ -176,21 +176,22 @@ export class FrameManager { this.observeDocumentForFrames(); } + static handledMessages: ContentToContentMessage['message'][] = [ + 'INITIALIZE_IFRAME', + 'IS_MONETIZATION_ALLOWED_ON_START', + 'IS_MONETIZATION_ALLOWED_ON_RESUME', + ]; + private bindMessageHandler() { this.window.addEventListener( 'message', - (event: any) => { + (event: MessageEvent) => { const { message, payload, id } = event.data; - if ( - ![ - ContentToContentAction.INITIALIZE_IFRAME, - ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, - ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, - ].includes(message) - ) { + if (!FrameManager.handledMessages.includes(message)) { return; } - const frame = this.findIframe(event.source); + const eventSource = event.source as Window; + const frame = this.findIframe(eventSource); if (!frame) { event.stopPropagation(); return; @@ -199,7 +200,7 @@ export class FrameManager { if (event.origin === this.window.location.href) return; switch (message) { - case ContentToContentAction.INITIALIZE_IFRAME: + case 'INITIALIZE_IFRAME': event.stopPropagation(); this.frames.set(frame, { frameId: id, @@ -207,7 +208,7 @@ export class FrameManager { }); return; - case ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START: + case 'IS_MONETIZATION_ALLOWED_ON_START': event.stopPropagation(); if (frame.allow === 'monetization') { this.frames.set(frame, { @@ -216,19 +217,15 @@ export class FrameManager { (p: StartMonetizationPayloadEntry) => p.requestId, ), }); - event.source.postMessage( - { - message: ContentToContentAction.START_MONETIZATION, - id, - payload, - }, + eventSource.postMessage( + { message: 'START_MONETIZATION', id, payload }, '*', ); } return; - case ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME: + case 'IS_MONETIZATION_ALLOWED_ON_RESUME': event.stopPropagation(); if (frame.allow === 'monetization') { this.frames.set(frame, { @@ -237,12 +234,8 @@ export class FrameManager { (p: ResumeMonetizationPayloadEntry) => p.requestId, ), }); - event.source.postMessage( - { - message: ContentToContentAction.RESUME_MONETIZATION, - id, - payload, - }, + eventSource.postMessage( + { message: 'RESUME_MONETIZATION', id, payload }, '*', ); } diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index f9253d95..93cc6a17 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -9,7 +9,7 @@ import type { StopMonetizationPayload, StopMonetizationPayloadEntry, } from '@/shared/messages'; -import { ContentToContentAction } from '../messages'; +import type { ContentToContentMessage } from '../messages'; import type { Cradle } from '@/content/container'; export class MonetizationLinkManager extends EventEmitter { @@ -129,25 +129,23 @@ export class MonetizationLinkManager extends EventEmitter { } private bindMessageHandler() { - type Message = { - message: ContentToContentAction; - id: string; - payload: any; - }; - this.window.addEventListener('message', (event: MessageEvent) => { - const { message, id, payload } = event.data; - - if (event.origin === window.location.href || id !== this.id) return; - - switch (message) { - case ContentToContentAction.START_MONETIZATION: - return void this.sendStartMonetization(payload, true); - case ContentToContentAction.RESUME_MONETIZATION: - return void this.sendResumeMonetization(payload, true); - default: - return; - } - }); + this.window.addEventListener( + 'message', + (event: MessageEvent) => { + const { message, id, payload } = event.data; + + if (event.origin === window.location.href || id !== this.id) return; + + switch (message) { + case 'START_MONETIZATION': + return void this.sendStartMonetization(payload, true); + case 'RESUME_MONETIZATION': + return void this.sendResumeMonetization(payload, true); + default: + return; + } + }, + ); } /** @throws never throws */ @@ -268,10 +266,7 @@ export class MonetizationLinkManager extends EventEmitter { if (this.isTopFrame) { await this.message.send('START_MONETIZATION', payload); } else if (this.isFirstLevelFrame && !onlyToTopIframe) { - this.postMessage( - ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START, - payload, - ); + this.postMessage('IS_MONETIZATION_ALLOWED_ON_START', payload); } } @@ -289,10 +284,7 @@ export class MonetizationLinkManager extends EventEmitter { await this.message.send('RESUME_MONETIZATION', payload); } } else if (this.isFirstLevelFrame && !onlyToTopIframe) { - this.postMessage( - ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME, - payload, - ); + this.postMessage('IS_MONETIZATION_ALLOWED_ON_RESUME', payload); } } @@ -338,7 +330,10 @@ export class MonetizationLinkManager extends EventEmitter { } } - private postMessage(message: ContentToContentAction, payload: any) { + private postMessage( + message: K, + payload: Extract['payload'], + ) { this.window.parent.postMessage({ message, id: this.id, payload }, '*'); } From d1e837f90135cc945cd438e0243730f320b96883 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:28:18 +0530 Subject: [PATCH 17/34] nit --- src/content/services/monetizationLinkManager.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 93cc6a17..88ce2fd4 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -213,7 +213,10 @@ export class MonetizationLinkManager extends EventEmitter { tag.dispatchEvent(new Event('error')); } - dispatchMonetizationEvent({ requestId, details }: MonetizationEventPayload) { + public dispatchMonetizationEvent({ + requestId, + details, + }: MonetizationEventPayload) { for (const [tag, tagDetails] of this.monetizationLinks) { if (tagDetails.requestId !== requestId) continue; From 0bea75c60733b6acce5d0ed02c1b1dce22fc5c4b Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:02:50 +0530 Subject: [PATCH 18/34] implement MonetizationLinkManager.end (unlisten everything) --- .../services/monetizationLinkManager.ts | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 88ce2fd4..e81b8ebe 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -49,10 +49,6 @@ export class MonetizationLinkManager extends EventEmitter { this.isTopFrame = window === window.top; this.isFirstLevelFrame = window.parent === window.top; this.id = crypto.randomUUID(); - - if (!this.isTopFrame && this.isFirstLevelFrame) { - this.bindMessageHandler(); - } } start(): void { @@ -82,19 +78,29 @@ export class MonetizationLinkManager extends EventEmitter { ); } - end() {} + end() { + this.documentObserver.disconnect(); + this.monetizationLinkAttrObserver.disconnect(); + this.monetizationLinks.clear(); + this.document.removeEventListener( + 'visibilitychange', + this.onDocumentVisibilityChange, + ); + this.window.removeEventListener('message', this.onWindowMessage); + } /** * Check if iframe or not */ private async run() { - this.document.addEventListener('visibilitychange', () => { - if (this.document.visibilityState === 'visible') { - void this.resumeMonetization(); - } else { - void this.stopMonetization(); - } - }); + this.document.addEventListener( + 'visibilitychange', + this.onDocumentVisibilityChange, + ); + + if (!this.isTopFrame && this.isFirstLevelFrame) { + this.window.addEventListener('message', this.onWindowMessage); + } this.document .querySelectorAll('[onmonetization]') @@ -128,25 +134,20 @@ export class MonetizationLinkManager extends EventEmitter { await this.sendStartMonetization(validLinks.map((e) => e.details)); } - private bindMessageHandler() { - this.window.addEventListener( - 'message', - (event: MessageEvent) => { - const { message, id, payload } = event.data; - - if (event.origin === window.location.href || id !== this.id) return; - - switch (message) { - case 'START_MONETIZATION': - return void this.sendStartMonetization(payload, true); - case 'RESUME_MONETIZATION': - return void this.sendResumeMonetization(payload, true); - default: - return; - } - }, - ); - } + private onWindowMessage = (event: MessageEvent) => { + const { message, id, payload } = event.data; + + if (event.origin === window.location.href || id !== this.id) return; + + switch (message) { + case 'START_MONETIZATION': + return void this.sendStartMonetization(payload, true); + case 'RESUME_MONETIZATION': + return void this.sendResumeMonetization(payload, true); + default: + return; + } + }; /** @throws never throws */ private async checkLink(link: HTMLLinkElement) { @@ -291,6 +292,14 @@ export class MonetizationLinkManager extends EventEmitter { } } + private onDocumentVisibilityChange = async () => { + if (this.document.visibilityState === 'visible') { + await this.resumeMonetization(); + } else { + await this.stopMonetization(); + } + }; + private async onWholeDocumentObserved(records: MutationRecord[]) { const stopMonetizationPayload: StopMonetizationPayload = []; From 8bf32a253748658c9af9b5a5792406be565eec4e Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:03:55 +0530 Subject: [PATCH 19/34] don't define polyfill again on re-inject Relatively safe to do as we don't update it often. Would require page reload otherwise - which can lead to data loss. "undefining" properties is also not an option - as otherwise any script on page on reconfigure them. --- src/content/polyfill.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/content/polyfill.ts b/src/content/polyfill.ts index 062dae0d..b2af4015 100644 --- a/src/content/polyfill.ts +++ b/src/content/polyfill.ts @@ -1,5 +1,12 @@ import type { MonetizationEventPayload } from '@/shared/messages'; (function () { + const link = document.createElement('link'); + if (link.relList.supports('monetization')) { + // eslint-disable-next-line no-console + console.debug('Monetization is already supported'); + return; + } + const handlers = new WeakMap(); const attributes: PropertyDescriptor & ThisType = { enumerable: true, From 76f2e9290c315ee93da7513d780747c329cab71d Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:05:12 +0530 Subject: [PATCH 20/34] cleanup MonetizationLinkManager on disconnect Prevents Extension context invalidated error (at least I don't see it in console anymore) --- src/content/services/contentScript.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index 85466344..0df90e1e 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -42,6 +42,11 @@ export class ContentScript { this.monetizationLinkManager.start(); } + + this.browser.runtime.connect().onDisconnect.addListener(() => { + this.logger.info('Disconnected, cleaning up'); + this.monetizationLinkManager.end(); + }); } bindMessageHandler() { From 32662ae5b4395d882cfb7e73eb936232e384a286 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:10:03 +0530 Subject: [PATCH 21/34] rename CHECK_WALLET_ADDRESS_URL to GET_WALLET_ADDRESS_INFO --- src/background/services/background.ts | 2 +- src/content/services/monetizationLinkManager.ts | 2 +- src/shared/messages.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/background/services/background.ts b/src/background/services/background.ts index f5b9fbad..d4e1ec03 100644 --- a/src/background/services/background.ts +++ b/src/background/services/background.ts @@ -185,7 +185,7 @@ export class Background { // endregion // region Content - case 'CHECK_WALLET_ADDRESS_URL': + case 'GET_WALLET_ADDRESS_INFO': return success( await getWalletInformation(message.payload.walletAddressUrl), ); diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index e81b8ebe..2bb04e91 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -179,7 +179,7 @@ export class MonetizationLinkManager extends EventEmitter { const walletAddressUrl = link.href.trim(); try { checkWalletAddressUrlFormat(walletAddressUrl); - const response = await this.message.send('CHECK_WALLET_ADDRESS_URL', { + const response = await this.message.send('GET_WALLET_ADDRESS_INFO', { walletAddressUrl, }); diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 2f317328..960d3a33 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -143,7 +143,7 @@ export type PopupToBackgroundMessage = { // #endregion // #region Content ↦ BG -export interface CheckWalletAddressUrlPayload { +export interface GetWalletAddressInfoPayload { walletAddressUrl: string; } @@ -169,8 +169,8 @@ export interface IsTabMonetizedPayload { } export type ContentToBackgroundMessage = { - CHECK_WALLET_ADDRESS_URL: { - input: CheckWalletAddressUrlPayload; + GET_WALLET_ADDRESS_INFO: { + input: GetWalletAddressInfoPayload; output: WalletAddress; }; STOP_MONETIZATION: { From 729e14ff83210d8a6066d0e7abad7bd84b431e92 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:16:12 +0530 Subject: [PATCH 22/34] move checkWalletAddressUrlFormat into monetizationLinkManager as static checkHrefFormat --- .../services/monetizationLinkManager.ts | 32 +++++++++++++++++-- src/content/utils.ts | 29 ----------------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 2bb04e91..54b9ebdf 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'events'; import type { MonetizationTagDetails } from '../types'; import type { WalletAddress } from '@interledger/open-payments/dist/types'; -import { checkWalletAddressUrlFormat, mozClone } from '../utils'; +import { mozClone, WalletAddressFormatError } from '../utils'; import type { MonetizationEventPayload, ResumeMonetizationPayload, @@ -178,7 +178,7 @@ export class MonetizationLinkManager extends EventEmitter { ): Promise { const walletAddressUrl = link.href.trim(); try { - checkWalletAddressUrlFormat(walletAddressUrl); + MonetizationLinkManager.checkHrefFormat(walletAddressUrl); const response = await this.message.send('GET_WALLET_ADDRESS_INFO', { walletAddressUrl, }); @@ -468,6 +468,34 @@ export class MonetizationLinkManager extends EventEmitter { return { requestId: details.requestId, intent: 'remove' }; } + + static checkHrefFormat(href: string): void { + let url: URL; + try { + url = new URL(href); + if (url.protocol !== 'https:') { + throw new WalletAddressFormatError( + `Wallet address URL must be specified as a fully resolved https:// url, ` + + `got ${JSON.stringify(href)} `, + ); + } + } catch (e) { + if (e instanceof WalletAddressFormatError) { + throw e; + } + throw new WalletAddressFormatError( + `Invalid wallet address URL: ${JSON.stringify(href)}`, + ); + } + + const { hash, search, port, username, password } = url; + + if (hash || search || port || username || password) { + throw new WalletAddressFormatError( + `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}`, + ); + } + } } function isDocumentReady(doc: Document) { diff --git a/src/content/utils.ts b/src/content/utils.ts index 1a341747..2899b74d 100644 --- a/src/content/utils.ts +++ b/src/content/utils.ts @@ -1,34 +1,5 @@ export class WalletAddressFormatError extends Error {} -export function checkWalletAddressUrlFormat(walletAddressUrl: string): void { - let url: URL; - try { - url = new URL(walletAddressUrl); - if (url.protocol !== 'https:') { - throw new WalletAddressFormatError( - `Wallet address URL must be specified as a fully resolved https:// url, ` + - `got ${JSON.stringify(walletAddressUrl)} `, - ); - } - } catch (e) { - if (e instanceof WalletAddressFormatError) { - throw e; - } else { - throw new WalletAddressFormatError( - `Invalid wallet address URL: ${JSON.stringify(walletAddressUrl)}`, - ); - } - } - - const { hash, search, port, username, password } = url; - - if (hash || search || port || username || password) { - throw new WalletAddressFormatError( - `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}`, - ); - } -} - type DefaultView = WindowProxy & typeof globalThis; type CloneInto = (obj: unknown, _window: DefaultView | null) => typeof obj; declare const cloneInto: CloneInto | undefined; From 5e47bfc23e97469872c4550e0b79c6a8ea85a2e0 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:17:31 +0530 Subject: [PATCH 23/34] move isNotNull to shared/helpers --- src/content/services/monetizationLinkManager.ts | 5 +---- src/shared/helpers.ts | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 54b9ebdf..d55c40f6 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -1,6 +1,7 @@ import { EventEmitter } from 'events'; import type { MonetizationTagDetails } from '../types'; import type { WalletAddress } from '@interledger/open-payments/dist/types'; +import { isNotNull } from '@/shared/helpers'; import { mozClone, WalletAddressFormatError } from '../utils'; import type { MonetizationEventPayload, @@ -520,7 +521,3 @@ function getMonetizationLinkTags( return monetizationTag ? [monetizationTag] : []; } } - -function isNotNull(value: T | null): value is T { - return value !== null; -} diff --git a/src/shared/helpers.ts b/src/shared/helpers.ts index 5f990de2..db5bc83b 100644 --- a/src/shared/helpers.ts +++ b/src/shared/helpers.ts @@ -222,6 +222,10 @@ export function objectEquals>(a: T, b: T) { return JSON.stringify(a, keysA.sort()) === JSON.stringify(b, keysB.sort()); } +export function isNotNull(value: T | null): value is T { + return value !== null; +} + export const removeQueryParams = (urlString: string) => { const url = new URL(urlString); return url.origin + url.pathname; From 1b8457058408b2e4c11b95af5071244fe2167ef0 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:24:00 +0530 Subject: [PATCH 24/34] move isDocumentReady in --- .../services/monetizationLinkManager.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index d55c40f6..a83cf14b 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -53,7 +53,15 @@ export class MonetizationLinkManager extends EventEmitter { } start(): void { - if (isDocumentReady(this.document)) { + const isDocumentReady = () => { + const doc = this.document; + return ( + (doc.readyState === 'interactive' || doc.readyState === 'complete') && + doc.visibilityState === 'visible' + ); + }; + + if (isDocumentReady()) { void this.run(); return; } @@ -61,13 +69,13 @@ export class MonetizationLinkManager extends EventEmitter { document.addEventListener( 'readystatechange', () => { - if (isDocumentReady(this.document)) { + if (isDocumentReady()) { void this.run(); } else { document.addEventListener( 'visibilitychange', () => { - if (isDocumentReady(this.document)) { + if (isDocumentReady()) { void this.run(); } }, @@ -499,12 +507,6 @@ export class MonetizationLinkManager extends EventEmitter { } } -function isDocumentReady(doc: Document) { - return ( - (doc.readyState === 'interactive' || doc.readyState === 'complete') && - doc.visibilityState === 'visible' - ); -} function getMonetizationLinkTags( document: Document, From d061527c6c6d40d8dcad81e733c31ac2bdf8488c Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:24:22 +0530 Subject: [PATCH 25/34] don't use statics, keep helpers separately testable --- .../services/monetizationLinkManager.ts | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index a83cf14b..dd22c163 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -187,7 +187,7 @@ export class MonetizationLinkManager extends EventEmitter { ): Promise { const walletAddressUrl = link.href.trim(); try { - MonetizationLinkManager.checkHrefFormat(walletAddressUrl); + checkHrefFormat(walletAddressUrl); const response = await this.message.send('GET_WALLET_ADDRESS_INFO', { walletAddressUrl, }); @@ -477,37 +477,8 @@ export class MonetizationLinkManager extends EventEmitter { return { requestId: details.requestId, intent: 'remove' }; } - - static checkHrefFormat(href: string): void { - let url: URL; - try { - url = new URL(href); - if (url.protocol !== 'https:') { - throw new WalletAddressFormatError( - `Wallet address URL must be specified as a fully resolved https:// url, ` + - `got ${JSON.stringify(href)} `, - ); - } - } catch (e) { - if (e instanceof WalletAddressFormatError) { - throw e; - } - throw new WalletAddressFormatError( - `Invalid wallet address URL: ${JSON.stringify(href)}`, - ); - } - - const { hash, search, port, username, password } = url; - - if (hash || search || port || username || password) { - throw new WalletAddressFormatError( - `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}`, - ); - } - } } - function getMonetizationLinkTags( document: Document, isTopFrame: boolean, @@ -523,3 +494,31 @@ function getMonetizationLinkTags( return monetizationTag ? [monetizationTag] : []; } } + +function checkHrefFormat(href: string): void { + let url: URL; + try { + url = new URL(href); + if (url.protocol !== 'https:') { + throw new WalletAddressFormatError( + `Wallet address URL must be specified as a fully resolved https:// url, ` + + `got ${JSON.stringify(href)} `, + ); + } + } catch (e) { + if (e instanceof WalletAddressFormatError) { + throw e; + } + throw new WalletAddressFormatError( + `Invalid wallet address URL: ${JSON.stringify(href)}`, + ); + } + + const { hash, search, port, username, password } = url; + + if (hash || search || port || username || password) { + throw new WalletAddressFormatError( + `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}`, + ); + } +} From ce72dfbe826491efc7e9a7d1c0d369f8120db612 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:31:45 +0530 Subject: [PATCH 26/34] move context-invalidation bug related code out of this PR --- src/content/polyfill.ts | 7 ------- src/content/services/contentScript.ts | 5 ----- 2 files changed, 12 deletions(-) diff --git a/src/content/polyfill.ts b/src/content/polyfill.ts index b2af4015..062dae0d 100644 --- a/src/content/polyfill.ts +++ b/src/content/polyfill.ts @@ -1,12 +1,5 @@ import type { MonetizationEventPayload } from '@/shared/messages'; (function () { - const link = document.createElement('link'); - if (link.relList.supports('monetization')) { - // eslint-disable-next-line no-console - console.debug('Monetization is already supported'); - return; - } - const handlers = new WeakMap(); const attributes: PropertyDescriptor & ThisType = { enumerable: true, diff --git a/src/content/services/contentScript.ts b/src/content/services/contentScript.ts index 0df90e1e..85466344 100644 --- a/src/content/services/contentScript.ts +++ b/src/content/services/contentScript.ts @@ -42,11 +42,6 @@ export class ContentScript { this.monetizationLinkManager.start(); } - - this.browser.runtime.connect().onDisconnect.addListener(() => { - this.logger.info('Disconnected, cleaning up'); - this.monetizationLinkManager.end(); - }); } bindMessageHandler() { From d35164593457752731700c996f32ae137127a86d Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:30:13 +0530 Subject: [PATCH 27/34] nit: simplify toggleWM logic --- src/background/services/monetization.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index d657a223..2c23856e 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -243,11 +243,12 @@ export class MonetizationService { async toggleWM() { const { enabled } = await this.storage.get(['enabled']); - await this.storage.set({ enabled: !enabled }); - if (enabled) { - this.stopAllSessions(); - } else { + const nowEnabled = !enabled; + await this.storage.set({ enabled: nowEnabled }); + if (nowEnabled) { await this.resumePaymentSessionActiveTab(); + } else { + this.stopAllSessions(); } } From 52e5f87ed564cfdfb5ab77425f81ef897250c61c Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:33:25 +0530 Subject: [PATCH 28/34] move getMonetizationLinkTags into MonetizationLinkManager --- .../services/monetizationLinkManager.ts | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index dd22c163..9d0840d0 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -123,10 +123,7 @@ export class MonetizationLinkManager extends EventEmitter { attributeFilter: ['onmonetization'], }); - const monetizationLinks = getMonetizationLinkTags( - this.document, - this.isTopFrame, - ); + const monetizationLinks = this.getMonetizationLinkTags(); for (const link of monetizationLinks) { this.observeLinkAttrs(link); @@ -158,6 +155,21 @@ export class MonetizationLinkManager extends EventEmitter { } }; + private getMonetizationLinkTags(): HTMLLinkElement[] { + if (this.isTopFrame) { + return Array.from( + this.document.querySelectorAll( + 'link[rel="monetization"]', + ), + ); + } else { + const monetizationTag = this.document.querySelector( + 'head link[rel="monetization"]', + ); + return monetizationTag ? [monetizationTag] : []; + } + } + /** @throws never throws */ private async checkLink(link: HTMLLinkElement) { if (!(link instanceof HTMLLinkElement && link.rel === 'monetization')) { @@ -479,22 +491,6 @@ export class MonetizationLinkManager extends EventEmitter { } } -function getMonetizationLinkTags( - document: Document, - isTopFrame: boolean, -): HTMLLinkElement[] { - if (isTopFrame) { - return Array.from( - document.querySelectorAll('link[rel="monetization"]'), - ); - } else { - const monetizationTag = document.querySelector( - 'head link[rel="monetization"]', - ); - return monetizationTag ? [monetizationTag] : []; - } -} - function checkHrefFormat(href: string): void { let url: URL; try { From 0aec47122d5f5b6be25f39f64d10d93ccbbc974a Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:34:27 +0530 Subject: [PATCH 29/34] frameManager: move HANDLED_MESSAGES to top-level constant --- src/content/services/frameManager.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/content/services/frameManager.ts b/src/content/services/frameManager.ts index 6c47e3bc..68b5835e 100644 --- a/src/content/services/frameManager.ts +++ b/src/content/services/frameManager.ts @@ -6,6 +6,12 @@ import type { } from '@/shared/messages'; import type { Cradle } from '@/content/container'; +const HANDLED_MESSAGES: ContentToContentMessage['message'][] = [ + 'INITIALIZE_IFRAME', + 'IS_MONETIZATION_ALLOWED_ON_START', + 'IS_MONETIZATION_ALLOWED_ON_RESUME', +]; + export class FrameManager { private window: Cradle['window']; private document: Cradle['document']; @@ -176,18 +182,12 @@ export class FrameManager { this.observeDocumentForFrames(); } - static handledMessages: ContentToContentMessage['message'][] = [ - 'INITIALIZE_IFRAME', - 'IS_MONETIZATION_ALLOWED_ON_START', - 'IS_MONETIZATION_ALLOWED_ON_RESUME', - ]; - private bindMessageHandler() { this.window.addEventListener( 'message', (event: MessageEvent) => { const { message, payload, id } = event.data; - if (!FrameManager.handledMessages.includes(message)) { + if (!HANDLED_MESSAGES.includes(message)) { return; } const eventSource = event.source as Window; From 92d3122682dbae87284b71ae1ba7523fc421f57a Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:36:12 +0530 Subject: [PATCH 30/34] style: import order --- src/content/services/monetizationLinkManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 9d0840d0..46cecb90 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -1,8 +1,7 @@ import { EventEmitter } from 'events'; -import type { MonetizationTagDetails } from '../types'; -import type { WalletAddress } from '@interledger/open-payments/dist/types'; import { isNotNull } from '@/shared/helpers'; import { mozClone, WalletAddressFormatError } from '../utils'; +import type { WalletAddress } from '@interledger/open-payments/dist/types'; import type { MonetizationEventPayload, ResumeMonetizationPayload, @@ -10,8 +9,9 @@ import type { StopMonetizationPayload, StopMonetizationPayloadEntry, } from '@/shared/messages'; -import type { ContentToContentMessage } from '../messages'; import type { Cradle } from '@/content/container'; +import type { MonetizationTagDetails } from '../types'; +import type { ContentToContentMessage } from '../messages'; export class MonetizationLinkManager extends EventEmitter { private window: Cradle['window']; From c7babdb348547ae382c4ee877a21da89ff0f6250 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:37:35 +0530 Subject: [PATCH 31/34] remove unused types --- src/content/services/monetizationLinkManager.ts | 3 +-- src/content/types.ts | 9 --------- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 src/content/types.ts diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 46cecb90..a61f0ec0 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -10,7 +10,6 @@ import type { StopMonetizationPayloadEntry, } from '@/shared/messages'; import type { Cradle } from '@/content/container'; -import type { MonetizationTagDetails } from '../types'; import type { ContentToContentMessage } from '../messages'; export class MonetizationLinkManager extends EventEmitter { @@ -27,7 +26,7 @@ export class MonetizationLinkManager extends EventEmitter { // only entries corresponding to valid wallet addresses are here private monetizationLinks = new Map< HTMLLinkElement, - MonetizationTagDetails + { walletAddress: WalletAddress; requestId: string } >(); constructor({ window, document, logger, message }: Cradle) { diff --git a/src/content/types.ts b/src/content/types.ts deleted file mode 100644 index 736a19f8..00000000 --- a/src/content/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { WalletAddress } from '@interledger/open-payments/dist/types'; - -export type MonetizationTag = HTMLLinkElement & { href?: string }; -export type MonetizationTagList = NodeListOf; - -export type MonetizationTagDetails = { - walletAddress: WalletAddress; - requestId: string; -}; From c9ab8170a9ee4bf3db8fc2f026cc12e604a30634 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:40:28 +0530 Subject: [PATCH 32/34] move checkHrefFormat into class also --- .../services/monetizationLinkManager.ts | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index a61f0ec0..7e7117fb 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -198,7 +198,7 @@ export class MonetizationLinkManager extends EventEmitter { ): Promise { const walletAddressUrl = link.href.trim(); try { - checkHrefFormat(walletAddressUrl); + this.checkHrefFormat(walletAddressUrl); const response = await this.message.send('GET_WALLET_ADDRESS_INFO', { walletAddressUrl, }); @@ -218,6 +218,34 @@ export class MonetizationLinkManager extends EventEmitter { } } + private checkHrefFormat(href: string): void { + let url: URL; + try { + url = new URL(href); + if (url.protocol !== 'https:') { + throw new WalletAddressFormatError( + `Wallet address URL must be specified as a fully resolved https:// url, ` + + `got ${JSON.stringify(href)} `, + ); + } + } catch (e) { + if (e instanceof WalletAddressFormatError) { + throw e; + } + throw new WalletAddressFormatError( + `Invalid wallet address URL: ${JSON.stringify(href)}`, + ); + } + + const { hash, search, port, username, password } = url; + + if (hash || search || port || username || password) { + throw new WalletAddressFormatError( + `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}`, + ); + } + } + private observeLinkAttrs(link: HTMLLinkElement) { this.monetizationLinkAttrObserver.observe(link, { childList: false, @@ -489,31 +517,3 @@ export class MonetizationLinkManager extends EventEmitter { return { requestId: details.requestId, intent: 'remove' }; } } - -function checkHrefFormat(href: string): void { - let url: URL; - try { - url = new URL(href); - if (url.protocol !== 'https:') { - throw new WalletAddressFormatError( - `Wallet address URL must be specified as a fully resolved https:// url, ` + - `got ${JSON.stringify(href)} `, - ); - } - } catch (e) { - if (e instanceof WalletAddressFormatError) { - throw e; - } - throw new WalletAddressFormatError( - `Invalid wallet address URL: ${JSON.stringify(href)}`, - ); - } - - const { hash, search, port, username, password } = url; - - if (hash || search || port || username || password) { - throw new WalletAddressFormatError( - `Wallet address URL must not contain query/fragment/port/username/password elements. Received: ${JSON.stringify({ hash, search, port, username, password })}`, - ); - } -} From 3cad011dcb117cd3c166a5a91fd7ac7a3fa14af5 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:24:15 +0530 Subject: [PATCH 33/34] fix iframe integration (tried method reuse too much) --- src/content/services/monetizationLinkManager.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 7e7117fb..3dce3302 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -108,6 +108,7 @@ export class MonetizationLinkManager extends EventEmitter { if (!this.isTopFrame && this.isFirstLevelFrame) { this.window.addEventListener('message', this.onWindowMessage); + this.postMessage('INITIALIZE_IFRAME', undefined); } this.document @@ -146,9 +147,9 @@ export class MonetizationLinkManager extends EventEmitter { switch (message) { case 'START_MONETIZATION': - return void this.sendStartMonetization(payload, true); + return void this.message.send('START_MONETIZATION', payload); case 'RESUME_MONETIZATION': - return void this.sendResumeMonetization(payload, true); + return void this.message.send('RESUME_MONETIZATION', payload); default: return; } From f2543d9f304c75dd309b756231b6e0089ea5f4ac Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:28:36 +0530 Subject: [PATCH 34/34] add resume payload length check --- src/content/services/monetizationLinkManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 3dce3302..4a4770bb 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -332,10 +332,10 @@ export class MonetizationLinkManager extends EventEmitter { payload: ResumeMonetizationPayload, onlyToTopIframe = false, ) { + if (!payload.length) return; + if (this.isTopFrame) { - if (payload.length) { - await this.message.send('RESUME_MONETIZATION', payload); - } + await this.message.send('RESUME_MONETIZATION', payload); } else if (this.isFirstLevelFrame && !onlyToTopIframe) { this.postMessage('IS_MONETIZATION_ALLOWED_ON_RESUME', payload); }