` at runtime inside `boot()` and pass the **element reference** (not the string) to `Phaser.Game({ parent })`. The cascading `ScaleManager.resize → Cannot set properties of undefined (setting 'width')` errors all trace back to `game.canvas` never being created when either of these fail. |
| NineSlice won't shrink in landscape | Phaser's `NineSlice` silently clamps its minimum dimensions to `(leftWidth + rightWidth) × (topHeight + bottomHeight)` **in source pixels**. Symptom: one UI chrome element stays huge in landscape while every other `sd()`-driven element rescales correctly — because the requested `sd(H)` is below the inset total. Shrinking the insets fixes the scale clamp but stretches the art's rounded corners. The right fix: **operate the NineSlice in source-pixel space and scale it uniformly with `setScale(sd(1))`**. Keep `setSize(wSrc, hSrc)` in source px so the center stretches horizontally with text growth while corners stay intact; the `sd(1)` scale is what makes the whole object shrink in landscape, bypassing the min-dim clamp entirely. When reading `this.preview.width` (which is in scaled px) to decide `wSrc`, divide by `sd(1)` to convert back to source space. Tween callers that reset `setScale(1)` also need updating — the bg's base scale is now `sd(1)`, not 1. See [SelectionLine.ts](src/game/SelectionLine.ts) for the pattern. |
| Unity (un) submission: "not allowed to use window.top" | Luna's static scan flags the literal `window.top`, which Phaser 3 ships internally. In `build-all.mjs`, rewrite `window.top` → `window.self` for the `un` HTML only (other networks keep the original). Safe inside the playable's own frame because the iframe-vs-top check collapses to always-top. |
+| MRAID network missing `mraid.js` | MRAID 3.0 creatives must request `mraid.js` early. Inject `` exactly once for Applovin (`al`), Ironsource (`is`), and Unity (`un`), even if the container may also expose runtime MRAID objects. |
+| MRAID listener wired too late | Previewers can probe MRAID before any Phaser scene is ready — if listeners are wired only inside `bindLifecycle` / scene `create()`, the creative can miss `ready`, `exposureChange`, or `viewableChange`. Fix: call `initMraid()` at `networks.ts` module-load time, guarded by `getState() === 'loading'` → `addEventListener('ready', setupMraid)` vs immediate call. Cache `_mraidViewable` / `_mraidExposed`; in `bindLifecycle` apply cached hidden/non-exposed state immediately. |
+| MRAID 3.0 `exposureChange` not handled | MRAID 3.0 containers dispatch `exposureChange` with `exposedPercentage` directly instead of — or in addition to — `viewableChange`. If only listening to `viewableChange`, the ad may never receive pause signals on newer AL/IS/Unity builds. Register both: `mraid.addEventListener('exposureChange', exposedPercentage => onExposure(exposedPercentage > 0))` and `mraid.addEventListener('viewableChange', onViewable)` inside `initMraid()`. Seed initial state from `mraid.isViewable()` on ready. |
+| MRAID 3.0 `audioVolumeChange` not handled | MRAID 3.0 containers fire `audioVolumeChange` with a volume level (0–100) or `null` when unavailable. Without a listener, the ad ignores host-level mute/unmute signals; if `null` is divided by 100, the ad can mute accidentally. Register `mraid.addEventListener('audioVolumeChange', v => { if (typeof v === 'number') scene.sound.setVolume(v / 100) })` in `initMraid()`. |
+| MRAID `error` listener missing | Validators expect failed or unsupported MRAID calls to be observable. Register `mraid.addEventListener('error', (message, action) => console.warn('[MRAID error]', { message, action }))` during early MRAID setup. |
+| MRAID `stateChange` listener missing | If the creative uses state-changing container behavior such as `mraid.close()`, `expand`, or `resize`, validators expect a `stateChange` listener. Register `mraid.addEventListener('stateChange', state => console.log('[MRAID stateChange]', state))` with the other early MRAID callbacks. |
+| Custom close control not wired to MRAID | If the creative renders its own close button, the button must call a guarded close helper: `notifyGameClose()` then `mraid.close()` when `typeof mraid.close === 'function'` and `mraid.getState() !== 'loading'`. Do not use CTA/install buttons as close controls. |
+| MRAID init not awaited before Phaser boot | If `new Phaser.Game()` is created before `initMraid()` resolves, the scene may fire `create()` before MRAID state is known — `bindLifecycle` then cannot apply a cached hidden/paused state. Make `boot()` async and `await initMraid()` before constructing the Phaser game. `initMraid()` must self-resolve after a timeout (~2 s) so a missing or non-firing MRAID container never blocks startup. |
+| Late MRAID injection not handled | Some containers inject `window.mraid` after the script executes — a synchronous `typeof window.mraid !== 'undefined'` check at module load returns false even though MRAID will be present. After the immediate check fails, poll briefly (~500 ms) before concluding no-MRAID, then proceed. |
+| Unsafe optional MRAID API calls | `useCustomClose()` was removed in MRAID 3.0, and APIs such as `expand`, `resize`, `storePicture`, and `createCalendarEvent` are optional/capability-dependent. Do not call them unless the project intentionally supports the feature and guards every call with `typeof mraid.method === 'function'` / `mraid.supports(...)` where relevant. |
## Ironsource — Runtime Analysis
Ironsource requires every `_is.html` build to pass **LevelPlay → Creative Management → Playable Workshop** (Runtime Analysis / Playable Validator) before launch. Skipping this can get the ad disabled. The validator runs in an iOS-like WKWebView sandbox and exposes console errors — use it as a free diagnostic channel for iPhone-only failures when a Mac + Safari remote-debug isn't available.
+
+## Moloco — Upload Checklist & CTA Implementation
+Creative specs: HTML5 (`.html` / `.htm`), < 5 MB, portrait + landscape.
+
+Before uploading to Moloco Ads, confirm all three:
+- [ ] No `XMLHttpRequest` anywhere in the ad (Moloco rejects it outright)
+- [ ] CTA calls `FbPlayableAd.onCTAClick()` with no parameters — Moloco uses this for store redirect (not `window.clickTag`)
+- [ ] No JavaScript redirects in ad file assets
+
+### CTA implementation options
+
+**Option 1 — event listener**
+```html
+
+
+```
+
+**Option 2 — inline onClick**
+```html
+
+```
+
+Moloco's validator requires `FbPlayableAd.onCTAClick()` — this is why it shares priority #2 in the CTA chain with Facebook. `window.clickTag` is kept as a fallback at priority #7.
+
+***EOF***
+ [05/28/2026]
diff --git a/aiAssets/MRAID-3.0-Best-Practices-Guide.pdf b/aiAssets/MRAID-3.0-Best-Practices-Guide.pdf
new file mode 100644
index 0000000..c94aab1
Binary files /dev/null and b/aiAssets/MRAID-3.0-Best-Practices-Guide.pdf differ
diff --git a/aiAssets/MRAID_3.0_FINAL_June_2018.pdf b/aiAssets/MRAID_3.0_FINAL_June_2018.pdf
new file mode 100644
index 0000000..16d184a
Binary files /dev/null and b/aiAssets/MRAID_3.0_FINAL_June_2018.pdf differ
diff --git a/dist/hpl-toolbox-0.1.6.exe b/dist/hpl-toolbox-0.1.6.exe
index 4648022..95592fd 100644
Binary files a/dist/hpl-toolbox-0.1.6.exe and b/dist/hpl-toolbox-0.1.6.exe differ
diff --git a/dist/hpl-toolbox-0.1.6.vsix b/dist/hpl-toolbox-0.1.6.vsix
index 94ecdaa..72c896b 100644
Binary files a/dist/hpl-toolbox-0.1.6.vsix and b/dist/hpl-toolbox-0.1.6.vsix differ
diff --git a/src/tools/mraidChecker.ts b/src/tools/mraidChecker.ts
index a29480b..14b3a0b 100644
--- a/src/tools/mraidChecker.ts
+++ b/src/tools/mraidChecker.ts
@@ -32,6 +32,7 @@ interface MraidRule {
}
interface ScanContext {
+ filePath: string;
html: string;
scriptText: string;
lowerHtml: string;
@@ -118,7 +119,7 @@ export function openMraidChecker(_context: vscode.ExtensionContext) {
for (const file of targets) {
try {
const content = fs.readFileSync(file, 'utf8');
- const issues = checkMraid(content);
+ const issues = checkMraid(content, file);
const display = baseDir ? path.relative(baseDir, file) || path.basename(file) : file;
results.push({ file: display, ok: issues.length === 0, issues });
} catch {
@@ -164,9 +165,10 @@ async function collectHtmlFiles(root: string): Promise
{
return out;
}
-function checkMraid(html: string): MraidIssue[] {
+function checkMraid(html: string, filePath = ''): MraidIssue[] {
const scriptText = extractScriptText(html);
const ctx: ScanContext = {
+ filePath,
html,
scriptText,
lowerHtml: html.toLowerCase(),
@@ -226,6 +228,14 @@ function hasMraidReference(ctx: ScanContext): boolean {
return /\bmraid\b/i.test(ctx.html);
}
+function countMraidJsReferences(ctx: ScanContext): number {
+ return (ctx.html.match(/\bmraid\.js\b/gi) || []).length;
+}
+
+function hasViewportMeta(ctx: ScanContext): boolean {
+ return /]*\bname\s*=\s*['"]viewport['"][^>]*>/i.test(ctx.html);
+}
+
function hasMraidCall(ctx: ScanContext, method: string): boolean {
return ctx.mraidCallLines.has(method);
}
@@ -253,6 +263,77 @@ function hasReadyGate(ctx: ScanContext): boolean {
);
}
+function hasDomReadyGate(ctx: ScanContext): boolean {
+ return (
+ /document\s*\.\s*readyState/i.test(ctx.scriptText) ||
+ /DOMContentLoaded/i.test(ctx.scriptText) ||
+ /\bwindow\s*\.\s*(addEventListener\s*\(\s*['"]load['"]|onload\b)/i.test(ctx.scriptText)
+ );
+}
+
+function hasReadyListenerAndStateFallback(ctx: ScanContext): boolean {
+ return hasEventListener(ctx, 'ready') && hasMraidCall(ctx, 'getState');
+}
+
+function hasMraidTypeofGuard(ctx: ScanContext): boolean {
+ return /typeof\s+(?:window\s*\.\s*)?mraid\s*!={1,2}\s*['"]undefined['"]/i.test(ctx.scriptText);
+}
+
+function hasMraidFunctionGuard(ctx: ScanContext, method: string): boolean {
+ const escaped = method.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ return new RegExp(`typeof\\s+(?:window\\s*\\.\\s*)?mraid\\s*\\.\\s*${escaped}\\s*={2,3}\\s*['"]function['"]|typeof\\s+(?:window\\s*\\.\\s*)?mraid\\s*\\.\\s*${escaped}\\s*!={1,2}\\s*['"]undefined['"]`, 'i').test(ctx.scriptText);
+}
+
+function hasMraidReadyTimeout(ctx: ScanContext): boolean {
+ return /setTimeout\s*\(/i.test(ctx.scriptText) && /\bmraid\b/i.test(ctx.scriptText);
+}
+
+function hasLateMraidDetection(ctx: ScanContext): boolean {
+ return /setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\(/i.test(ctx.scriptText) && /window\s*\.\s*mraid|\bmraid\b/i.test(ctx.scriptText);
+}
+
+function hasMraidOpenGuard(ctx: ScanContext): boolean {
+ return (
+ /typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*open\s*={2,3}\s*['"]function['"]/i.test(ctx.scriptText) ||
+ /typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*open\s*!={1,2}\s*['"]undefined['"]/i.test(ctx.scriptText)
+ );
+}
+
+function hasMraidOpenLoadingGuard(ctx: ScanContext): boolean {
+ return (
+ /mraid\s*\.\s*getState\s*\(\s*\)\s*!={1,2}\s*['"]loading['"]/i.test(ctx.scriptText) ||
+ /['"]loading['"]\s*!={1,2}\s*mraid\s*\.\s*getState\s*\(\s*\)/i.test(ctx.scriptText) ||
+ /typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*getState\s*!={1,2}\s*['"]function['"]/i.test(ctx.scriptText) ||
+ (hasMraidCall(ctx, 'getState') && /\b\w+\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*\w+\b/i.test(ctx.scriptText)) ||
+ (hasMraidCall(ctx, 'getState') && /\b\w+\s*={2,3}\s*['"]loading['"][\s\S]{0,500}\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText))
+ );
+}
+
+function hasAudioVolumeNullSafeHandling(ctx: ScanContext): boolean {
+ return hasEventListener(ctx, 'audioVolumeChange') && (/typeof\s+\w+\s*={2,3}\s*['"]number['"]/i.test(ctx.scriptText) || /!=\s*null|!==\s*null/i.test(ctx.scriptText));
+}
+
+function hasLifecycleSignal(ctx: ScanContext, name: string): boolean {
+ return new RegExp(`\\b${name}\\b`, 'i').test(ctx.html);
+}
+
+function hasUnsupportedHyperlink(ctx: ScanContext): boolean {
+ return /]*\bhref\s*=/i.test(ctx.html);
+}
+
+function hasTwoPartExpand(ctx: ScanContext): boolean {
+ return /mraid\s*\.\s*expand\s*\(\s*[^)\s]/i.test(ctx.scriptText);
+}
+
+function opensVideoUrl(ctx: ScanContext): boolean {
+ return /mraid\s*\.\s*open\s*\(\s*[^)]*\.(mp4|m4v|mov|webm|avi|m3u8)(?:['"`?#)]|$)/i.test(ctx.scriptText);
+}
+
+function isInMraidNetworkPath(ctx: ScanContext): boolean {
+ if (!ctx.filePath) return true;
+ return isInSupportedMraidFolderPath(ctx.filePath) || /_(al|is|un)\.html?$/i.test(path.basename(ctx.filePath));
+}
+
function hasEventListener(ctx: ScanContext, eventName: string): boolean {
const pattern = new RegExp(`mraid\\s*\\.\\s*addEventListener\\s*\\(\\s*['"]${eventName}['"]`, 'i');
return pattern.test(ctx.scriptText);
@@ -264,15 +345,32 @@ function hasSupportCheck(ctx: ScanContext, feature: string): boolean {
}
function usesDirectBrowserNavigation(ctx: ScanContext): boolean {
- return (
- /\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText) ||
- /\blocation\s*\.\s*(href|assign|replace)\b/i.test(ctx.scriptText)
- );
+ return /\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText);
}
-const MRAID_REFERENCE = 'MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/';
+function usesLocationNavigation(ctx: ScanContext): boolean {
+ return /\blocation\s*\.\s*(href|assign|replace)\b/i.test(ctx.scriptText);
+}
+
+const MRAID_REFERENCE = 'GUIDE.md, MRAID 3.0 specification, and MRAID 3.0 Best Practices Guide.';
const MRAID_RULES: MraidRule[] = [
+ {
+ id: 'mraid-js-missing',
+ severity: 'requirement',
+ title: 'mraid.js reference not found',
+ detail: 'MRAID creatives for AppLovin, ironSource, and Unity must request mraid.js early in the generated HTML.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => isInMraidNetworkPath(ctx) && countMraidJsReferences(ctx) === 0 ? issue(MRAID_RULES_BY_ID['mraid-js-missing']) : null,
+ },
+ {
+ id: 'mraid-js-duplicate',
+ severity: 'requirement',
+ title: 'Multiple mraid.js references found',
+ detail: 'Request mraid.js exactly once; duplicate references can inject multiple MRAID libraries and slow or destabilize the ad.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => countMraidJsReferences(ctx) > 1 ? issue(MRAID_RULES_BY_ID['mraid-js-duplicate']) : null,
+ },
{
id: 'mraid-reference',
severity: 'requirement',
@@ -281,6 +379,30 @@ const MRAID_RULES: MraidRule[] = [
reference: MRAID_REFERENCE,
test: (ctx) => hasMraidReference(ctx) ? null : issue(MRAID_RULES_BY_ID['mraid-reference']),
},
+ {
+ id: 'viewport-meta',
+ severity: 'best-practice',
+ title: 'Viewport meta tag not found',
+ detail: 'MRAID creatives should include a mobile viewport meta tag so the ad scales predictably on mobile WebViews.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasViewportMeta(ctx) ? null : issue(MRAID_RULES_BY_ID['viewport-meta']),
+ },
+ {
+ id: 'mraid-typeof-guard',
+ severity: 'requirement',
+ title: 'MRAID object is not guarded',
+ detail: 'Guard MRAID access with typeof mraid !== "undefined" or an equivalent window.mraid guard before making MRAID calls.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidReference(ctx) && !hasMraidTypeofGuard(ctx) ? issue(MRAID_RULES_BY_ID['mraid-typeof-guard']) : null,
+ },
+ {
+ id: 'dom-ready-gate',
+ severity: 'requirement',
+ title: 'DOM readiness is not guarded',
+ detail: 'MRAID startup should wait for DOM readiness as well as MRAID readiness before building or binding rich media content.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => !hasMraidReference(ctx) || hasDomReadyGate(ctx) ? null : issue(MRAID_RULES_BY_ID['dom-ready-gate']),
+ },
{
id: 'ready-gate',
severity: 'requirement',
@@ -289,6 +411,30 @@ const MRAID_RULES: MraidRule[] = [
reference: MRAID_REFERENCE,
test: (ctx) => !hasMraidReference(ctx) || hasReadyGate(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-gate']),
},
+ {
+ id: 'ready-state-fallback',
+ severity: 'requirement',
+ title: 'MRAID ready listener/state fallback pair not found',
+ detail: 'Use both mraid.addEventListener("ready", ...) and mraid.getState() so startup works when ready fires before the listener attaches.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => !hasMraidReference(ctx) || hasReadyListenerAndStateFallback(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-state-fallback']),
+ },
+ {
+ id: 'ready-timeout-fallback',
+ severity: 'best-practice',
+ title: 'MRAID ready timeout fallback not found',
+ detail: 'A timeout fallback prevents startup hangs when a container never fires ready.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => !hasMraidReference(ctx) || hasMraidReadyTimeout(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-timeout-fallback']),
+ },
+ {
+ id: 'late-mraid-detection',
+ severity: 'best-practice',
+ title: 'Late MRAID injection detection not found',
+ detail: 'Poll or retry briefly for late window.mraid injection before deciding the creative is running without MRAID.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => !hasMraidReference(ctx) || hasLateMraidDetection(ctx) ? null : issue(MRAID_RULES_BY_ID['late-mraid-detection']),
+ },
{
id: 'error-listener',
severity: 'best-practice',
@@ -300,21 +446,45 @@ const MRAID_RULES: MraidRule[] = [
{
id: 'viewability',
severity: 'best-practice',
- title: 'Viewability handling not found',
- detail: 'Use mraid.isViewable() and/or viewableChange before starting animation, audio, video, or timers.',
+ title: 'MRAID viewability fallback chain not found',
+ detail: 'Use exposureChange for MRAID 3.0 and keep viewableChange or isViewable() as MRAID 2.0 fallback before starting animation, audio, video, or timers.',
reference: MRAID_REFERENCE,
test: (ctx) => {
- const hasVisibilitySignal = hasMraidCall(ctx, 'isViewable') || hasEventListener(ctx, 'viewableChange') || hasEventListener(ctx, 'exposureChange');
+ const hasVisibilitySignal = hasEventListener(ctx, 'exposureChange') && (hasMraidCall(ctx, 'isViewable') || hasEventListener(ctx, 'viewableChange'));
return !hasMraidReference(ctx) || hasVisibilitySignal ? null : issue(MRAID_RULES_BY_ID['viewability']);
},
},
+ {
+ id: 'audio-volume-change',
+ severity: 'best-practice',
+ title: 'audioVolumeChange handling not found',
+ detail: 'Listen for audioVolumeChange and ignore null values; only apply volume math when the value is numeric.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => !hasMraidReference(ctx) || hasAudioVolumeNullSafeHandling(ctx) ? null : issue(MRAID_RULES_BY_ID['audio-volume-change']),
+ },
{
id: 'open-api',
severity: 'requirement',
title: 'Browser navigation used instead of mraid.open',
- detail: 'Use mraid.open(url) for click-through navigation inside an MRAID ad container.',
+ detail: 'Use mraid.open(url) for click-through navigation inside an MRAID ad container; avoid hyperlinks, location redirects, and unguarded browser navigation.',
reference: MRAID_REFERENCE,
- test: (ctx) => usesDirectBrowserNavigation(ctx) && !hasMraidCall(ctx, 'open') ? issue(MRAID_RULES_BY_ID['open-api']) : null,
+ test: (ctx) => hasUnsupportedHyperlink(ctx) || usesLocationNavigation(ctx) || (usesDirectBrowserNavigation(ctx) && !hasMraidCall(ctx, 'open')) ? issue(MRAID_RULES_BY_ID['open-api']) : null,
+ },
+ {
+ id: 'open-guarded-fallback',
+ severity: 'requirement',
+ title: 'mraid.open guarded fallback not found',
+ detail: 'Guard mraid.open with function/state checks and provide a window.open fallback when MRAID is unavailable or still loading.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidCall(ctx, 'open') && (!hasMraidOpenGuard(ctx) || !hasMraidOpenLoadingGuard(ctx) || !usesDirectBrowserNavigation(ctx)) ? issue(MRAID_RULES_BY_ID['open-guarded-fallback'], mraidLine(ctx, 'open')) : null,
+ },
+ {
+ id: 'open-video-url',
+ severity: 'best-practice',
+ title: 'mraid.open appears to target video media',
+ detail: 'Use mraid.playVideo(url) for native video playback cases instead of mraid.open(videoUrl).',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => opensVideoUrl(ctx) && !hasMraidCall(ctx, 'playVideo') ? issue(MRAID_RULES_BY_ID['open-video-url'], mraidLine(ctx, 'open')) : null,
},
{
id: 'close-api',
@@ -327,29 +497,93 @@ const MRAID_RULES: MraidRule[] = [
return appearsToHaveCloseUi && !hasMraidCall(ctx, 'close') ? issue(MRAID_RULES_BY_ID['close-api']) : null;
},
},
+ {
+ id: 'unload-fallback',
+ severity: 'best-practice',
+ title: 'No mraid.unload graceful failure path found',
+ detail: 'Use mraid.unload() when the ad cannot run or refuses to show, instead of leaving a broken or blank creative.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidReference(ctx) && !hasMraidCall(ctx, 'unload') ? issue(MRAID_RULES_BY_ID['unload-fallback']) : null,
+ },
+ {
+ id: 'resize-properties',
+ severity: 'requirement',
+ title: 'mraid.resize used without setResizeProperties',
+ detail: 'Call mraid.setResizeProperties(...) before mraid.resize(); otherwise the container should emit an error.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidCall(ctx, 'resize') && !hasMraidCall(ctx, 'setResizeProperties') ? issue(MRAID_RULES_BY_ID['resize-properties'], mraidLine(ctx, 'resize')) : null,
+ },
+ {
+ id: 'use-custom-close',
+ severity: 'best-practice',
+ title: 'Deprecated useCustomClose found',
+ detail: 'MRAID 3.0 ignores useCustomClose; the host provides the mandatory close control.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidCall(ctx, 'useCustomClose') || /useCustomClose/i.test(ctx.scriptText) ? issue(MRAID_RULES_BY_ID['use-custom-close'], mraidLine(ctx, 'useCustomClose')) : null,
+ },
+ {
+ id: 'two-part-expand',
+ severity: 'best-practice',
+ title: 'Two-part expand appears to be used',
+ detail: 'Two-part expandable ads are deprecated in MRAID 3.0; use self-contained one-part expandables.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasTwoPartExpand(ctx) ? issue(MRAID_RULES_BY_ID['two-part-expand'], mraidLine(ctx, 'expand')) : null,
+ },
{
id: 'expand-support',
severity: 'requirement',
- title: 'mraid.expand used without supports check',
- detail: 'Check mraid.supports("expand") before using expand behavior.',
+ title: 'mraid.expand used without function guard',
+ detail: 'Guard mraid.expand with a function check before using expand behavior.',
reference: MRAID_REFERENCE,
- test: (ctx) => hasMraidCall(ctx, 'expand') && !hasSupportCheck(ctx, 'expand') ? issue(MRAID_RULES_BY_ID['expand-support'], mraidLine(ctx, 'expand')) : null,
+ test: (ctx) => hasMraidCall(ctx, 'expand') && !hasMraidFunctionGuard(ctx, 'expand') ? issue(MRAID_RULES_BY_ID['expand-support'], mraidLine(ctx, 'expand')) : null,
},
{
id: 'resize-support',
severity: 'requirement',
- title: 'mraid.resize used without supports check',
- detail: 'Check mraid.supports("resize") before using resize behavior.',
+ title: 'mraid.resize used without function guard',
+ detail: 'Guard mraid.resize with a function check before using resize behavior.',
reference: MRAID_REFERENCE,
- test: (ctx) => hasMraidCall(ctx, 'resize') && !hasSupportCheck(ctx, 'resize') ? issue(MRAID_RULES_BY_ID['resize-support'], mraidLine(ctx, 'resize')) : null,
+ test: (ctx) => hasMraidCall(ctx, 'resize') && !hasMraidFunctionGuard(ctx, 'resize') ? issue(MRAID_RULES_BY_ID['resize-support'], mraidLine(ctx, 'resize')) : null,
+ },
+ {
+ id: 'store-picture-support',
+ severity: 'requirement',
+ title: 'storePicture used without supports check',
+ detail: 'Call mraid.supports("storePicture") before mraid.storePicture(...).',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidCall(ctx, 'storePicture') && !hasSupportCheck(ctx, 'storePicture') ? issue(MRAID_RULES_BY_ID['store-picture-support'], mraidLine(ctx, 'storePicture')) : null,
+ },
+ {
+ id: 'calendar-support',
+ severity: 'requirement',
+ title: 'createCalendarEvent used without supports check',
+ detail: 'Call mraid.supports("calendar") before mraid.createCalendarEvent(...).',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidCall(ctx, 'createCalendarEvent') && !hasSupportCheck(ctx, 'calendar') ? issue(MRAID_RULES_BY_ID['calendar-support'], mraidLine(ctx, 'createCalendarEvent')) : null,
+ },
+ {
+ id: 'location-support',
+ severity: 'requirement',
+ title: 'getLocation used without supports check',
+ detail: 'Call mraid.supports("location") before mraid.getLocation(). Do not use HTML5 geolocation as a substitute in MRAID.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidCall(ctx, 'getLocation') && !hasSupportCheck(ctx, 'location') ? issue(MRAID_RULES_BY_ID['location-support'], mraidLine(ctx, 'getLocation')) : null,
+ },
+ {
+ id: 'vpaid-support',
+ severity: 'requirement',
+ title: 'VPAID integration used without supports check',
+ detail: 'Call mraid.supports("vpaid") before mraid.initVpaid(...).',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => hasMraidCall(ctx, 'initVpaid') && !hasSupportCheck(ctx, 'vpaid') ? issue(MRAID_RULES_BY_ID['vpaid-support'], mraidLine(ctx, 'initVpaid')) : null,
},
{
id: 'orientation-support',
severity: 'best-practice',
- title: 'Orientation properties set without supports check',
- detail: 'Check mraid.supports("orientation") before requiring orientation behavior.',
+ title: 'Orientation properties set without function guard',
+ detail: 'Guard mraid.setOrientationProperties with a function check before requiring orientation behavior.',
reference: MRAID_REFERENCE,
- test: (ctx) => hasMraidCall(ctx, 'setOrientationProperties') && !hasSupportCheck(ctx, 'orientation') ? issue(MRAID_RULES_BY_ID['orientation-support'], mraidLine(ctx, 'setOrientationProperties')) : null,
+ test: (ctx) => hasMraidCall(ctx, 'setOrientationProperties') && !hasMraidFunctionGuard(ctx, 'setOrientationProperties') ? issue(MRAID_RULES_BY_ID['orientation-support'], mraidLine(ctx, 'setOrientationProperties')) : null,
},
{
id: 'size-change',
@@ -373,6 +607,35 @@ const MRAID_RULES: MraidRule[] = [
return changesState && !hasEventListener(ctx, 'stateChange') ? issue(MRAID_RULES_BY_ID['state-change']) : null;
},
},
+ {
+ id: 'output-script-hygiene',
+ severity: 'requirement',
+ title: 'Rejected script attributes found',
+ detail: 'Playable output should not contain type="module" or crossorigin on script tags; ad networks can reject ES modules/CORS attributes.',
+ reference: MRAID_REFERENCE,
+ test: (ctx) => /`)
- mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`)
- mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`)
- mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`)
- mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`)
- mraidCloseUiRx = regexp.MustCompile(`(?i)\b(close|dismiss|skip)\b`)
- mraidLoadingLeftRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*={0,2}\s*['"]loading['"]`)
- mraidLoadingRightRx = regexp.MustCompile(`(?i)['"]loading['"]\s*={0,2}\s*mraid\s*\.\s*getState\s*\(\s*\)`)
+ mraidScriptRx = regexp.MustCompile(`(?is)`)
+ mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`)
+ mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`)
+ mraidJsRx = regexp.MustCompile(`(?i)\bmraid\.js\b`)
+ mraidViewportRx = regexp.MustCompile(`(?i)]*\bname\s*=\s*['"]viewport['"][^>]*>`)
+ mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`)
+ mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`)
+ mraidHyperlinkRx = regexp.MustCompile(`(?i)]*\bhref\s*=`)
+ mraidCloseUiRx = regexp.MustCompile(`(?i)\b(close|dismiss|skip)\b`)
+ mraidLoadingLeftRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*={0,2}\s*['"]loading['"]`)
+ mraidLoadingRightRx = regexp.MustCompile(`(?i)['"]loading['"]\s*={0,2}\s*mraid\s*\.\s*getState\s*\(\s*\)`)
+ mraidDomReadyRx = regexp.MustCompile(`(?i)document\s*\.\s*readyState|DOMContentLoaded|\bwindow\s*\.\s*(addEventListener\s*\(\s*['"]load['"]|onload\b)`)
+ mraidTypeofGuardRx = regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*!={1,2}\s*['"]undefined['"]`)
+ mraidReadyTimeoutRx = regexp.MustCompile(`(?is)setTimeout\s*\(.*mraid|mraid.*setTimeout\s*\(`)
+ mraidLateDetectionRx = regexp.MustCompile(`(?i)(setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\().*(window\s*\.\s*mraid|\bmraid\b)|(window\s*\.\s*mraid|\bmraid\b).*(setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\()`)
+ mraidOpenGuardRx = regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*\.\s*open\s*={2,3}\s*['"]function['"]|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*open\s*!={1,2}\s*['"]undefined['"]`)
+ mraidOpenLoadingGuardRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*mraid\s*\.\s*getState\s*\(\s*\)|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*getState\s*!={1,2}\s*['"]function['"]`)
+ mraidVariableLoadingGuardRx = regexp.MustCompile(`(?i)\b\w+\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*\w+\b`)
+ mraidLoadingFallbackRx = regexp.MustCompile(`(?is)\b\w+\s*={2,3}\s*['"]loading['"].{0,500}\bwindow\s*\.\s*open\s*\(`)
+ mraidAudioSafeRx = regexp.MustCompile(`(?i)typeof\s+\w+\s*={2,3}\s*['"]number['"]|!=\s*null|!==\s*null`)
+ mraidTwoPartExpandRx = regexp.MustCompile(`(?i)mraid\s*\.\s*expand\s*\(\s*[^)\s]`)
+ mraidVideoOpenRx = regexp.MustCompile(`(?i)mraid\s*\.\s*open\s*\(\s*[^)]*\.(mp4|m4v|mov|webm|avi|m3u8)(['"` + "`" + `?#)]|$)`)
+ mraidRejectedScriptAttrsRx = regexp.MustCompile(`(?i)