Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Start monetization iframe #241

Merged
merged 8 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/content/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { createLogger, Logger } from '@/shared/logger'
import { ContentScript } from './services/contentScript'
import { MonetizationTagManager } from './services/monetizationTagManager'
import { LOG_LEVEL } from '@/shared/defines'
import { FrameManager } from './services/frameManager'

interface Cradle {
logger: Logger
browser: Browser
document: Document
window: Window
monetizationTagManager: MonetizationTagManager
frameManager: FrameManager
contentScript: ContentScript
}

Expand All @@ -24,6 +27,12 @@ export const configureContainer = () => {
logger: asValue(logger),
browser: asValue(browser),
document: asValue(document),
window: asValue(window),
frameManager: asClass(FrameManager)
.singleton()
.inject(() => ({
logger: logger.getLogger('content-script:frameManager')
})),
monetizationTagManager: asClass(MonetizationTagManager)
.singleton()
.inject(() => ({
Expand Down
10 changes: 10 additions & 0 deletions src/content/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum ContentToContentAction {
INITILIZE_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',
IS_FRAME_MONETIZED = 'IS_FRAME_MONETIZED'
}
19 changes: 16 additions & 3 deletions src/content/services/contentScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ import { MonetizationTagManager } from './monetizationTagManager'
import { type Browser } from 'webextension-polyfill'
import { BackgroundToContentAction, ToContentMessage } from '@/shared/messages'
import { failure } from '@/shared/helpers'
import { FrameManager } from './frameManager'

export class ContentScript {
private isFirstLevelFrame: boolean
private isTopFrame: boolean

constructor(
private browser: Browser,
private window: Window,
private logger: Logger,
private monetizationTagManager: MonetizationTagManager
private monetizationTagManager: MonetizationTagManager,
private frameManager: FrameManager
) {
this.isTopFrame = window === window.top
this.isFirstLevelFrame = window.parent === window.top

this.bindMessageHandler()
}

start() {
this.logger.info('Content script started')
if (this.isFirstLevelFrame) {
this.logger.info('Content script started')

if (this.isTopFrame) this.frameManager.start()

this.monetizationTagManager.start()
this.monetizationTagManager.start()
}
}

bindMessageHandler() {
Expand Down
289 changes: 289 additions & 0 deletions src/content/services/frameManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import { Logger } from '@/shared/logger'
import { isTabMonetized, stopMonetization } from '../lib/messages'
import { ContentToContentAction } from '../messages'

export class FrameManager {
private documentObserver: MutationObserver
private frameAllowAttrObserver: MutationObserver
private isFrameMonetized: boolean
private frames = new Map<
HTMLIFrameElement,
{ frameId: string | null; requestIds: string[]; isFrameMonetized?: boolean }
>()

constructor(
private window: Window,
private document: Document,
private logger: Logger
) {
this.documentObserver = new MutationObserver((records) =>
this.onWholeDocumentObserved(records)
)

this.frameAllowAttrObserver = new MutationObserver((records) =>
this.onFrameAllowAttrChange(records)
)
}

private findIframe(sourceWindow: Window): HTMLIFrameElement | null {
const iframes = this.frames.keys()
let frame

do {
frame = iframes.next()
if (frame.done) return null
if (frame.value.contentWindow === sourceWindow) return frame.value
} while (!frame.done)

return null
}

private observeDocumentForFrames() {
this.documentObserver.observe(this.document, {
subtree: true,
childList: true
})
}

private observeFrameAllowAttrs(frame: HTMLIFrameElement) {
this.frameAllowAttrObserver.observe(frame, {
childList: false,
attributeOldValue: true,
attributeFilter: ['allow']
})
}

async onFrameAllowAttrChange(records: MutationRecord[]) {
const handledTags = new Set<Node>()

// 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 HTMLIFrameElement
if (handledTags.has(target)) {
continue
}
const hasTarget = this.frames.has(target)
const typeSpecified =
target instanceof HTMLIFrameElement && target.allow === 'monetization'

if (!hasTarget && typeSpecified) {
await this.onAddedFrame(target)
handledTags.add(target)
} else if (hasTarget && !typeSpecified) {
this.onRemovedFrame(target)
handledTags.add(target)
} else if (!hasTarget && !typeSpecified) {
// ignore these changes
handledTags.add(target)
}
}
}

private async onAddedFrame(frame: HTMLIFrameElement) {
this.frames.set(frame, {
frameId: null,
requestIds: [],
isFrameMonetized: false
})
}

private async onRemovedFrame(frame: HTMLIFrameElement) {
this.logger.info('onRemovedFrame', frame)

const frameDetails = this.frames.get(frame)

frameDetails?.requestIds.forEach((requestId) =>
stopMonetization({ requestId })
)

this.frames.delete(frame)

let isMonetized = false

this.frames.forEach((value) => {
if (value.isFrameMonetized) isMonetized = true
})

isTabMonetized({ value: isMonetized || this.isFrameMonetized })
}

private onWholeDocumentObserved(records: MutationRecord[]) {
for (const record of records) {
if (record.type === 'childList') {
record.removedNodes.forEach((node) => this.check('removed', node))
}
}

for (const record of records) {
if (record.type === 'childList') {
record.addedNodes.forEach((node) => this.check('added', node))
}
}
}

async check(op: string, node: Node) {
if (node instanceof HTMLIFrameElement) {
if (op === 'added') {
this.observeFrameAllowAttrs(node)
await this.onAddedFrame(node)
} else if (op === 'removed' && this.frames.has(node)) {
this.onRemovedFrame(node)
}
}
}

start(): void {
this.bindMessageHandler()

if (
document.readyState === 'interactive' ||
document.readyState === 'complete'
)
this.run()

document.addEventListener(
'readystatechange',
() => {
if (document.readyState === 'interactive') {
this.run()
}
},
{ once: true }
)
}

private run() {
const frames: NodeListOf<HTMLIFrameElement> =
this.document.querySelectorAll('iframe')

frames.forEach(async (frame) => {
try {
this.observeFrameAllowAttrs(frame)
await this.onAddedFrame(frame)
} catch (e) {
this.logger.error(e)
}
})

this.observeDocumentForFrames()
}

private bindMessageHandler() {
this.window.addEventListener(
'message',
(event: any) => {
const { message, payload, id } = event.data
const frame = this.findIframe(event.source)

if (!frame) {
if (message === ContentToContentAction.IS_FRAME_MONETIZED) {
event.stopPropagation()

let isMonetized = false

this.isFrameMonetized = payload.isMonetized
this.frames.forEach((value) => {
if (value.isFrameMonetized) isMonetized = true
})

isTabMonetized({ value: isMonetized || this.isFrameMonetized })
}
return
}

if (event.origin === this.window.location.href) return

const frameDetails = this.frames.get(frame)

switch (message) {
case ContentToContentAction.INITILIZE_IFRAME:
event.stopPropagation()
this.frames.set(frame, {
frameId: id,
requestIds: [],
isFrameMonetized: false
})
return

case ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_START:
event.stopPropagation()
if (frame.allow === 'monetization') {
this.frames.set(frame, {
frameId: id,
requestIds: [payload.requestId],
isFrameMonetized: true
})

event.source.postMessage(
{
message: ContentToContentAction.START_MONETIZATION,
id,
payload
},
'*'
)
}

return

case ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_RESUME:
event.stopPropagation()
if (frame.allow === 'monetization') {
this.frames.set(frame, {
frameId: id,
requestIds: [payload.requestId],
isFrameMonetized: true
})

event.source.postMessage(
{
message: ContentToContentAction.RESUME_MONETIZATION,
id,
payload
},
'*'
)
}
return

case ContentToContentAction.IS_MONETIZATION_ALLOWED_ON_STOP:
event.stopPropagation()
if (frameDetails?.requestIds.length) {
event.source.postMessage(
{
message: ContentToContentAction.STOP_MONETIZATION,
id,
payload
},
'*'
)
}

return

case ContentToContentAction.IS_FRAME_MONETIZED: {
event.stopPropagation()
let isMonetized = false
if (!frameDetails) return

this.frames.set(frame, {
...frameDetails,
isFrameMonetized: payload.isMonetized
})
this.frames.forEach((value) => {
if (value.isFrameMonetized) isMonetized = true
})

isTabMonetized({ value: isMonetized || this.isFrameMonetized })

return
}
default:
return
}
},
{ capture: true }
)
}
}
Loading
Loading