-
-
Notifications
You must be signed in to change notification settings - Fork 44
fix: enforce first-party popup cap for ads #114
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
1c4ea14
a83dc82
6734b52
6c9ec6e
8c1dfa8
2e02ceb
91eba7f
85d891e
e37699d
e2b542b
b5a0607
beb5218
6edda36
90820bf
0cace6b
fe6d0a0
06177ee
19cca55
db4bfe8
94bf27c
92c9fda
f42c131
3e88a92
6644249
6b4ffbb
38697f0
031a706
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,155 @@ | ||
| import { default as randomWeightedChoice } from 'random-weighted-choice' | ||
|
|
||
| const AD_POPUP_CAP_DURATION_MS = 30 * 60 * 1000 | ||
| const AD_LAST_POPUP_AT_STORAGE_KEY = 'ads-last-popup-at' | ||
| const STACK_URL_REGEX = /https?:\/\/[^\s)]+/g | ||
|
|
||
| type WindowOpenArgs = Parameters<Window['open']> | ||
| type WindowOpenResult = ReturnType<Window['open']> | ||
|
|
||
| function getScriptUrlsFromStack(stack: string): URL[] { | ||
| const matches = stack.match(STACK_URL_REGEX) | ||
|
|
||
| if (!matches) { | ||
| return [] | ||
| } | ||
|
|
||
| const urls: URL[] = [] | ||
|
|
||
| for (const rawMatch of matches) { | ||
| const normalizedUrl = rawMatch | ||
| .replace(/[),]$/, '') | ||
| .replace(/:\d+:\d+$/, '') | ||
|
|
||
| try { | ||
| urls.push(new URL(normalizedUrl)) | ||
| } catch { | ||
| // Ignore malformed URLs from stack traces | ||
| } | ||
| } | ||
|
|
||
| return urls | ||
| } | ||
|
|
||
| function isLikelyVendorPopupCall(stack: string | undefined, hasUserActivation: boolean): boolean { | ||
| if (!stack) { | ||
| return !hasUserActivation | ||
| } | ||
|
|
||
| const callerScriptUrls = getScriptUrlsFromStack(stack) | ||
|
|
||
| if (callerScriptUrls.length === 0) { | ||
| return !hasUserActivation | ||
| } | ||
|
|
||
| const currentOrigin = window.location.origin | ||
|
|
||
| for (const scriptUrl of callerScriptUrls) { | ||
| if (scriptUrl.origin !== currentOrigin) { | ||
| return true | ||
| } | ||
|
|
||
| // Keep the heuristic broad: treat same-origin static /js scripts as likely ad/vendor callers. | ||
| if (scriptUrl.pathname.startsWith('/js/')) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return !hasUserActivation | ||
| } | ||
|
|
||
| export default function () { | ||
| const popunderScript = useState<string>('popunder-script', () => '') | ||
| const pushScript = useState<string>('push-notification-script', () => '') | ||
| const isPopupGuardInstalled = useState<boolean>('ads-popup-guard-installed', () => false) | ||
| const isPopupGuardArmed = useState<boolean>('ads-popup-guard-armed', () => false) | ||
| const lastAdPopupAtInMemory = useState<number | null>('ads-last-popup-at-in-memory', () => null) | ||
|
|
||
| if (!import.meta.client) { | ||
| return | ||
| } | ||
|
|
||
| function getLastAdPopupAt(): number | null { | ||
| if (lastAdPopupAtInMemory.value !== null) { | ||
| return lastAdPopupAtInMemory.value | ||
| } | ||
|
|
||
| try { | ||
| const rawLastPopupAt = window.localStorage.getItem(AD_LAST_POPUP_AT_STORAGE_KEY) | ||
|
|
||
| if (rawLastPopupAt) { | ||
| const parsedLastPopupAt = Number.parseInt(rawLastPopupAt, 10) | ||
|
|
||
| if (Number.isFinite(parsedLastPopupAt) && parsedLastPopupAt > 0) { | ||
| lastAdPopupAtInMemory.value = parsedLastPopupAt | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| return parsedLastPopupAt | ||
| } | ||
| } | ||
| } catch { | ||
| // Ignore storage failures and use in-memory fallback | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| function recordAdPopupOpened(at = Date.now()) { | ||
| lastAdPopupAtInMemory.value = at | ||
|
|
||
| try { | ||
| window.localStorage.setItem(AD_LAST_POPUP_AT_STORAGE_KEY, String(at)) | ||
| } catch { | ||
| // Ignore storage failures and keep the in-memory fallback | ||
| } | ||
| } | ||
|
|
||
| function isAdPopupCapActive(): boolean { | ||
| const lastAdPopupAt = getLastAdPopupAt() | ||
|
|
||
| if (!lastAdPopupAt) { | ||
| return false | ||
| } | ||
|
|
||
| return Date.now() - lastAdPopupAt < AD_POPUP_CAP_DURATION_MS | ||
| } | ||
|
|
||
| if (!isPopupGuardInstalled.value) { | ||
| const originalWindowOpen = window.open.bind(window) | ||
|
|
||
| window.open = (...args: WindowOpenArgs): WindowOpenResult => { | ||
| if (!isPopupGuardArmed.value) { | ||
| return originalWindowOpen(...args) | ||
| } | ||
|
|
||
| const userActivation = (window.navigator as Navigator & { | ||
| userActivation?: { isActive: boolean } | ||
| }).userActivation | ||
|
|
||
| const hasUserActivation = userActivation?.isActive ?? true | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| const stack = new Error().stack | ||
|
|
||
| if (!isLikelyVendorPopupCall(stack, hasUserActivation)) { | ||
| return originalWindowOpen(...args) | ||
| } | ||
|
|
||
| if (isAdPopupCapActive()) { | ||
| return null | ||
| } | ||
|
|
||
| recordAdPopupOpened() | ||
|
|
||
| return originalWindowOpen(...args) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check git history for recordAdPopupOpened ordering changes
git log --oneline -10 -- composables/useAdvertisements.ts
git show 6c9ec6e --stat 2>/dev/null || echo "Commit 6c9ec6e not found in history"Repository: Rule-34/App Length of output: 433 🏁 Script executed: # Show the actual current content at lines 145-151
sed -n '145,151p' composables/useAdvertisements.ts
# Show the diff from commit 6c9ec6e to see what it changed
git show 6c9ec6e -- composables/useAdvertisements.ts | head -200Repository: Rule-34/App Length of output: 5555 🏁 Script executed: # Get more context around the actual problematic code pattern
rg -A 10 -B 5 "recordAdPopupOpened" composables/useAdvertisements.ts
# Also check the exact content to understand if code was reverted
git log --oneline --all -- composables/useAdvertisements.ts | head -20Repository: Rule-34/App Length of output: 1493 🏁 Script executed: # Show what the most recent commit changed
git show 90820bf -- composables/useAdvertisements.ts
# Get line numbers for the problematic pattern
rg -n "recordAdPopupOpened()" composables/useAdvertisements.tsRepository: Rule-34/App Length of output: 10223 🏁 Script executed: # Get exact line numbers for the problematic pattern
rg -n "if \(isAdPopupCapActive" composables/useAdvertisements.ts -A 10Repository: Rule-34/App Length of output: 646 Record popup cap only after confirming popup opens successfully. At line 166, Proposed fix: record only after successful open if (isAdPopupCapActive()) {
logAdPopupGuard('block-capped-popunder')
return null
}
- recordAdPopupOpened()
+ const popupHandle = originalWindowOpen(...args)
- logAdPopupGuard('allow-popunder', {
- cappedUntil: Date.now() + AD_POPUP_CAP_DURATION_MS
- })
+ if (popupHandle) {
+ recordAdPopupOpened()
+
+ logAdPopupGuard('allow-popunder', {
+ cappedUntil: Date.now() + AD_POPUP_CAP_DURATION_MS
+ })
+ }
- return originalWindowOpen(...args)
+ return popupHandle🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| isPopupGuardInstalled.value = true | ||
| } | ||
|
|
||
| // Phase 1: stop injecting ad scripts while the 30-minute popup cap is active. | ||
| if (isAdPopupCapActive()) { | ||
| return | ||
| } | ||
|
|
||
| // Phase 2 (arming): once scripts load, guard vendor-like popups with the first-party cap. | ||
| isPopupGuardArmed.value = true | ||
|
AlejandroAkbal marked this conversation as resolved.
|
||
|
|
||
| const popunderAds = [ | ||
| /** | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.