Skip to content

Commit

Permalink
feat: Start monetization iframe (#241)
Browse files Browse the repository at this point in the history
* Start monetization iframe

* add stop

* Iframe complete logic- start, resume, stop

* Solve conflicts

* fix build
---------

Co-authored-by: Diana Fulga <[email protected]>
  • Loading branch information
dianafulga and Diana Fulga authored May 17, 2024
1 parent 625761f commit 273e664
Show file tree
Hide file tree
Showing 7 changed files with 491 additions and 34 deletions.
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

0 comments on commit 273e664

Please sign in to comment.